mirror of
https://gitee.com/zhangsken/AndroidIDE.git
synced 2026-10-16 07:49:44 +08:00
feat(projects): implement project deserialization and transformation (1/2)
This commit is contained in:
@@ -412,7 +412,7 @@ public class GradleBuildService extends Service implements BuildService, IToolin
|
||||
public CompletableFuture<TaskExecutionResult> executeTasks(@NonNull String... tasks) {
|
||||
checkServerStarted();
|
||||
final var message =
|
||||
new TaskExecutionMessage(":", Arrays.asList(tasks), getGradleInstallationDir());
|
||||
new TaskExecutionMessage(IProject.ROOT_PROJECT_PATH, Arrays.asList(tasks), getGradleInstallationDir());
|
||||
return performBuildTasks(server.executeTasks(message));
|
||||
}
|
||||
|
||||
|
||||
+47
-137
@@ -19,16 +19,16 @@ package com.itsaky.androidide.projects
|
||||
|
||||
import androidx.annotation.RestrictTo
|
||||
import androidx.annotation.RestrictTo.Scope.LIBRARY
|
||||
import com.itsaky.androidide.builder.model.DefaultProjectSyncIssues
|
||||
import com.itsaky.androidide.tooling.api.IAndroidProject
|
||||
import com.itsaky.androidide.tooling.api.IGradleProject
|
||||
import com.itsaky.androidide.tooling.api.IJavaProject
|
||||
import com.itsaky.androidide.tooling.api.IProject
|
||||
import com.itsaky.androidide.tooling.api.IProject.Type
|
||||
import com.itsaky.androidide.tooling.api.messages.VariantDataRequest
|
||||
import com.itsaky.androidide.tooling.api.messages.result.SimpleModuleData
|
||||
import com.itsaky.androidide.tooling.api.messages.result.SimpleVariantData
|
||||
import com.itsaky.androidide.tooling.api.model.AndroidModule
|
||||
import com.itsaky.androidide.tooling.api.model.GradleTask
|
||||
import com.itsaky.androidide.tooling.api.model.IdeGradleProject
|
||||
import com.itsaky.androidide.tooling.api.ProjectType
|
||||
import com.itsaky.androidide.tooling.api.models.BasicProjectMetadata
|
||||
import com.itsaky.androidide.tooling.api.models.params.StringParameter
|
||||
import com.itsaky.androidide.tooling.api.models.result.SelectProjectResult
|
||||
import com.itsaky.androidide.utils.ILogger
|
||||
import java.io.File
|
||||
import java.util.concurrent.*
|
||||
|
||||
/**
|
||||
@@ -41,156 +41,66 @@ open class CachingProject(val project: IProject) : IProject {
|
||||
|
||||
private val log = ILogger.newInstance(javaClass.simpleName)
|
||||
|
||||
private val mName: String by lazy { this.project.name.get() ?: "" }
|
||||
private val mDescription: String by lazy { this.project.description.get() ?: "" }
|
||||
private val mProjectPath: String by lazy { this.project.projectPath.get() ?: "" }
|
||||
private val mProjectDir: File by lazy {
|
||||
this.project.projectDir.get() ?: File(IProject.FILE_PATH_NOT_AVAILABLE)
|
||||
}
|
||||
private val mBuildDir: File by lazy {
|
||||
this.project.buildDir.get() ?: File(IProject.FILE_PATH_NOT_AVAILABLE)
|
||||
}
|
||||
private val mBuildScript: File by lazy {
|
||||
this.project.buildScript.get() ?: File(IProject.FILE_PATH_NOT_AVAILABLE)
|
||||
}
|
||||
private val mProjectType: Type by lazy { this.project.type.get() ?: Type.Gradle }
|
||||
private val mTasks = mutableListOf<GradleTask>()
|
||||
private val projects = mutableListOf<BasicProjectMetadata>()
|
||||
private val syncIssues = mutableMapOf<String, DefaultProjectSyncIssues>()
|
||||
|
||||
private var mFirstAppModule: AndroidModule? = null
|
||||
private val mCachedVariants: MutableMap<VariantDataRequest, SimpleVariantData> = mutableMapOf()
|
||||
private val mCachedProjects: MutableMap<String, IdeGradleProject> = mutableMapOf()
|
||||
private val mModules: MutableList<SimpleModuleData> = mutableListOf()
|
||||
|
||||
override fun isProjectInitialized(): CompletableFuture<Boolean> {
|
||||
return CompletableFuture.completedFuture(true)
|
||||
}
|
||||
|
||||
override fun getName(): CompletableFuture<String> {
|
||||
return CompletableFuture.completedFuture(this.mName)
|
||||
}
|
||||
|
||||
override fun getDescription(): CompletableFuture<String> {
|
||||
return CompletableFuture.completedFuture(mDescription)
|
||||
}
|
||||
|
||||
override fun getProjectPath(): CompletableFuture<String> {
|
||||
return CompletableFuture.completedFuture(mProjectPath)
|
||||
}
|
||||
|
||||
override fun getType(): CompletableFuture<Type> {
|
||||
return CompletableFuture.completedFuture(mProjectType)
|
||||
}
|
||||
|
||||
override fun getProjectDir(): CompletableFuture<File> {
|
||||
return CompletableFuture.completedFuture(mProjectDir)
|
||||
}
|
||||
|
||||
override fun getBuildDir(): CompletableFuture<File> {
|
||||
return CompletableFuture.completedFuture(mBuildDir)
|
||||
}
|
||||
|
||||
override fun getBuildScript(): CompletableFuture<File> {
|
||||
return CompletableFuture.completedFuture(mBuildScript)
|
||||
}
|
||||
|
||||
override fun getTasks(): CompletableFuture<MutableList<GradleTask>> {
|
||||
if (mTasks.isNotEmpty()) {
|
||||
return CompletableFuture.completedFuture(mTasks)
|
||||
}
|
||||
|
||||
return this.project.tasks.whenComplete { tasks, throwable ->
|
||||
if (throwable != null) {
|
||||
log.warn("Unable to get tasks of project '$mName'", throwable)
|
||||
override fun getProjects(): CompletableFuture<List<BasicProjectMetadata>> {
|
||||
return if (this.projects.isNotEmpty()) {
|
||||
log.info("Using cached project metadata...")
|
||||
CompletableFuture.completedFuture(this.projects)
|
||||
} else this.project.getProjects().whenComplete { projects, err ->
|
||||
if (err != null || projects == null) {
|
||||
log.debug("Unable to fetch project metadata from tooling server", err)
|
||||
return@whenComplete
|
||||
}
|
||||
|
||||
if (tasks == null || tasks.isEmpty()) {
|
||||
log.warn("No tasks found in project '$mName'")
|
||||
if (projects.isEmpty()) {
|
||||
log.debug("Empty project metadata returned by tooling server. Ignoring...")
|
||||
return@whenComplete
|
||||
}
|
||||
|
||||
mTasks.clear()
|
||||
mTasks.addAll(tasks)
|
||||
this.projects.clear()
|
||||
this.projects.addAll(projects)
|
||||
}
|
||||
}
|
||||
|
||||
override fun listModules(): CompletableFuture<MutableList<SimpleModuleData>> {
|
||||
return if (this.mModules.isNotEmpty()) {
|
||||
log.debug("Using cached module data...")
|
||||
CompletableFuture.completedFuture(mModules)
|
||||
} else {
|
||||
return this.project.listModules().whenComplete { modules, err ->
|
||||
if (err != null || modules == null) {
|
||||
log.debug("Unable to fetch module data from tooling server", err)
|
||||
return@whenComplete
|
||||
}
|
||||
|
||||
if (modules.isEmpty()) {
|
||||
log.debug("Empty module data returned by tooling server. Ignoring...")
|
||||
return@whenComplete
|
||||
}
|
||||
|
||||
mModules.clear()
|
||||
mModules.addAll(modules)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getVariantData(request: VariantDataRequest): CompletableFuture<SimpleVariantData> {
|
||||
val cached = mCachedVariants[request]
|
||||
if (cached != null) {
|
||||
return CompletableFuture.completedFuture(cached)
|
||||
}
|
||||
|
||||
return this.project.getVariantData(request).whenComplete { t, u ->
|
||||
if (t == null) {
|
||||
log.warn(
|
||||
"Unable to get variant data for " +
|
||||
"variant ${request.variantName} ('${request.projectPath}') from tooling server",
|
||||
u
|
||||
)
|
||||
override fun getProjectSyncIssues(): CompletableFuture<Map<String, DefaultProjectSyncIssues>> {
|
||||
return if (this.syncIssues.isNotEmpty()) {
|
||||
log.info("Using cached sync issues...")
|
||||
CompletableFuture.completedFuture(this.syncIssues)
|
||||
} else this.project.getProjectSyncIssues().whenComplete { syncIssues, err ->
|
||||
if (err != null || syncIssues == null) {
|
||||
log.debug("Unable to fetch project sync issues from tooling server", err)
|
||||
return@whenComplete
|
||||
}
|
||||
|
||||
mCachedVariants[request] = t
|
||||
}
|
||||
}
|
||||
|
||||
override fun findByPath(path: String): CompletableFuture<IdeGradleProject> {
|
||||
if (this.mCachedProjects.containsKey(path)) {
|
||||
return CompletableFuture.completedFuture(mCachedProjects[path]!!)
|
||||
}
|
||||
|
||||
return this.project.findByPath(path).whenComplete { project, throwable ->
|
||||
if (throwable != null) {
|
||||
log.warn("Unable to find project with path: '$path'", throwable)
|
||||
if (syncIssues.isEmpty()) {
|
||||
log.debug("No sync issues.")
|
||||
return@whenComplete
|
||||
}
|
||||
|
||||
if (project == null) {
|
||||
log.warn("Project with path '$path' not found")
|
||||
return@whenComplete
|
||||
}
|
||||
|
||||
mCachedProjects[path] = project
|
||||
this.syncIssues.clear()
|
||||
this.syncIssues.putAll(syncIssues)
|
||||
}
|
||||
}
|
||||
|
||||
override fun findAndroidModules(): CompletableFuture<MutableList<AndroidModule>> {
|
||||
return this.project.findAndroidModules()
|
||||
override fun selectProject(param: StringParameter): CompletableFuture<SelectProjectResult> {
|
||||
return project.selectProject(param)
|
||||
}
|
||||
|
||||
override fun findFirstAndroidModule(): CompletableFuture<AndroidModule> {
|
||||
return this.project.findFirstAndroidModule()
|
||||
override fun getType(): CompletableFuture<ProjectType> {
|
||||
return project.getType()
|
||||
}
|
||||
|
||||
override fun findFirstAndroidAppModule(): CompletableFuture<AndroidModule> {
|
||||
if (mFirstAppModule != null) {
|
||||
return CompletableFuture.completedFuture(mFirstAppModule)
|
||||
}
|
||||
|
||||
return this.project.findFirstAndroidAppModule().whenComplete { module, _ ->
|
||||
this.mFirstAppModule = module
|
||||
}
|
||||
override fun asGradleProject(): IGradleProject {
|
||||
return project.asGradleProject()
|
||||
}
|
||||
}
|
||||
|
||||
override fun asAndroidProject(): IAndroidProject {
|
||||
return project.asAndroidProject()
|
||||
}
|
||||
|
||||
override fun asJavaProject(): IJavaProject {
|
||||
return project.asJavaProject()
|
||||
}
|
||||
}
|
||||
+16
-20
@@ -32,12 +32,11 @@ import com.itsaky.androidide.builder.model.DefaultSourceSetContainer
|
||||
import com.itsaky.androidide.builder.model.DefaultViewBindingOptions
|
||||
import com.itsaky.androidide.builder.model.UNKNOWN_PACKAGE
|
||||
import com.itsaky.androidide.projects.ProjectManager
|
||||
import com.itsaky.androidide.tooling.api.IProject.Type.Android
|
||||
import com.itsaky.androidide.tooling.api.messages.result.SimpleModuleData
|
||||
import com.itsaky.androidide.tooling.api.messages.result.SimpleVariantData
|
||||
import com.itsaky.androidide.tooling.api.model.AndroidModule.Companion.FD_INTERMEDIATES
|
||||
import com.itsaky.androidide.tooling.api.model.GradleTask
|
||||
import com.itsaky.androidide.tooling.api.model.util.findPackageName
|
||||
import com.itsaky.androidide.tooling.api.IAndroidProject
|
||||
import com.itsaky.androidide.tooling.api.ProjectType.Android
|
||||
import com.itsaky.androidide.tooling.api.models.BasicAndroidVariantMetadata
|
||||
import com.itsaky.androidide.tooling.api.models.GradleTask
|
||||
import com.itsaky.androidide.tooling.api.util.findPackageName
|
||||
import com.itsaky.androidide.utils.ILogger
|
||||
import com.itsaky.androidide.xml.resources.ResourceTableRegistry
|
||||
import com.itsaky.androidide.xml.versions.ApiVersions
|
||||
@@ -68,7 +67,7 @@ import java.util.concurrent.CompletableFuture
|
||||
* @param projectType The type of Android project. See [ProjectType].
|
||||
* @param mainSourceSet The main source of this module.
|
||||
* @param flags The Android Gradle Plugin flags. No-op currently.
|
||||
* @param javaCompileOptions The java compilation options as configured in buildscript.
|
||||
* @param compilerSettings The java compilation options as configured in buildscript.
|
||||
* @param viewBindingOptions The view binding options of this module.
|
||||
* @param bootClassPaths The boot class paths of the project. Usually contains the path to the
|
||||
* `android.jar` file.
|
||||
@@ -97,15 +96,15 @@ open class AndroidModule( // Class must be open because BaseXMLTest mocks this..
|
||||
val projectType: ProjectType,
|
||||
val mainSourceSet: DefaultSourceSetContainer?,
|
||||
val flags: DefaultAndroidGradlePluginProjectFlags,
|
||||
val javaCompileOptions: DefaultJavaCompileOptions,
|
||||
override val compilerSettings: DefaultJavaCompileOptions,
|
||||
val viewBindingOptions: DefaultViewBindingOptions,
|
||||
val bootClassPaths: Collection<File>,
|
||||
val libraries: Set<String>,
|
||||
val libraryMap: Map<String, DefaultLibrary>,
|
||||
val dynamicFeatures: Collection<String>?,
|
||||
val lintCheckJars: List<File>,
|
||||
val modelSyncFiles: List<DefaultModelSyncFile>,
|
||||
val variants: List<SimpleVariantData> = listOf()
|
||||
val variants: List<BasicAndroidVariantMetadata> = listOf(),
|
||||
val classesJar: File?
|
||||
) :
|
||||
ModuleProject(
|
||||
name,
|
||||
@@ -114,27 +113,24 @@ open class AndroidModule( // Class must be open because BaseXMLTest mocks this..
|
||||
projectDir,
|
||||
buildDir,
|
||||
buildScript,
|
||||
tasks,
|
||||
javaCompileOptions
|
||||
),
|
||||
WithModuleData {
|
||||
tasks
|
||||
) {
|
||||
|
||||
private val log = ILogger.newInstance(javaClass.simpleName)
|
||||
override var moduleData: SimpleModuleData? = null
|
||||
|
||||
init {
|
||||
type = Android
|
||||
}
|
||||
|
||||
override fun getGeneratedJar(variant: String): File {
|
||||
return File(buildDir, "${FD_INTERMEDIATES}/compile_library_classes_jar/$variant/classes.jar")
|
||||
fun getGeneratedJar(variant: String): File {
|
||||
return classesJar ?: File("does-not-exist.jar")
|
||||
}
|
||||
|
||||
override fun getClassPaths(): Set<File> {
|
||||
return getModuleClasspaths()
|
||||
}
|
||||
|
||||
fun getVariant(name: String): SimpleVariantData? {
|
||||
fun getVariant(name: String): BasicAndroidVariantMetadata? {
|
||||
return this.variants.firstOrNull { it.name == name }
|
||||
}
|
||||
|
||||
@@ -355,8 +351,8 @@ open class AndroidModule( // Class must be open because BaseXMLTest mocks this..
|
||||
libraryMap.values
|
||||
.filter { library ->
|
||||
library.type == ANDROID_LIBRARY &&
|
||||
library.androidLibraryData!!.resFolder.exists() &&
|
||||
library.findPackageName() != UNKNOWN_PACKAGE
|
||||
library.androidLibraryData!!.resFolder.exists() &&
|
||||
library.findPackageName() != UNKNOWN_PACKAGE
|
||||
}
|
||||
.mapNotNull { library ->
|
||||
ResourceTableRegistry.getInstance()
|
||||
|
||||
+15
-26
@@ -19,12 +19,12 @@ package com.itsaky.androidide.projects.api
|
||||
|
||||
import com.itsaky.androidide.builder.model.IJavaCompilerSettings
|
||||
import com.itsaky.androidide.projects.ProjectManager
|
||||
import com.itsaky.androidide.tooling.api.IProject.Type.Java
|
||||
import com.itsaky.androidide.tooling.api.model.GradleTask
|
||||
import com.itsaky.androidide.tooling.api.model.JavaContentRoot
|
||||
import com.itsaky.androidide.tooling.api.model.JavaModuleDependency
|
||||
import com.itsaky.androidide.tooling.api.model.JavaModuleExternalDependency
|
||||
import com.itsaky.androidide.tooling.api.model.JavaModuleProjectDependency
|
||||
import com.itsaky.androidide.tooling.api.ProjectType.Java
|
||||
import com.itsaky.androidide.tooling.api.models.JavaModuleExternalDependency
|
||||
import com.itsaky.androidide.tooling.api.models.JavaModuleProjectDependency
|
||||
import com.itsaky.androidide.tooling.api.models.GradleTask
|
||||
import com.itsaky.androidide.tooling.api.models.JavaContentRoot
|
||||
import com.itsaky.androidide.tooling.api.models.JavaModuleDependency
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
@@ -51,9 +51,10 @@ class JavaModule(
|
||||
buildDir: File,
|
||||
buildScript: File,
|
||||
tasks: List<GradleTask>,
|
||||
compilerSettings: IJavaCompilerSettings,
|
||||
override val compilerSettings: IJavaCompilerSettings,
|
||||
val contentRoots: List<JavaContentRoot>,
|
||||
val dependencies: List<JavaModuleDependency>,
|
||||
val classesJar: File?
|
||||
) :
|
||||
ModuleProject(
|
||||
name,
|
||||
@@ -62,11 +63,11 @@ class JavaModule(
|
||||
projectDir,
|
||||
buildDir,
|
||||
buildScript,
|
||||
tasks,
|
||||
compilerSettings
|
||||
tasks
|
||||
) {
|
||||
|
||||
companion object {
|
||||
|
||||
const val SCOPE_COMPILE = "COMPILE"
|
||||
const val SCOPE_RUNTIME = "RUNTIME"
|
||||
}
|
||||
@@ -75,19 +76,6 @@ class JavaModule(
|
||||
type = Java
|
||||
}
|
||||
|
||||
override fun getGeneratedJar(variant: String): File {
|
||||
var jar = File(buildDir, "libs/$name.jar")
|
||||
if (jar.exists()) {
|
||||
return jar
|
||||
}
|
||||
|
||||
jar =
|
||||
File(buildDir, "libs").listFiles()?.first { it.name.startsWith(this.name) }
|
||||
?: File("module-jar-does-not-exist.jar")
|
||||
|
||||
return jar
|
||||
}
|
||||
|
||||
override fun getClassPaths(): Set<File> {
|
||||
return getModuleClasspaths()
|
||||
}
|
||||
@@ -107,7 +95,7 @@ class JavaModule(
|
||||
}
|
||||
|
||||
override fun getModuleClasspaths(): Set<File> {
|
||||
return mutableSetOf(getGeneratedJar(""))
|
||||
return mutableSetOf(classesJar ?: File("does-not-exist.jar"))
|
||||
}
|
||||
|
||||
override fun getCompileClasspaths(): Set<File> {
|
||||
@@ -116,7 +104,7 @@ class JavaModule(
|
||||
classpaths.addAll(getDependencyClasspaths())
|
||||
return classpaths
|
||||
}
|
||||
|
||||
|
||||
override fun getCompileModuleProjects(): List<ModuleProject> {
|
||||
val root = ProjectManager.rootProject ?: return emptyList()
|
||||
return this.dependencies
|
||||
@@ -125,8 +113,9 @@ class JavaModule(
|
||||
.mapNotNull { root.findByPath(it.projectPath) }
|
||||
.filterIsInstance(ModuleProject::class.java)
|
||||
}
|
||||
|
||||
|
||||
fun getDependencyClasspaths(): Set<File> {
|
||||
return this.dependencies.filterIsInstance<JavaModuleExternalDependency>().mapNotNull { it.jarFile }.toHashSet()
|
||||
return this.dependencies.filterIsInstance<JavaModuleExternalDependency>()
|
||||
.mapNotNull { it.jarFile }.toHashSet()
|
||||
}
|
||||
}
|
||||
|
||||
+19
-8
@@ -23,7 +23,8 @@ import com.itsaky.androidide.javac.services.fs.CacheFSInfoSingleton
|
||||
import com.itsaky.androidide.lookup.Lookup
|
||||
import com.itsaky.androidide.projects.classpath.JarFsClasspathReader
|
||||
import com.itsaky.androidide.projects.util.BootClasspathProvider
|
||||
import com.itsaky.androidide.tooling.api.model.GradleTask
|
||||
import com.itsaky.androidide.tooling.api.IModuleProject
|
||||
import com.itsaky.androidide.tooling.api.models.GradleTask
|
||||
import com.itsaky.androidide.utils.ClassTrie
|
||||
import com.itsaky.androidide.utils.DocumentUtils
|
||||
import com.itsaky.androidide.utils.ILogger
|
||||
@@ -46,20 +47,24 @@ abstract class ModuleProject(
|
||||
projectDir: File,
|
||||
buildDir: File,
|
||||
buildScript: File,
|
||||
tasks: List<GradleTask>,
|
||||
override val compilerSettings: IJavaCompilerSettings
|
||||
tasks: List<GradleTask>
|
||||
) :
|
||||
Project(name, description, path, projectDir, buildDir, buildScript, tasks),
|
||||
com.itsaky.androidide.tooling.api.model.ModuleProject {
|
||||
Project(name, description, path, projectDir, buildDir, buildScript, tasks) {
|
||||
|
||||
private val log = ILogger.newInstance(javaClass.simpleName)
|
||||
|
||||
abstract val compilerSettings: IJavaCompilerSettings
|
||||
|
||||
companion object {
|
||||
@JvmStatic val COMPLETION_MODULE_KEY = Lookup.Key<ModuleProject>()
|
||||
|
||||
@JvmStatic
|
||||
val COMPLETION_MODULE_KEY = Lookup.Key<ModuleProject>()
|
||||
}
|
||||
|
||||
@JvmField val compileJavaSourceClasses = SourceClassTrie()
|
||||
@JvmField val compileClasspathClasses = ClassTrie()
|
||||
@JvmField
|
||||
val compileJavaSourceClasses = SourceClassTrie()
|
||||
@JvmField
|
||||
val compileClasspathClasses = ClassTrie()
|
||||
|
||||
/**
|
||||
* Get the source directories of this module (non-transitive i.e for this module only).
|
||||
@@ -76,6 +81,12 @@ abstract class ModuleProject(
|
||||
*/
|
||||
abstract fun getCompileSourceDirectories(): Set<File>
|
||||
|
||||
/**
|
||||
* Get the classpaths for this module project. The returned list always included the
|
||||
* `classes.jar`.
|
||||
*/
|
||||
abstract fun getClassPaths() : Set<File>
|
||||
|
||||
/**
|
||||
* Get the JAR files for this module. This does not include JAR files of any dependencies.
|
||||
*
|
||||
|
||||
@@ -19,9 +19,8 @@ package com.itsaky.androidide.projects.api
|
||||
|
||||
import com.android.builder.model.v2.ide.ProjectType.APPLICATION
|
||||
import com.itsaky.androidide.tooling.api.IProject
|
||||
import com.itsaky.androidide.tooling.api.IProject.Type
|
||||
import com.itsaky.androidide.tooling.api.IProject.Type.Gradle
|
||||
import com.itsaky.androidide.tooling.api.model.GradleTask
|
||||
import com.itsaky.androidide.tooling.api.ProjectType
|
||||
import com.itsaky.androidide.tooling.api.models.GradleTask
|
||||
import java.io.File
|
||||
import java.nio.file.Path
|
||||
import java.util.concurrent.*
|
||||
@@ -53,7 +52,7 @@ open class Project(
|
||||
val subModules: List<Project> = mutableListOf()
|
||||
) {
|
||||
|
||||
var type: Type = Gradle
|
||||
var type: ProjectType = ProjectType.Gradle
|
||||
protected set
|
||||
|
||||
/**
|
||||
|
||||
+81
-62
@@ -17,17 +17,17 @@
|
||||
|
||||
package com.itsaky.androidide.projects.util
|
||||
|
||||
import com.android.builder.model.v2.ide.ProjectType.LIBRARY
|
||||
import com.itsaky.androidide.builder.model.DefaultViewBindingOptions
|
||||
import com.itsaky.androidide.projects.api.AndroidModule
|
||||
import com.itsaky.androidide.projects.api.JavaModule
|
||||
import com.itsaky.androidide.projects.api.Project
|
||||
import com.itsaky.androidide.tooling.api.IAndroidProject
|
||||
import com.itsaky.androidide.tooling.api.IJavaProject
|
||||
import com.itsaky.androidide.tooling.api.IProject
|
||||
import com.itsaky.androidide.tooling.api.IProject.Type.Android
|
||||
import com.itsaky.androidide.tooling.api.IProject.Type.Gradle
|
||||
import com.itsaky.androidide.tooling.api.IProject.Type.Java
|
||||
import com.itsaky.androidide.tooling.api.IProject.Type.Unknown
|
||||
import com.itsaky.androidide.tooling.api.messages.result.SimpleModuleData
|
||||
import com.itsaky.androidide.tooling.api.ProjectType
|
||||
import com.itsaky.androidide.tooling.api.models.AndroidProjectMetadata
|
||||
import com.itsaky.androidide.tooling.api.models.BasicProjectMetadata
|
||||
import com.itsaky.androidide.tooling.api.models.JavaProjectMetadata
|
||||
import com.itsaky.androidide.tooling.api.models.params.StringParameter
|
||||
import com.itsaky.androidide.utils.ILogger
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
|
||||
@@ -42,19 +42,32 @@ class ProjectTransformer {
|
||||
|
||||
fun transform(project: IProject): Project? {
|
||||
try {
|
||||
val path = project.projectPath.get()
|
||||
val allProjects = project.getProjects().get()
|
||||
val selectionResult = project.selectProject(StringParameter(IProject.ROOT_PROJECT_PATH)).get()
|
||||
check(selectionResult.isSuccessful) {
|
||||
"Cannot find root project with path '${IProject.ROOT_PROJECT_PATH}'"
|
||||
}
|
||||
|
||||
val rootProject = when (project.getType().get()) {
|
||||
ProjectType.Gradle -> project.asGradleProject()
|
||||
ProjectType.Android -> project.asGradleProject()
|
||||
else -> throw IllegalStateException(
|
||||
"Root project must be either an Android project or a Gradle project")
|
||||
}
|
||||
|
||||
val metadata = rootProject.getMetadata().get()
|
||||
return Project(
|
||||
name = project.name.get(),
|
||||
description = project.description.get() ?: "",
|
||||
path = path,
|
||||
projectDir = project.projectDir.get(),
|
||||
buildDir = project.buildDir.get(),
|
||||
buildScript = project.buildScript.get(),
|
||||
name = metadata.name ?: IProject.PROJECT_UNKNOWN,
|
||||
description = metadata.description ?: "",
|
||||
path = metadata.projectPath,
|
||||
projectDir = metadata.projectDir,
|
||||
buildDir = metadata.buildDir,
|
||||
buildScript = metadata.buildScript,
|
||||
|
||||
// As these lists will never change, we could make these thread-safe with
|
||||
// CopyOnWriteArrayList
|
||||
tasks = CopyOnWriteArrayList(project.tasks.get() ?: listOf()),
|
||||
subModules = CopyOnWriteArrayList(transform(project.listModules().get(), project))
|
||||
tasks = CopyOnWriteArrayList(rootProject.getTasks().get() ?: listOf()),
|
||||
subModules = CopyOnWriteArrayList(transform(allProjects, project))
|
||||
)
|
||||
} catch (error: Throwable) {
|
||||
log.error("Unable to transform project", error)
|
||||
@@ -63,52 +76,56 @@ class ProjectTransformer {
|
||||
}
|
||||
|
||||
private fun transform(
|
||||
project: com.itsaky.androidide.tooling.api.model.AndroidModule
|
||||
project: IAndroidProject
|
||||
): AndroidModule {
|
||||
val metadata = project.getMetadata().get() as AndroidProjectMetadata
|
||||
val libraryMap = project.getLibraryMap().get()
|
||||
return AndroidModule(
|
||||
name = project.name,
|
||||
description = project.description ?: "",
|
||||
path = project.projectPath,
|
||||
projectDir = project.projectDir,
|
||||
buildDir = project.buildDir,
|
||||
buildScript = project.buildScript,
|
||||
tasks = project.tasks,
|
||||
packageName = project.packageName,
|
||||
resourcePrefix = project.resourcePrefix,
|
||||
namespace = project.namespace,
|
||||
androidTestNamespace = project.androidTestNamespace,
|
||||
testFixtureNamespace = project.testFixturesNamespace,
|
||||
projectType = project.projectType ?: LIBRARY,
|
||||
mainSourceSet = project.mainSourceSet,
|
||||
flags = project.flags,
|
||||
javaCompileOptions = project.javaCompileOptions,
|
||||
viewBindingOptions = project.viewBindingOptions ?: DefaultViewBindingOptions(),
|
||||
bootClassPaths = project.bootClassPaths,
|
||||
libraries = project.libraries,
|
||||
libraryMap = project.libraryMap,
|
||||
dynamicFeatures = project.dynamicFeatures,
|
||||
lintCheckJars = project.lintChecksJars,
|
||||
modelSyncFiles = project.modelSyncFiles,
|
||||
variants = project.simpleVariants
|
||||
name = metadata.name ?: IProject.PROJECT_UNKNOWN,
|
||||
description = metadata.description ?: "",
|
||||
path = metadata.projectPath,
|
||||
projectDir = metadata.projectDir,
|
||||
buildDir = metadata.buildDir,
|
||||
buildScript = metadata.buildScript,
|
||||
tasks = project.getTasks().get(),
|
||||
packageName = metadata.packageName,
|
||||
resourcePrefix = metadata.resourcePrefix,
|
||||
namespace = metadata.namespace,
|
||||
androidTestNamespace = metadata.androidTestNamespace,
|
||||
testFixtureNamespace = metadata.testFixtureNamespace,
|
||||
projectType = metadata.androidType,
|
||||
mainSourceSet = project.getMainSourceSet().get(),
|
||||
flags = metadata.flags,
|
||||
compilerSettings = metadata.javaCompileOptions,
|
||||
viewBindingOptions = metadata.viewBindingOptions,
|
||||
bootClassPaths = project.getBootClasspaths().get(),
|
||||
libraries = libraryMap.keys,
|
||||
libraryMap = libraryMap,
|
||||
lintCheckJars = project.getLintCheckJars().get(),
|
||||
modelSyncFiles = project.getModelSyncFiles().get(),
|
||||
variants = project.getVariants().get(),
|
||||
classesJar = metadata.classesJar
|
||||
)
|
||||
}
|
||||
|
||||
private fun transform(project: com.itsaky.androidide.tooling.api.model.JavaModule): JavaModule {
|
||||
private fun transform(project: IJavaProject): JavaModule {
|
||||
val metadata = project.getMetadata() as JavaProjectMetadata
|
||||
return JavaModule(
|
||||
name = project.name,
|
||||
description = project.description ?: "",
|
||||
path = project.projectPath,
|
||||
projectDir = project.projectDir,
|
||||
buildDir = project.buildDir,
|
||||
buildScript = project.buildScript,
|
||||
tasks = project.tasks,
|
||||
contentRoots = project.contentRoots,
|
||||
dependencies = project.javaDependencies,
|
||||
compilerSettings = project.compilerSettings
|
||||
name = metadata.name ?: IProject.PROJECT_UNKNOWN,
|
||||
description = metadata.description ?: "",
|
||||
path = metadata.projectPath,
|
||||
projectDir = metadata.projectDir,
|
||||
buildDir = metadata.buildDir,
|
||||
buildScript = metadata.buildScript,
|
||||
tasks = project.getTasks().get(),
|
||||
contentRoots = project.getContentRoots().get(),
|
||||
dependencies = project.getDependencies().get(),
|
||||
compilerSettings = metadata.compilerSettings,
|
||||
classesJar = metadata.classesJar
|
||||
)
|
||||
}
|
||||
|
||||
private fun transform(modules: MutableList<SimpleModuleData>, root: IProject): List<Project> {
|
||||
private fun transform(modules: List<BasicProjectMetadata>, root: IProject): List<Project> {
|
||||
return mutableListOf<Project>().apply {
|
||||
for (module in modules) {
|
||||
add(createProject(module, root))
|
||||
@@ -116,17 +133,19 @@ class ProjectTransformer {
|
||||
}
|
||||
}
|
||||
|
||||
private fun createProject(module: SimpleModuleData, root: IProject): Project {
|
||||
val project =
|
||||
root.findByPath(module.path).get()
|
||||
?: throw java.lang.IllegalStateException("Invalid module data")
|
||||
val type = project.type.get() ?: throw java.lang.IllegalStateException("Invalid module data")
|
||||
private fun createProject(moduleMetadata: BasicProjectMetadata, root: IProject): Project {
|
||||
val selectionResult = root.selectProject(StringParameter(moduleMetadata.projectPath)).get()
|
||||
check(selectionResult.isSuccessful) {
|
||||
"Selection failed for project '${moduleMetadata.projectPath}' but it is included in all projects."
|
||||
}
|
||||
|
||||
val type = root.getType().get() ?: throw java.lang.IllegalStateException("Invalid module data")
|
||||
|
||||
return when (type) {
|
||||
Gradle,
|
||||
Unknown -> transform(project)!!
|
||||
Android -> transform(project as com.itsaky.androidide.tooling.api.model.AndroidModule)
|
||||
Java -> transform(project as com.itsaky.androidide.tooling.api.model.JavaModule)
|
||||
ProjectType.Gradle,
|
||||
ProjectType.Unknown -> transform(project)!!
|
||||
ProjectType.Android -> transform(project as IAndroidProject)
|
||||
ProjectType.Java -> transform(project as IJavaProject)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -71,9 +71,9 @@ class ModuleProjectTest {
|
||||
assertThat(app.compilerSettings.javaSourceVersion).isEqualTo("11")
|
||||
assertThat(app.compilerSettings.javaBytecodeVersion).isEqualTo("11")
|
||||
assertThat(app.compilerSettings.javaSourceVersion)
|
||||
.isEqualTo(app.javaCompileOptions.sourceCompatibility)
|
||||
.isEqualTo(app.compilerSettings.sourceCompatibility)
|
||||
assertThat(app.compilerSettings.javaBytecodeVersion)
|
||||
.isEqualTo(app.javaCompileOptions.targetCompatibility)
|
||||
.isEqualTo(app.compilerSettings.targetCompatibility)
|
||||
|
||||
val anotherAndroidLib = root.findByPath(":another-android-library")
|
||||
assertThat(anotherAndroidLib).isNotNull()
|
||||
|
||||
+1
-1
@@ -148,7 +148,7 @@ internal class ToolingApiServerImpl(private val project: ProjectImpl) :
|
||||
|
||||
var projectPath = message.projectPath
|
||||
if (projectPath == null) {
|
||||
projectPath = ":"
|
||||
projectPath = IProject.ROOT_PROJECT_PATH
|
||||
}
|
||||
|
||||
val project = this.project!!.run {
|
||||
|
||||
+6
-7
@@ -158,18 +158,16 @@ internal class AndroidProjectImpl(
|
||||
return CompletableFuture.supplyAsync { copy(androidProject.modelSyncFiles) }
|
||||
}
|
||||
|
||||
override fun getClassesJar(): CompletableFuture<File> {
|
||||
private fun getClassesJar(): File {
|
||||
// TODO(itsaky): this should handle product flavors as well
|
||||
return CompletableFuture.supplyAsync {
|
||||
File(gradleProject.buildDirectory,
|
||||
"${IAndroidProject.FD_INTERMEDIATES}/compile_library_classes_jar/$variant/classes.jar")
|
||||
}
|
||||
return File(gradleProject.buildDirectory,
|
||||
"${IAndroidProject.FD_INTERMEDIATES}/compile_library_classes_jar/$variant/classes.jar")
|
||||
}
|
||||
|
||||
override fun getClasspaths(): CompletableFuture<List<File>> {
|
||||
return CompletableFuture.supplyAsync {
|
||||
mutableListOf<File>().apply {
|
||||
add(getClassesJar().get())
|
||||
add(getClassesJar())
|
||||
getVariant(StringParameter(variant)).get()?.mainArtifact?.classJars?.let(this::addAll)
|
||||
}
|
||||
}
|
||||
@@ -193,7 +191,8 @@ internal class AndroidProjectImpl(
|
||||
androidProject.resourcePrefix,
|
||||
androidProject.namespace,
|
||||
androidProject.androidTestNamespace,
|
||||
androidProject.testFixturesNamespace
|
||||
androidProject.testFixturesNamespace,
|
||||
getClassesJar()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+13
-15
@@ -105,32 +105,30 @@ internal class JavaProjectImpl(
|
||||
moduleVersion.group, moduleVersion.name, moduleVersion.version)
|
||||
}
|
||||
|
||||
override fun getClassesJar(): CompletableFuture<File> {
|
||||
return CompletableFuture.supplyAsync {
|
||||
val metadata = getMetadata().get()
|
||||
var jar = File(metadata.buildDir, "libs/${metadata.name}.jar")
|
||||
if (jar.exists()) {
|
||||
return@supplyAsync jar
|
||||
}
|
||||
|
||||
jar =
|
||||
File(metadata.buildDir, "libs").listFiles()?.first { metadata.name?.let(it.name::startsWith) ?: false }
|
||||
?: File("module-jar-does-not-exist.jar")
|
||||
|
||||
return@supplyAsync jar
|
||||
private fun getClassesJar(): File {
|
||||
val metadata = getMetadata().get()
|
||||
var jar = File(metadata.buildDir, "libs/${metadata.name}.jar")
|
||||
if (jar.exists()) {
|
||||
return jar
|
||||
}
|
||||
|
||||
jar =
|
||||
File(metadata.buildDir, "libs").listFiles()?.first { metadata.name?.let(it.name::startsWith) ?: false }
|
||||
?: File("module-jar-does-not-exist.jar")
|
||||
|
||||
return jar
|
||||
}
|
||||
|
||||
override fun getClasspaths(): CompletableFuture<List<File>> {
|
||||
return CompletableFuture.supplyAsync {
|
||||
getDependencies().get().mapNotNull { it.jarFile }.toMutableList().apply { add(getClassesJar().get()) }
|
||||
getDependencies().get().mapNotNull { it.jarFile }.toMutableList().apply { add(getClassesJar()) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun getMetadata(): CompletableFuture<ProjectMetadata> {
|
||||
return CompletableFuture.supplyAsync {
|
||||
val base = super.getMetadata().get()
|
||||
return@supplyAsync JavaProjectMetadata(base, compilerSettings)
|
||||
return@supplyAsync JavaProjectMetadata(base, compilerSettings, getClassesJar())
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -52,7 +52,7 @@ internal class ProjectImpl(var rootProject: IGradleProject? = null,
|
||||
}
|
||||
|
||||
private fun getProject(path: String): IGradleProject? {
|
||||
return if (":" == path) rootProject else projects.find {
|
||||
return if (IProject.ROOT_PROJECT_PATH == path) rootProject else projects.find {
|
||||
it.getMetadata().get().projectPath == path
|
||||
}
|
||||
}
|
||||
|
||||
-5
@@ -92,11 +92,6 @@ internal class ForwardingProject(var project: IGradleProject? = null) : IGradleP
|
||||
UnsupportedOperationException())
|
||||
}
|
||||
|
||||
override fun getClassesJar(): CompletableFuture<File> {
|
||||
return this.javaProject?.getClassesJar() ?: CompletableFuture.failedFuture(
|
||||
UnsupportedOperationException())
|
||||
}
|
||||
|
||||
override fun getClasspaths(): CompletableFuture<List<File>> {
|
||||
return this.javaProject?.getClasspaths() ?: CompletableFuture.failedFuture(
|
||||
UnsupportedOperationException())
|
||||
|
||||
+2
-2
@@ -42,9 +42,9 @@ object RootModelBuilder : AbstractModelBuilder<ProjectConnectionAndAndroidVarian
|
||||
|
||||
val ideaModules = ideaProject.modules
|
||||
val modulePaths = mapOf(*ideaModules.map { it.name to it.gradleProject.path }.toTypedArray())
|
||||
val rootModule = ideaModules.find { it.gradleProject.path == ":" }
|
||||
val rootModule = ideaModules.find { it.gradleProject.path == IProject.ROOT_PROJECT_PATH }
|
||||
?: throw ModelBuilderException(
|
||||
"No GradleProject model is associated with project path: ':'")
|
||||
"No GradleProject model is associated with project path: '${IProject.ROOT_PROJECT_PATH}'")
|
||||
|
||||
val rootProjectVersions = getAndroidVersions(rootModule, controller)
|
||||
|
||||
|
||||
+1
-1
@@ -59,7 +59,7 @@ class MultiModuleAndroidProjectTest {
|
||||
val isInitialized = server.isServerInitialized().get()
|
||||
assertThat(isInitialized).isTrue()
|
||||
|
||||
var selectionResult = project.selectProject(StringParameter(":")).get()
|
||||
var selectionResult = project.selectProject(StringParameter(IProject.ROOT_PROJECT_PATH)).get()
|
||||
assertThat(selectionResult.isSuccessful).isTrue()
|
||||
assertThat(project.getType().get()).isEqualTo(ProjectType.Gradle)
|
||||
|
||||
|
||||
+1
-6
@@ -27,14 +27,9 @@ import java.util.concurrent.CompletableFuture
|
||||
*/
|
||||
interface IModuleProject : IGradleProject {
|
||||
|
||||
/**
|
||||
* Get the `classes.jar` file for this module project.
|
||||
*/
|
||||
fun getClassesJar(): CompletableFuture<File>
|
||||
|
||||
/**
|
||||
* Get the classpaths for this module project. The returned list always included the
|
||||
* `classes.jar` file from [getClassesJar].
|
||||
* `classes.jar`.
|
||||
*/
|
||||
fun getClasspaths(): CompletableFuture<List<File>>
|
||||
}
|
||||
+13
@@ -44,4 +44,17 @@ interface IProject : IProjectQueries {
|
||||
*/
|
||||
@JsonRequest
|
||||
fun getProjectSyncIssues(): CompletableFuture<Map<String, DefaultProjectSyncIssues>>
|
||||
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* Gradle project path for the root project.
|
||||
*/
|
||||
const val ROOT_PROJECT_PATH = ":"
|
||||
|
||||
/**
|
||||
* Name that can be used for project whose [BasicProjectMetadata.name] property is null.
|
||||
*/
|
||||
const val PROJECT_UNKNOWN = "Unknown"
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -65,17 +65,17 @@ interface IProjectQueries {
|
||||
* Get the selected project as Gradle project.
|
||||
*/
|
||||
@JsonDelegate
|
||||
fun asGradleProject(): IGradleProject?
|
||||
fun asGradleProject(): IGradleProject
|
||||
|
||||
/**
|
||||
* Get the selected project as Android project.
|
||||
*/
|
||||
@JsonDelegate
|
||||
fun asAndroidProject(): IAndroidProject?
|
||||
fun asAndroidProject(): IAndroidProject
|
||||
|
||||
/**
|
||||
* Get the selected project as Java project.
|
||||
*/
|
||||
@JsonDelegate
|
||||
fun asJavaProject(): IJavaProject?
|
||||
fun asJavaProject(): IJavaProject
|
||||
}
|
||||
+9
-4
@@ -21,6 +21,7 @@ import com.itsaky.androidide.builder.model.DefaultAndroidGradlePluginProjectFlag
|
||||
import com.itsaky.androidide.builder.model.DefaultJavaCompileOptions
|
||||
import com.itsaky.androidide.builder.model.DefaultViewBindingOptions
|
||||
import com.itsaky.androidide.tooling.api.ProjectType
|
||||
import com.itsaky.androidide.tooling.api.models.result.IModuleMetadata
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
@@ -44,8 +45,10 @@ class AndroidProjectMetadata(
|
||||
val resourcePrefix: String?,
|
||||
val namespace: String?,
|
||||
val androidTestNamespace: String?,
|
||||
val testFixtureNamespace: String?
|
||||
) : ProjectMetadata(name, path, projectDir, buildDir, description, buildScript, type) {
|
||||
val testFixtureNamespace: String?,
|
||||
override val classesJar: File?
|
||||
) : ProjectMetadata(name, path, projectDir, buildDir, description, buildScript, type),
|
||||
IModuleMetadata {
|
||||
|
||||
constructor(
|
||||
base: ProjectMetadata,
|
||||
@@ -57,7 +60,8 @@ class AndroidProjectMetadata(
|
||||
resourcePrefix: String?,
|
||||
namespace: String?,
|
||||
androidTestNamespace: String?,
|
||||
testFixtureNamespace: String?) :
|
||||
testFixtureNamespace: String?,
|
||||
classesJar: File?) :
|
||||
|
||||
this(
|
||||
base.name,
|
||||
@@ -75,7 +79,8 @@ class AndroidProjectMetadata(
|
||||
resourcePrefix,
|
||||
namespace,
|
||||
androidTestNamespace,
|
||||
testFixtureNamespace
|
||||
testFixtureNamespace,
|
||||
classesJar
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+7
-4
@@ -19,6 +19,7 @@ package com.itsaky.androidide.tooling.api.models
|
||||
|
||||
import com.itsaky.androidide.builder.model.IJavaCompilerSettings
|
||||
import com.itsaky.androidide.tooling.api.ProjectType
|
||||
import com.itsaky.androidide.tooling.api.models.result.IModuleMetadata
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
@@ -35,10 +36,11 @@ class JavaProjectMetadata(
|
||||
description: String?,
|
||||
buildScript: File,
|
||||
type: ProjectType,
|
||||
val compilerSettings: IJavaCompilerSettings
|
||||
) : ProjectMetadata(name, path, projectDir, buildDir, description, buildScript, type) {
|
||||
val compilerSettings: IJavaCompilerSettings,
|
||||
override val classesJar: File?
|
||||
) : ProjectMetadata(name, path, projectDir, buildDir, description, buildScript, type), IModuleMetadata {
|
||||
|
||||
constructor(base: ProjectMetadata, compilerSettings: IJavaCompilerSettings) :
|
||||
constructor(base: ProjectMetadata, compilerSettings: IJavaCompilerSettings, classesJar: File?) :
|
||||
this(
|
||||
base.name,
|
||||
base.projectPath,
|
||||
@@ -47,6 +49,7 @@ class JavaProjectMetadata(
|
||||
base.description,
|
||||
base.buildScript,
|
||||
base.type,
|
||||
compilerSettings
|
||||
compilerSettings,
|
||||
classesJar
|
||||
)
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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.api.models.result
|
||||
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Common metadata for module projects.
|
||||
*
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
interface IModuleMetadata {
|
||||
|
||||
/**
|
||||
* The JAR file for this module project.
|
||||
*/
|
||||
val classesJar: File?
|
||||
}
|
||||
Reference in New Issue
Block a user