build: reorganize and reconfigure modules to resolve dependency resolution errors

Gradle, for some reason, does not the path of modules to identify them uniquely. Instead, the 'build id' is used to do so, which consists of the maven group and artifact ID. So, even if two modules are located in separate directories, but have the name group and artifact ID, including both of them in other modules may result in dependency resolution errors.
This commit is contained in:
Akash Yadav
2024-05-29 13:11:44 +05:30
parent c0c185cfb7
commit 1efd6a04d5
2457 changed files with 1069 additions and 709 deletions
+1
View File
@@ -0,0 +1 @@
/build
+92
View File
@@ -0,0 +1,92 @@
/*
* 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/>.
*/
import com.android.SdkConstants
import com.itsaky.androidide.build.config.AGP_VERSION_MINIMUM
import com.itsaky.androidide.build.config.BuildConfig
import com.itsaky.androidide.build.config.CI
import com.itsaky.androidide.build.config.FDroidConfig
import com.itsaky.androidide.build.config.ProjectConfig
import com.itsaky.androidide.build.config.VersionUtils
import com.itsaky.androidide.build.config.downloadVersion
import com.itsaky.androidide.build.config.publishingVersion
import com.itsaky.androidide.build.config.replaceContents
import com.itsaky.androidide.build.config.simpleVersionName
import org.jetbrains.kotlin.incremental.createDirectory
plugins {
//noinspection JavaPluginLanguageLevel
id("java-library")
id("com.vanniktech.maven.publish.base")
}
groupConfig {
groupIdSuffix.set("utils")
}
description = "Information about the AndroidIDE build"
val buildInfoGenDir: Provider<Directory> = project.layout.buildDirectory.dir("generated/buildInfo")
.also { it.get().asFile.createDirectory() }
sourceSets { getByName("main").java.srcDir(buildInfoGenDir) }
tasks.create("generateBuildInfo") {
val buildInfoPath = "com/itsaky/androidide/buildinfo/BuildInfo.java"
val buildInfo = buildInfoGenDir.get().file(buildInfoPath)
val buildInfoIn = project.file("src/main/java/${buildInfoPath}.in")
doLast {
buildInfoIn.replaceContents(
dest = buildInfo.asFile,
comment = "//",
candidates =
arrayOf(
"PACKAGE_NAME" to BuildConfig.packageName,
"MVN_GROUP_ID" to BuildConfig.packageName,
"VERSION_NAME" to rootProject.version.toString(),
"VERSION_NAME_SIMPLE" to rootProject.simpleVersionName,
"VERSION_NAME_PUBLISHING" to rootProject.publishingVersion,
"VERSION_NAME_DOWNLOAD" to rootProject.downloadVersion,
"FDROID_BUILD" to FDroidConfig.isFDroidBuild.toString(),
"FDROID_BUILD_VERSION_NAME" to (FDroidConfig.fDroidVersionName ?: "null"),
"FDROID_BUILD_VERSION_CODE" to (FDroidConfig.fDroidVersionCode ?: -1).toString(),
"CI_BUILD" to CI.isCiBuild.toString(),
"CI_GIT_BRANCH" to if (CI.isGitRepo) CI.branchName else "NOT-A-GIT-REPO",
"CI_COMMIT_HASH" to if (CI.isGitRepo) CI.commitHash else "NOT-A-GIT-REPO",
"REPO_HOST" to ProjectConfig.REPO_HOST,
"REPO_OWNER" to ProjectConfig.REPO_OWNER,
"REPO_NAME" to ProjectConfig.REPO_NAME,
"PROJECT_SITE" to ProjectConfig.PROJECT_SITE,
"AGP_VERSION_MININUM" to AGP_VERSION_MINIMUM,
"AGP_VERSION_LATEST" to libs.versions.agp.tooling.get(),
"AGP_VERSION_GRADLE_LATEST" to SdkConstants.GRADLE_LATEST_VERSION,
"SNAPSHOTS_REPOSITORY" to VersionUtils.SONATYPE_SNAPSHOTS_REPO,
"PUBLIC_REPOSITORY" to VersionUtils.SONATYPE_PUBLIC_REPO,
)
)
}
}
tasks.withType<JavaCompile> { dependsOn("generateBuildInfo") }
tasks.withType<Jar> { dependsOn("generateBuildInfo") }
@@ -0,0 +1,67 @@
/*
* 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.buildinfo;
/**
* Contains various information about the AndroidIDE build.
*/
public class BuildInfo {
// ---------- Basic info -----------------
public static final String PACKAGE_NAME = "@@PACKAGE_NAME@@";
// ---------- Tooling version info --------------
public static final String AGP_VERSION_MININUM = "@@AGP_VERSION_MININUM@@";
public static final String AGP_VERSION_LATEST = "@@AGP_VERSION_LATEST@@";
public static final String AGP_VERSION_GRADLE_LATEST = "@@AGP_VERSION_GRADLE_LATEST@@";
// ---------- Maven version info ----------------
public static final String MVN_GROUP_ID = "@@MVN_GROUP_ID@@";
public static final String VERSION_NAME = "@@VERSION_NAME@@";
public static final String VERSION_NAME_SIMPLE = "@@VERSION_NAME_SIMPLE@@";
public static final String VERSION_NAME_PUBLISHING = "@@VERSION_NAME_PUBLISHING@@";
public static final String VERSION_NAME_DOWNLOAD = "@@VERSION_NAME_DOWNLOAD@@";
// --------- CI info --------------------
public static final boolean CI_BUILD = @@CI_BUILD@@;
public static final String CI_GIT_BRANCH = "@@CI_GIT_BRANCH@@";
public static final String CI_GIT_COMMIT_HASH = "@@CI_COMMIT_HASH@@";
// --------- CI info --------------------
public static final boolean FDROID_BUILD = @@FDROID_BUILD@@;
public static final String FDROID_BUILD_VERSION_NAME = "@@FDROID_BUILD_VERSION_NAME@@";
public static final int FDROID_BUILD_VERSION_CODE = @@FDROID_BUILD_VERSION_CODE@@;
// ------- SCM info --------------------
public static final String REPO_HOST = "@@REPO_HOST@@";
public static final String REPO_OWNER = "@@REPO_OWNER@@";
public static final String REPO_NAME = "@@REPO_NAME@@";
public static final String REPO_URL = "https://@@REPO_HOST@@/@@REPO_OWNER@@/@@REPO_NAME@@";
public static final String SCM_GIT = "scm:git:git://@@REPO_HOST@@/@@REPO_OWNER@@/@@REPO_NAME@@.git";
public static final String SCM_SSH = "scm:git:ssh://git@@@REPO_HOST@@/@@REPO_OWNER@@/@@REPO_NAME@@.git";
// ------- Misc ------------------------
public static final String PROJECT_SITE = "@@PROJECT_SITE@@";
public static final String SNAPSHOTS_REPOSITORY = "@@SNAPSHOTS_REPOSITORY@@";
public static final String PUBLIC_REPOSITORY = "@@PUBLIC_REPOSITORY@@";
}
+1
View File
@@ -0,0 +1 @@
/build
+21
View File
@@ -0,0 +1,21 @@
# Flashbar
Based on [Flashbar](https://github.com/aritraroy/Flashbar).
## License
```
Copyright 2016 aritraroy
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
```
+43
View File
@@ -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/>.
*/
import com.itsaky.androidide.build.config.BuildConfig
plugins {
id("com.android.library")
id("kotlin-android")
}
groupConfig {
groupIdSuffix.set("utils")
}
android {
namespace = "${BuildConfig.packageName}.flashbar"
}
dependencies {
implementation(libs.common.utilcode)
implementation(libs.androidx.appcompat)
implementation(libs.androidx.annotation)
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.constraintlayout)
implementation(libs.google.material)
implementation(projects.core.resources)
}
+21
View File
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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/>.
-->
<manifest />
@@ -0,0 +1,666 @@
/*
* 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.flashbar
import android.app.Activity
import android.graphics.Bitmap
import android.graphics.PorterDuff
import android.graphics.Typeface
import android.graphics.drawable.Drawable
import android.text.Spanned
import android.widget.ImageView.ScaleType
import android.widget.ImageView.ScaleType.CENTER_CROP
import androidx.annotation.ColorInt
import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.annotation.StyleRes
import androidx.core.content.ContextCompat
import com.itsaky.androidide.flashbar.Flashbar.Gravity.BOTTOM
import com.itsaky.androidide.flashbar.Flashbar.Gravity.TOP
import com.itsaky.androidide.flashbar.Flashbar.ProgressPosition.LEFT
import com.itsaky.androidide.flashbar.Flashbar.ProgressPosition.RIGHT
import com.itsaky.androidide.flashbar.anim.FlashAnim
import com.itsaky.androidide.flashbar.anim.FlashAnimBarBuilder
import com.itsaky.androidide.flashbar.anim.FlashAnimIconBuilder
private const val DEFAULT_SHADOW_STRENGTH = 4
private const val DEFAUT_ICON_SCALE = 1.0f
@Suppress("UNUSED", "MemberVisibilityCanBePrivate")
class Flashbar private constructor(private var builder: Builder) {
private lateinit var flashbarContainerView: FlashbarContainerView
lateinit var flashbarView: FlashbarView
/** Shows a flashbar */
fun show() {
flashbarContainerView.show(builder.activity)
}
/** Dismisses a flashbar */
fun dismiss() {
flashbarContainerView.dismiss()
}
/**
* Returns true/false depending on whether the flashbar is showing or not This represents the
* partial appearance of the flashbar
*/
fun isShowing() = flashbarContainerView.isBarShowing()
/**
* Returns true/false depending on whether the flashbar has been completely shown or not This
* represents the complete appearance of the flashbar
*/
fun isShown() = flashbarContainerView.isBarShown()
private fun construct() {
flashbarContainerView = FlashbarContainerView(builder.activity)
flashbarContainerView.adjustOrientation(builder.activity)
flashbarContainerView.addParent(this)
flashbarContainerView.fitsSystemWindows = true
flashbarView = FlashbarView(builder.activity)
flashbarView.init(builder.gravity)
flashbarView.adjustWitPositionAndOrientation(builder.activity, builder.gravity)
flashbarView.addParent(flashbarContainerView)
flashbarContainerView.attach(flashbarView)
initializeContainerDecor()
initializeBarDecor()
flashbarContainerView.construct()
}
private fun initializeContainerDecor() {
with(flashbarContainerView) {
setDuration(builder.duration)
setBarShowListener(builder.onBarShowListener)
setBarDismissListener(builder.onBarDismissListener)
setBarDismissOnTapOutside(builder.barDismissOnTapOutside)
setOnTapOutsideListener(builder.onTapOutsideListener)
setOverlay(builder.overlay)
setOverlayColor(builder.overlayColor)
setOverlayBlockable(builder.overlayBlockable)
setVibrationTargets(builder.vibrationTargets)
setIconAnim(builder.iconAnimBuilder)
setEnterAnim(builder.enterAnimBuilder!!)
setExitAnim(builder.exitAnimBuilder!!)
enableSwipeToDismiss(builder.enableSwipeToDismiss)
}
}
private fun initializeBarDecor() {
with(flashbarView) {
setBarBackgroundColor(builder.backgroundColor)
setBarBackgroundDrawable(builder.backgroundDrawable)
setBarTapListener(builder.onBarTapListener)
setTitle(builder.title)
setTitleSpanned(builder.titleSpanned)
setTitleTypeface(builder.titleTypeface)
setTitleSizeInPx(builder.titleSizeInPx)
setTitleSizeInSp(builder.titleSizeInSp)
setTitleColor(builder.titleColor)
setTitleAppearance(builder.titleAppearance)
setMessage(builder.message)
setMessageSpanned(builder.messageSpanned)
setMessageTypeface(builder.messageTypeface)
setMessageSizeInPx(builder.messageSizeInPx)
setMessageSizeInSp(builder.messageSizeInSp)
setMessageColor(builder.messageColor)
setMessageAppearance(builder.messageAppearance)
setPrimaryActionText(builder.primaryActionText)
setPrimaryActionTextSpanned(builder.primaryActionTextSpanned)
setPrimaryActionTextTypeface(builder.primaryActionTextTypeface)
setPrimaryActionTextSizeInPx(builder.primaryActionTextSizeInPx)
setPrimaryActionTextSizeInSp(builder.primaryActionTextSizeInSp)
setPrimaryActionTextColor(builder.primaryActionTextColor)
setPrimaryActionTextAppearance(builder.primaryActionTextAppearance)
setPrimaryActionTapListener(builder.onPrimaryActionTapListener)
setPositiveActionText(builder.positiveActionText)
setPositiveActionTextSpanned(builder.positiveActionTextSpanned)
setPositiveActionTextTypeface(builder.positiveActionTextTypeface)
setPositiveActionTextSizeInPx(builder.positiveActionTextSizeInPx)
setPositiveActionTextSizeInSp(builder.positiveActionTextSizeInSp)
setPositiveActionTextColor(builder.positiveActionTextColor)
setPositiveActionTextAppearance(builder.positiveActionTextAppearance)
setPositiveActionTapListener(builder.onPositiveActionTapListener)
setNegativeActionText(builder.negativeActionText)
setNegativeActionTextSpanned(builder.negativeActionTextSpanned)
setNegativeActionTextTypeface(builder.negativeActionTextTypeface)
setNegativeActionTextSizeInPx(builder.negativeActionTextSizeInPx)
setNegativeActionTextSizeInSp(builder.negativeActionTextSizeInSp)
setNegativeActionTextColor(builder.negativeActionTextColor)
setNegativeActionTextAppearance(builder.negativeActionTextAppearance)
setNegativeActionTapListener(builder.onNegativeActionTapListener)
showIcon(builder.showIcon)
showIconScale(builder.iconScale, builder.iconScaleType)
setIconDrawable(builder.iconDrawable)
setIconBitmap(builder.iconBitmap)
setIconColorFilter(builder.iconColorFilter, builder.iconColorFilterMode)
setProgressPosition(builder.progressPosition)
setProgressTint(builder.progressTint, builder.progressPosition)
}
}
class Builder(internal var activity: Activity) {
internal var gravity: Gravity = BOTTOM
internal var backgroundColor: Int? = null
internal var backgroundDrawable: Drawable? = null
internal var duration: Long = DURATION_INDEFINITE
internal var onBarTapListener: OnTapListener? = null
internal var onBarShowListener: OnBarShowListener? = null
internal var onBarDismissListener: OnBarDismissListener? = null
internal var barDismissOnTapOutside: Boolean = false
internal var onTapOutsideListener: OnTapListener? = null
internal var overlay: Boolean = false
internal var overlayColor: Int = ContextCompat.getColor(activity, R.color.modal)
internal var overlayBlockable: Boolean = false
internal var shadowStrength: Int = DEFAULT_SHADOW_STRENGTH
internal var enableSwipeToDismiss: Boolean = false
internal var vibrationTargets: List<Vibration> = emptyList()
internal var title: String? = null
internal var titleSpanned: Spanned? = null
internal var titleTypeface: Typeface? = null
internal var titleSizeInPx: Float? = null
internal var titleSizeInSp: Float? = null
internal var titleColor: Int? = null
internal var titleAppearance: Int? = null
internal var message: String? = null
internal var messageSpanned: Spanned? = null
internal var messageTypeface: Typeface? = null
internal var messageSizeInPx: Float? = null
internal var messageSizeInSp: Float? = null
internal var messageColor: Int? = null
internal var messageAppearance: Int? = null
internal var primaryActionText: String? = null
internal var primaryActionTextSpanned: Spanned? = null
internal var primaryActionTextTypeface: Typeface? = null
internal var primaryActionTextSizeInPx: Float? = null
internal var primaryActionTextSizeInSp: Float? = null
internal var primaryActionTextColor: Int? = null
internal var primaryActionTextAppearance: Int? = null
internal var onPrimaryActionTapListener: OnActionTapListener? = null
internal var positiveActionText: String? = null
internal var positiveActionTextSpanned: Spanned? = null
internal var positiveActionTextTypeface: Typeface? = null
internal var positiveActionTextSizeInPx: Float? = null
internal var positiveActionTextSizeInSp: Float? = null
internal var positiveActionTextColor: Int? = null
internal var positiveActionTextAppearance: Int? = null
internal var onPositiveActionTapListener: OnActionTapListener? = null
internal var negativeActionText: String? = null
internal var negativeActionTextSpanned: Spanned? = null
internal var negativeActionTextTypeface: Typeface? = null
internal var negativeActionTextSizeInPx: Float? = null
internal var negativeActionTextSizeInSp: Float? = null
internal var negativeActionTextColor: Int? = null
internal var negativeActionTextAppearance: Int? = null
internal var onNegativeActionTapListener: OnActionTapListener? = null
internal var showIcon: Boolean = false
internal var iconScale: Float = DEFAUT_ICON_SCALE
internal var iconScaleType: ScaleType = CENTER_CROP
internal var iconDrawable: Drawable? = null
internal var iconBitmap: Bitmap? = null
internal var iconColorFilter: Int? = null
internal var iconColorFilterMode: PorterDuff.Mode? = null
internal var iconAnimBuilder: FlashAnimIconBuilder? = null
internal var progressPosition: ProgressPosition? = null
internal var progressTint: Int? = null
internal var enterAnimBuilder: FlashAnimBarBuilder? = null
internal var exitAnimBuilder: FlashAnimBarBuilder? = null
/**
* Specifies the gravity from where the flashbar will be shown (top/bottom) Default gravity is
* TOP
*/
fun gravity(gravity: Gravity) = apply { this.gravity = gravity }
/** Specifies the background drawable of the flashbar */
fun backgroundDrawable(drawable: Drawable) = apply { this.backgroundDrawable = drawable }
/** Specifies the background drawable resource of the flashbar */
fun backgroundDrawable(@DrawableRes drawableId: Int) = apply {
this.backgroundDrawable = ContextCompat.getDrawable(activity, drawableId)
}
/** Specifies the background color of the flashbar */
fun backgroundColor(@ColorInt color: Int) = apply { this.backgroundColor = color }
/** Specifies the background color resource of the flashbar */
fun backgroundColorRes(@ColorRes colorId: Int) = apply {
this.backgroundColor = ContextCompat.getColor(activity, colorId)
}
/** Sets listener to receive tap events on the surface of the bar */
fun listenBarTaps(listener: OnTapListener) = apply { this.onBarTapListener = listener }
/**
* Specifies the duration for which the flashbar will be visible By default, the duration is
* infinite
*/
fun duration(milliseconds: Long) = apply {
require(milliseconds == DURATION_INDEFINITE || milliseconds > 0) { "Duration can only be $DURATION_INDEFINITE or > 0" }
this.duration = milliseconds
}
/** Sets listener to receive bar showing/shown events */
fun barShowListener(listener: OnBarShowListener) = apply { this.onBarShowListener = listener }
/** Sets listener to receive bar dismissing/dismissed events */
fun barDismissListener(listener: OnBarDismissListener) = apply {
this.onBarDismissListener = listener
}
/** Sets listener to receive tap events outside flashbar surface */
fun listenOutsideTaps(listener: OnTapListener) = apply { this.onTapOutsideListener = listener }
/** Dismisses the bar on being tapped outside */
fun dismissOnTapOutside() = apply { this.barDismissOnTapOutside = true }
/** Shows the modal overlay */
fun showOverlay() = apply { this.overlay = true }
/** Specifies modal overlay color */
fun overlayColor(@ColorInt color: Int) = apply { this.overlayColor = color }
/**
* Specifies modal overlay color resource Modal overlay is automatically shown if color is set
*/
fun overlayColorRes(@ColorRes colorId: Int) = apply {
this.overlayColor = ContextCompat.getColor(activity, colorId)
}
/** Specifies if modal overlay is blockable and should comsume touch events */
fun overlayBlockable() = apply { this.overlayBlockable = true }
/** Specifies the enter animation of the flashbar */
fun enterAnimation(builder: FlashAnimBarBuilder) = apply { this.enterAnimBuilder = builder }
/** Specifies the exit animation of the flashbar */
fun exitAnimation(builder: FlashAnimBarBuilder) = apply { this.exitAnimBuilder = builder }
/** Enables swipe-to-dismiss for the flashbar */
fun enableSwipeToDismiss() = apply { this.enableSwipeToDismiss = true }
/** Specifies whether the device should vibrate during flashbar enter/exit/both */
fun vibrateOn(vararg vibrate: Vibration) = apply {
require(vibrate.isNotEmpty()) { "Vibration targets can not be empty" }
this.vibrationTargets = vibrate.toList()
}
/** Specifies the title string */
fun title(title: String) = apply { this.title = title }
/** Specifies the title string res */
fun title(@StringRes titleId: Int) = apply { this.title = activity.getString(titleId) }
/** Specifies the title span */
fun title(title: Spanned) = apply { this.titleSpanned = title }
/** Specifies the title typeface */
fun titleTypeface(typeface: Typeface) = apply { this.titleTypeface = typeface }
/** Specifies the title size (in pixels) */
fun titleSizeInPx(size: Float) = apply { this.titleSizeInPx = size }
/** Specifies the title size (in sp) */
fun titleSizeInSp(size: Float) = apply { this.titleSizeInSp = size }
/** Specifies the title color */
fun titleColor(@ColorInt color: Int) = apply { this.titleColor = color }
/** Specifies the title color resource */
fun titleColorRes(@ColorRes colorId: Int) = apply {
this.titleColor = ContextCompat.getColor(activity, colorId)
}
/** Specifies the title appearance */
fun titleAppearance(@StyleRes appearance: Int) = apply { this.titleAppearance = appearance }
/** Specifies the message string */
fun message(message: String) = apply { this.message = message }
/** Specifies the message string resource */
fun message(@StringRes messageId: Int) = apply { this.message = activity.getString(messageId) }
/** Specifies the message string span */
fun message(message: Spanned) = apply { this.messageSpanned = message }
/** Specifies the message typeface */
fun messageTypeface(typeface: Typeface) = apply { this.messageTypeface = typeface }
/** Specifies the message size (in pixels) */
fun messageSizeInPx(size: Float) = apply { this.messageSizeInPx = size }
/** Specifies the message size (in sp) */
fun messageSizeInSp(size: Float) = apply { this.messageSizeInSp = size }
/** Specifies the message color */
fun messageColor(@ColorInt color: Int) = apply { this.messageColor = color }
/** Specifies the message color resource */
fun messageColorRes(@ColorRes colorId: Int) = apply {
this.messageColor = ContextCompat.getColor(activity, colorId)
}
/** Specifies the message appearance */
fun messageAppearance(@StyleRes appearance: Int) = apply { this.messageAppearance = appearance }
/** Specifies the primary action text string */
fun primaryActionText(text: String) = apply {
require(progressPosition != RIGHT) { "Cannot show action button if right progress is set" }
this.primaryActionText = text
}
/** Specifies the primary action text string resource */
fun primaryActionText(@StringRes actionTextId: Int) = apply {
primaryActionText(activity.getString(actionTextId))
}
/** Specifies the primary action text string span */
fun primaryActionText(actionText: Spanned) = apply {
this.primaryActionTextSpanned = actionText
}
/** Specifies the primary action text typeface */
fun primaryActionTextTypeface(typeface: Typeface) = apply {
this.primaryActionTextTypeface = typeface
}
/** Specifies the primary action text size (in pixels) */
fun primaryActionTextSizeInPx(size: Float) = apply { this.primaryActionTextSizeInPx = size }
/** Specifies the primary action text size (in sp) */
fun primaryActionTextSizeInSp(size: Float) = apply { this.primaryActionTextSizeInSp = size }
/** Specifies the primary action text color */
fun primaryActionTextColor(@ColorInt color: Int) = apply { this.primaryActionTextColor = color }
/** Specifies the primary action text color resource */
fun primaryActionTextColorRes(@ColorRes colorId: Int) = apply {
this.primaryActionTextColor = ContextCompat.getColor(activity, colorId)
}
/** Specifies the primary action text appearance */
fun primaryActionTextAppearance(@StyleRes appearance: Int) = apply {
this.primaryActionTextAppearance = appearance
}
/** Sets listener to receive tap events on the primary action */
fun primaryActionTapListener(onActionTapListener: OnActionTapListener) = apply {
this.onPrimaryActionTapListener = onActionTapListener
}
/** Specifies the positive action text string */
fun positiveActionText(text: String) = apply { this.positiveActionText = text }
/** Specifies the positive action text string resource */
fun positiveActionText(@StringRes actionTextId: Int) = apply {
positiveActionText(activity.getString(actionTextId))
}
/** Specifies the positive action text string span */
fun positiveActionText(actionText: Spanned) = apply {
this.positiveActionTextSpanned = actionText
}
/** Specifies the positive action text typeface */
fun positiveActionTextTypeface(typeface: Typeface) = apply {
this.positiveActionTextTypeface = typeface
}
/** Specifies the positive action text size (in pixels) */
fun positiveActionTextSizeInPx(size: Float) = apply { this.positiveActionTextSizeInPx = size }
/** Specifies the positive action text size (in sp) */
fun positiveActionTextSizeInSp(size: Float) = apply { this.positiveActionTextSizeInSp = size }
/** Specifies the positive action text color */
fun positiveActionTextColor(@ColorInt color: Int) = apply {
this.positiveActionTextColor = color
}
/** Specifies the positive action text color resource */
fun positiveActionTextColorRes(@ColorRes colorId: Int) = apply {
this.positiveActionTextColor = ContextCompat.getColor(activity, colorId)
}
/** Specifies the positive action text appearance */
fun positiveActionTextAppearance(@StyleRes appearance: Int) = apply {
this.positiveActionTextAppearance = appearance
}
/** Sets listener to receive tap events on the positive action */
fun positiveActionTapListener(onActionTapListener: OnActionTapListener) = apply {
this.onPositiveActionTapListener = onActionTapListener
}
/** Specifies the negative action text string */
fun negativeActionText(text: String) = apply { this.negativeActionText = text }
/** Specifies the negative action text string resource */
fun negativeActionText(@StringRes actionTextId: Int) = apply {
negativeActionText(activity.getString(actionTextId))
}
/** Specifies the negative action text string span */
fun negativeActionText(actionText: Spanned) = apply {
this.negativeActionTextSpanned = actionText
}
/** Specifies the negative action text typeface */
fun negativeActionTextTypeface(typeface: Typeface) = apply {
this.negativeActionTextTypeface = typeface
}
/** Specifies the negative action text size (in pixels) */
fun negativeActionTextSizeInPx(size: Float) = apply { this.negativeActionTextSizeInPx = size }
/** Specifies the negative action text size (in sp) */
fun negativeActionTextSizeInSp(size: Float) = apply { this.negativeActionTextSizeInSp = size }
/** Specifies the negative action text color */
fun negativeActionTextColor(@ColorInt color: Int) = apply {
this.negativeActionTextColor = color
}
/** Specifies the negative action text color resource */
fun negativeActionTextColorRes(@ColorRes colorId: Int) = apply {
this.negativeActionTextColor = ContextCompat.getColor(activity, colorId)
}
/** Specifies the negative action text appearance */
fun negativeActionTextAppearance(@StyleRes appearance: Int) = apply {
this.negativeActionTextAppearance = appearance
}
/** Sets listener to receive tap events on the negative action */
fun negativeActionTapListener(onActionTapListener: OnActionTapListener) = apply {
this.onNegativeActionTapListener = onActionTapListener
}
/** Specifies if the icon should be shown. Also configures its scale factor and scale type */
@JvmOverloads
fun showIcon(scale: Float = DEFAUT_ICON_SCALE, scaleType: ScaleType = CENTER_CROP) = apply {
require(progressPosition != LEFT) { "Cannot show icon if left progress is set" }
require(scale > 0) { "Icon scale cannot be negative or zero" }
this.showIcon = true
this.iconScale = scale
this.iconScaleType = scaleType
}
/** Specifies the icon drawable */
fun icon(icon: Drawable) = apply { this.iconDrawable = icon }
/** Specifies the icon drawable resource */
fun icon(@DrawableRes iconId: Int) = apply {
this.iconDrawable = ContextCompat.getDrawable(activity, iconId)
}
/** Specifies the icon bitmap */
fun icon(bitmap: Bitmap) = apply { this.iconBitmap = bitmap }
/** Specifies the icon color filter and mode */
@JvmOverloads
fun iconColorFilter(@ColorInt color: Int, mode: PorterDuff.Mode? = null) = apply {
this.iconColorFilter = color
this.iconColorFilterMode = mode
}
/** Specifies the icon color filter resource and mode */
@JvmOverloads
fun iconColorFilterRes(@ColorRes colorId: Int, mode: PorterDuff.Mode? = null) = apply {
this.iconColorFilter = ContextCompat.getColor(activity, colorId)
this.iconColorFilterMode = mode
}
/** Specifies the icon builder */
fun iconAnimation(builder: FlashAnimIconBuilder) = apply { this.iconAnimBuilder = builder }
/** Specifies the gravity in which the indeterminate progress is shown (left/right) */
fun showProgress(position: ProgressPosition) = apply {
this.progressPosition = position
if (progressPosition == LEFT && showIcon) {
throw IllegalArgumentException("Cannot show progress at left if icon is already set")
}
if (progressPosition == RIGHT && primaryActionText != null) {
throw IllegalArgumentException(
"Cannot show progress at right if action button is already set"
)
}
}
/** Specifies the indeterminate progress tint */
fun progressTint(@ColorInt color: Int) = apply { this.progressTint = color }
/** Specifies the indeterminate progress tint resource */
fun progressTintRes(@ColorRes colorId: Int) = apply {
this.progressTint = ContextCompat.getColor(activity, colorId)
}
/** Builds a flashbar instance */
fun build(): Flashbar {
configureAnimation()
val flashbar = Flashbar(this)
flashbar.construct()
return flashbar
}
/** Shows the flashbar */
fun show() = build().show()
private fun configureAnimation() {
enterAnimBuilder =
if (enterAnimBuilder == null) {
when (gravity) {
TOP -> FlashAnim.with(activity).animateBar().enter().fromTop()
BOTTOM -> FlashAnim.with(activity).animateBar().enter().fromBottom()
}
} else {
when (gravity) {
TOP -> enterAnimBuilder!!.enter().fromTop()
BOTTOM -> enterAnimBuilder!!.enter().fromBottom()
}
}
exitAnimBuilder =
if (exitAnimBuilder == null) {
when (gravity) {
TOP -> FlashAnim.with(activity).animateBar().exit().fromTop()
BOTTOM -> FlashAnim.with(activity).animateBar().exit().fromBottom()
}
} else {
when (gravity) {
TOP -> exitAnimBuilder!!.exit().fromTop()
BOTTOM -> exitAnimBuilder!!.exit().fromBottom()
}
}
}
}
companion object {
const val DURATION_SHORT = 1000L
const val DURATION_LONG = 2500L
const val DURATION_INDEFINITE = -1L
}
enum class Gravity {
TOP,
BOTTOM
}
enum class DismissEvent {
TIMEOUT,
MANUAL,
TAP_OUTSIDE,
SWIPE
}
enum class Vibration {
SHOW,
DISMISS
}
enum class ProgressPosition {
LEFT,
RIGHT
}
fun interface OnActionTapListener {
fun onActionTapped(bar: Flashbar)
}
interface OnBarDismissListener {
fun onDismissing(bar: Flashbar, isSwiped: Boolean)
fun onDismissProgress(bar: Flashbar, progress: Float)
fun onDismissed(bar: Flashbar, event: DismissEvent)
}
interface OnTapListener {
fun onTap(flashbar: Flashbar)
}
interface OnBarShowListener {
fun onShowing(bar: Flashbar)
fun onShowProgress(bar: Flashbar, progress: Float)
fun onShown(bar: Flashbar)
}
}
@@ -0,0 +1,312 @@
/*
* 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.flashbar
import android.app.Activity
import android.content.Context
import android.graphics.Rect
import android.view.HapticFeedbackConstants.VIRTUAL_KEY
import android.view.MotionEvent
import android.view.MotionEvent.ACTION_DOWN
import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.widget.RelativeLayout
import com.itsaky.androidide.flashbar.Flashbar.Companion.DURATION_INDEFINITE
import com.itsaky.androidide.flashbar.Flashbar.DismissEvent
import com.itsaky.androidide.flashbar.Flashbar.DismissEvent.*
import com.itsaky.androidide.flashbar.Flashbar.Vibration.DISMISS
import com.itsaky.androidide.flashbar.SwipeDismissTouchListener.DismissCallbacks
import com.itsaky.androidide.flashbar.anim.FlashAnim
import com.itsaky.androidide.flashbar.Flashbar.OnBarDismissListener
import com.itsaky.androidide.flashbar.Flashbar.OnBarShowListener
import com.itsaky.androidide.flashbar.Flashbar.OnTapListener
import com.itsaky.androidide.flashbar.Flashbar.Vibration
import com.itsaky.androidide.flashbar.Flashbar.Vibration.SHOW
import com.itsaky.androidide.flashbar.anim.FlashAnimBarBuilder
import com.itsaky.androidide.flashbar.anim.FlashAnimIconBuilder
import com.itsaky.androidide.flashbar.util.NavigationBarPosition.*
import com.itsaky.androidide.flashbar.util.afterMeasured
import com.itsaky.androidide.flashbar.util.getNavigationBarPosition
import com.itsaky.androidide.flashbar.util.getNavigationBarSizeInPx
import com.itsaky.androidide.flashbar.util.getRootView
/**
* Container withView matching the height and width of the parent to hold a FlashbarView.
* It will occupy the entire screens size but will be completely transparent. The
* FlashbarView inside is the only visible component in it.
*/
internal class FlashbarContainerView(context: Context)
: RelativeLayout(context), DismissCallbacks {
private val dismissRunnable = Runnable { dismissInternal(TIMEOUT) }
internal lateinit var parentFlashbar: Flashbar
private lateinit var flashbarView: FlashbarView
private lateinit var enterAnimBuilder: FlashAnimBarBuilder
private lateinit var exitAnimBuilder: FlashAnimBarBuilder
private lateinit var vibrationTargets: List<Vibration>
private var onBarShowListener: OnBarShowListener? = null
private var onBarDismissListener: OnBarDismissListener? = null
private var onTapOutsideListener: OnTapListener? = null
private var overlayColor: Int? = null
private var iconAnimBuilder: FlashAnimIconBuilder? = null
private var duration = DURATION_INDEFINITE
private var isBarShowing = false
private var isBarShown = false
private var isBarDismissing = false
private var barDismissOnTapOutside: Boolean = false
private var earlyDismissalRequested = false
private var showOverlay: Boolean = false
private var overlayBlockable: Boolean = false
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
ACTION_DOWN -> {
val rect = Rect()
flashbarView.getHitRect(rect)
// Checks if the tap was outside the bar
if (!rect.contains(event.x.toInt(), event.y.toInt())) {
onTapOutsideListener?.onTap(parentFlashbar)
if (barDismissOnTapOutside) {
dismissInternal(TAP_OUTSIDE)
}
}
}
}
return super.onInterceptTouchEvent(event)
}
override fun onSwipe(isSwiping: Boolean) {
isBarDismissing = isSwiping
if (isSwiping) {
onBarDismissListener?.onDismissing(parentFlashbar, true)
}
}
override fun onDismiss(view: View) {
removeCallbacks(dismissRunnable)
(parent as? ViewGroup)?.removeView(this@FlashbarContainerView)
isBarShown = false
flashbarView.stopIconAnimation()
if (vibrationTargets.contains(DISMISS)) {
performHapticFeedback(VIRTUAL_KEY)
}
onBarDismissListener?.onDismissed(parentFlashbar, SWIPE)
}
internal fun attach(flashbarView: FlashbarView) {
this.flashbarView = flashbarView
}
internal fun construct() {
isHapticFeedbackEnabled = true
if (showOverlay) {
setBackgroundColor(overlayColor!!)
if (overlayBlockable) {
isClickable = true
isFocusable = true
}
}
addView(flashbarView)
}
internal fun addParent(flashbar: Flashbar) {
this.parentFlashbar = flashbar
}
internal fun adjustOrientation(activity: Activity) {
val flashbarContainerViewLp = LayoutParams(MATCH_PARENT, MATCH_PARENT)
val navigationBarPosition = activity.getNavigationBarPosition()
val navigationBarSize = activity.getNavigationBarSizeInPx()
when (navigationBarPosition) {
LEFT -> flashbarContainerViewLp.leftMargin = navigationBarSize
TOP -> flashbarContainerViewLp.topMargin = navigationBarSize
RIGHT -> flashbarContainerViewLp.rightMargin = navigationBarSize
BOTTOM -> flashbarContainerViewLp.bottomMargin = navigationBarSize
}
layoutParams = flashbarContainerViewLp
}
internal fun show(activity: Activity) {
if (isBarShowing || isBarShown) return
val activityRootView = activity.getRootView() ?: return
// Only add the withView to the parent once
if (this.parent == null) activityRootView.addView(this)
activityRootView.afterMeasured {
val enterAnim = enterAnimBuilder.withView(flashbarView).build()
enterAnim.start(object : FlashAnim.InternalAnimListener {
override fun onStart() {
isBarShowing = true
onBarShowListener?.onShowing(parentFlashbar)
}
override fun onUpdate(progress: Float) {
onBarShowListener?.onShowProgress(parentFlashbar, progress)
}
override fun onStop() {
isBarShowing = false
isBarShown = true
flashbarView.startIconAnimation(iconAnimBuilder)
if (vibrationTargets.contains(SHOW)) {
performHapticFeedback(VIRTUAL_KEY)
}
onBarShowListener?.onShown(parentFlashbar)
if (earlyDismissalRequested) {
dismiss()
earlyDismissalRequested = false
}
}
})
handleDismiss()
}
}
internal fun dismiss() {
dismissInternal(MANUAL)
}
internal fun isBarShowing() = isBarShowing
internal fun isBarShown() = isBarShown
internal fun setDuration(duration: Long) {
this.duration = duration
}
internal fun setBarShowListener(listener: OnBarShowListener?) {
this.onBarShowListener = listener
}
internal fun setBarDismissListener(listener: OnBarDismissListener?) {
this.onBarDismissListener = listener
}
internal fun setBarDismissOnTapOutside(dismiss: Boolean) {
this.barDismissOnTapOutside = dismiss
}
internal fun setOnTapOutsideListener(listener: OnTapListener?) {
this.onTapOutsideListener = listener
}
internal fun setOverlay(overlay: Boolean) {
this.showOverlay = overlay
}
internal fun setOverlayColor(color: Int) {
this.overlayColor = color
}
internal fun setOverlayBlockable(blockable: Boolean) {
this.overlayBlockable = blockable
}
internal fun setEnterAnim(builder: FlashAnimBarBuilder) {
this.enterAnimBuilder = builder
}
internal fun setExitAnim(builder: FlashAnimBarBuilder) {
this.exitAnimBuilder = builder
}
internal fun enableSwipeToDismiss(enable: Boolean) {
this.flashbarView.enableSwipeToDismiss(enable, this)
}
internal fun setVibrationTargets(targets: List<Vibration>) {
this.vibrationTargets = targets
}
internal fun setIconAnim(builder: FlashAnimIconBuilder?) {
this.iconAnimBuilder = builder
}
private fun handleDismiss() {
if (duration != DURATION_INDEFINITE) {
postDelayed(dismissRunnable, duration)
}
}
private fun dismissInternal(event: DismissEvent) {
if (isBarShowing) {
// Flashbar is currently being shown
// but a dismissal has been requested
// take this into account and dismiss flashbar afterwards
earlyDismissalRequested = true
return
}
if (isBarDismissing || !isBarShown) {
return
}
removeCallbacks(dismissRunnable)
val exitAnim = exitAnimBuilder.withView(flashbarView).build()
exitAnim.start(object : FlashAnim.InternalAnimListener {
override fun onStart() {
isBarDismissing = true
onBarDismissListener?.onDismissing(parentFlashbar, false)
}
override fun onUpdate(progress: Float) {
onBarDismissListener?.onDismissProgress(parentFlashbar, progress)
}
override fun onStop() {
isBarDismissing = false
isBarShown = false
if (vibrationTargets.contains(DISMISS)) {
performHapticFeedback(VIRTUAL_KEY)
}
onBarDismissListener?.onDismissed(parentFlashbar, event)
post { (parent as? ViewGroup)?.removeView(this@FlashbarContainerView) }
}
})
}
}
@@ -0,0 +1,488 @@
/*
* 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.flashbar
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.content.res.ColorStateList
import android.graphics.Bitmap
import android.graphics.PorterDuff
import android.graphics.Typeface
import android.graphics.drawable.Drawable
import android.text.Spanned
import android.text.TextUtils
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.RelativeLayout
import android.widget.RelativeLayout.ALIGN_PARENT_BOTTOM
import android.widget.RelativeLayout.ALIGN_PARENT_TOP
import android.widget.TextView
import androidx.annotation.ColorInt
import androidx.core.view.marginBottom
import androidx.core.view.marginLeft
import androidx.core.view.marginRight
import androidx.core.view.marginTop
import androidx.core.view.updateLayoutParams
import com.google.android.material.button.MaterialButton
import com.google.android.material.progressindicator.CircularProgressIndicator
import com.itsaky.androidide.flashbar.Flashbar.Gravity
import com.itsaky.androidide.flashbar.Flashbar.Gravity.BOTTOM
import com.itsaky.androidide.flashbar.Flashbar.Gravity.TOP
import com.itsaky.androidide.flashbar.Flashbar.OnActionTapListener
import com.itsaky.androidide.flashbar.Flashbar.OnTapListener
import com.itsaky.androidide.flashbar.Flashbar.ProgressPosition
import com.itsaky.androidide.flashbar.Flashbar.ProgressPosition.LEFT
import com.itsaky.androidide.flashbar.Flashbar.ProgressPosition.RIGHT
import com.itsaky.androidide.flashbar.SwipeDismissTouchListener.DismissCallbacks
import com.itsaky.androidide.flashbar.anim.FlashAnimIconBuilder
import com.itsaky.androidide.flashbar.databinding.FlashBarViewBinding
import com.itsaky.androidide.flashbar.util.getStatusBarHeightInPx
/**
* The actual Flashbar withView representation that can consist of the title, message, button, icon,
* etc. Its size is adaptive and depends solely on the amount of content present in it. It always
* matches the width of the screen.
*
* It can either be present at the top or at the bottom of the screen. It will always consume touch
* events and respond as necessary.
*/
class FlashbarView(context: Context) : LinearLayout(context) {
private val compensationMarginTop =
resources.getDimension(R.dimen.fb_top_compensation_margin).toInt()
private val compensationMarginBottom =
resources.getDimension(R.dimen.fb_bottom_compensation_margin).toInt()
private lateinit var binding: FlashBarViewBinding
private lateinit var parentFlashbarContainer: FlashbarContainerView
private lateinit var gravity: Gravity
private var isMarginCompensationApplied: Boolean = false
private val fbContent: LinearLayout
get() = this.binding.fbContent
private val fbRoot: LinearLayout
get() = this.binding.fbRoot
private val fbIcon: ImageView
get() = this.binding.fbIcon
private val fbLeftProgress: CircularProgressIndicator
get() = this.binding.fbLeftProgress
private val fbRightProgress: CircularProgressIndicator
get() = this.binding.fbRightProgress
private val fbMessage: TextView
get() = this.binding.fbMessage
private val fbPrimaryAction: MaterialButton
get() = this.binding.fbPrimaryAction
private val fbPositiveAction: MaterialButton
get() = this.binding.fbPositiveAction
private val fbNegativeAction: MaterialButton
get() = this.binding.fbNegativeAction
private val fbSecondaryActionContainer: LinearLayout
get() = this.binding.fbSecondaryActionContainer
private val fbTitle: TextView
get() = this.binding.fbTitle
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
if (!isMarginCompensationApplied) {
isMarginCompensationApplied = true
val params = layoutParams as MarginLayoutParams
when (gravity) {
TOP -> params.topMargin = -compensationMarginTop
BOTTOM -> params.bottomMargin = -compensationMarginBottom
}
requestLayout()
}
}
internal fun init(gravity: Gravity) {
this.gravity = gravity
this.orientation = VERTICAL
this.binding = FlashBarViewBinding.inflate(LayoutInflater.from(context), this, true)
this.binding.root.apply {
// add margin to the card view so that the card elevation is visible
val dp16 = context.resources.getDimensionPixelSize(R.dimen.fb_card_elevation)
val (topIncr, bottomIncr) = when (gravity) {
TOP -> 0 to dp16
BOTTOM -> dp16 to 0
}
updateLayoutParams<MarginLayoutParams> {
setMargins(
/* left = */ marginLeft,
/* top = */ marginTop + topIncr,
/* right = */ marginRight,
/* bottom = */ marginBottom + bottomIncr
)
}
}
}
internal fun adjustWitPositionAndOrientation(activity: Activity, gravity: Gravity) {
val flashbarViewLp = RelativeLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
val statusBarHeight = activity.getStatusBarHeightInPx()
val flashbarViewContentLp = fbContent.layoutParams as LayoutParams
when (gravity) {
TOP -> {
flashbarViewContentLp.topMargin = statusBarHeight.plus(compensationMarginTop / 2)
flashbarViewLp.addRule(ALIGN_PARENT_TOP)
}
BOTTOM -> {
flashbarViewContentLp.bottomMargin = compensationMarginBottom
flashbarViewLp.addRule(ALIGN_PARENT_BOTTOM)
}
}
fbContent.layoutParams = flashbarViewContentLp
layoutParams = flashbarViewLp
}
internal fun addParent(flashbarContainerView: FlashbarContainerView) {
this.parentFlashbarContainer = flashbarContainerView
}
fun setBarBackgroundDrawable(drawable: Drawable?) {
if (drawable == null) return
this.fbRoot.background = drawable
}
fun setBarBackgroundColor(@ColorInt color: Int?) {
if (color == null) return
this.fbRoot.setBackgroundColor(color)
}
fun setBarTapListener(listener: OnTapListener?) {
if (listener == null) return
this.fbRoot.setOnClickListener { listener.onTap(parentFlashbarContainer.parentFlashbar) }
}
fun setTitle(title: String?) {
if (TextUtils.isEmpty(title)) return
this.fbTitle.text = title
this.fbTitle.visibility = VISIBLE
}
fun setTitleSpanned(title: Spanned?) {
if (title == null) return
this.fbTitle.text = title
this.fbTitle.visibility = VISIBLE
}
fun setTitleTypeface(typeface: Typeface?) {
if (typeface == null) return
fbTitle.typeface = typeface
}
fun setTitleSizeInPx(size: Float?) {
if (size == null) return
fbTitle.setTextSize(TypedValue.COMPLEX_UNIT_PX, size)
}
fun setTitleSizeInSp(size: Float?) {
if (size == null) return
fbTitle.setTextSize(TypedValue.COMPLEX_UNIT_SP, size)
}
fun setTitleColor(color: Int?) {
if (color == null) return
fbTitle.setTextColor(color)
}
fun setTitleAppearance(titleAppearance: Int?) {
if (titleAppearance == null) return
this.fbTitle.setTextAppearance(titleAppearance)
}
fun setMessage(message: String?) {
if (TextUtils.isEmpty(message)) return
this.fbMessage.text = message
this.fbMessage.visibility = VISIBLE
}
fun setMessageSpanned(message: Spanned?) {
if (message == null) return
this.fbMessage.text = message
this.fbMessage.visibility = VISIBLE
}
fun setMessageTypeface(typeface: Typeface?) {
if (typeface == null) return
this.fbMessage.typeface = typeface
}
fun setMessageSizeInPx(size: Float?) {
if (size == null) return
this.fbMessage.setTextSize(TypedValue.COMPLEX_UNIT_PX, size)
}
fun setMessageSizeInSp(size: Float?) {
if (size == null) return
this.fbMessage.setTextSize(TypedValue.COMPLEX_UNIT_SP, size)
}
fun setMessageColor(color: Int?) {
if (color == null) return
this.fbMessage.setTextColor(color)
}
fun setMessageAppearance(messageAppearance: Int?) {
if (messageAppearance == null) return
this.fbMessage.setTextAppearance(messageAppearance)
}
fun setPrimaryActionText(text: String?) {
if (TextUtils.isEmpty(text)) return
this.fbPrimaryAction.text = text
this.fbPrimaryAction.visibility = VISIBLE
}
fun setPrimaryActionTextSpanned(text: Spanned?) {
if (text == null) return
this.fbPrimaryAction.text = text
this.fbPrimaryAction.visibility = VISIBLE
}
fun setPrimaryActionTextTypeface(typeface: Typeface?) {
if (typeface == null) return
this.fbPrimaryAction.typeface = typeface
}
fun setPrimaryActionTextSizeInPx(size: Float?) {
if (size == null) return
this.fbPrimaryAction.setTextSize(TypedValue.COMPLEX_UNIT_PX, size)
}
fun setPrimaryActionTextSizeInSp(size: Float?) {
if (size == null) return
this.fbPrimaryAction.setTextSize(TypedValue.COMPLEX_UNIT_SP, size)
}
fun setPrimaryActionTextColor(color: Int?) {
if (color == null) return
this.fbPrimaryAction.setTextColor(color)
}
fun setPrimaryActionTextAppearance(messageAppearance: Int?) {
if (messageAppearance == null) return
this.fbPrimaryAction.setTextAppearance(messageAppearance)
}
fun setPrimaryActionTapListener(listener: OnActionTapListener?) {
if (listener == null) return
this.fbPrimaryAction.setOnClickListener {
listener.onActionTapped(parentFlashbarContainer.parentFlashbar)
}
}
fun setPositiveActionText(text: String?) {
if (TextUtils.isEmpty(text)) return
this.fbSecondaryActionContainer.visibility = VISIBLE
this.fbPositiveAction.text = text
this.fbPositiveAction.visibility = VISIBLE
}
fun setPositiveActionTextSpanned(text: Spanned?) {
if (text == null) return
this.fbSecondaryActionContainer.visibility = VISIBLE
this.fbPositiveAction.text = text
this.fbPositiveAction.visibility = VISIBLE
}
fun setPositiveActionTextTypeface(typeface: Typeface?) {
if (typeface == null) return
this.fbPositiveAction.typeface = typeface
}
fun setPositiveActionTextSizeInPx(size: Float?) {
if (size == null) return
this.fbPositiveAction.setTextSize(TypedValue.COMPLEX_UNIT_PX, size)
}
fun setPositiveActionTextSizeInSp(size: Float?) {
if (size == null) return
this.fbPositiveAction.setTextSize(TypedValue.COMPLEX_UNIT_SP, size)
}
fun setPositiveActionTextColor(color: Int?) {
if (color == null) return
this.fbPositiveAction.setTextColor(color)
}
fun setPositiveActionTextAppearance(messageAppearance: Int?) {
if (messageAppearance == null) return
this.fbPositiveAction.setTextAppearance(messageAppearance)
}
fun setPositiveActionTapListener(listener: OnActionTapListener?) {
if (listener == null) return
this.fbPositiveAction.setOnClickListener {
listener.onActionTapped(parentFlashbarContainer.parentFlashbar)
}
}
fun setNegativeActionText(text: String?) {
if (TextUtils.isEmpty(text)) return
this.fbSecondaryActionContainer.visibility = VISIBLE
this.fbNegativeAction.text = text
this.fbNegativeAction.visibility = VISIBLE
}
fun setNegativeActionTextSpanned(text: Spanned?) {
if (text == null) return
this.fbSecondaryActionContainer.visibility = VISIBLE
this.fbNegativeAction.text = text
this.fbNegativeAction.visibility = VISIBLE
}
fun setNegativeActionTextTypeface(typeface: Typeface?) {
if (typeface == null) return
this.fbNegativeAction.typeface = typeface
}
fun setNegativeActionTextSizeInPx(size: Float?) {
if (size == null) return
this.fbNegativeAction.setTextSize(TypedValue.COMPLEX_UNIT_PX, size)
}
fun setNegativeActionTextSizeInSp(size: Float?) {
if (size == null) return
this.fbNegativeAction.setTextSize(TypedValue.COMPLEX_UNIT_SP, size)
}
fun setNegativeActionTextColor(color: Int?) {
if (color == null) return
this.fbNegativeAction.setTextColor(color)
}
fun setNegativeActionTextAppearance(messageAppearance: Int?) {
if (messageAppearance == null) return
this.fbNegativeAction.setTextAppearance(messageAppearance)
}
fun setNegativeActionTapListener(listener: OnActionTapListener?) {
if (listener == null) return
this.fbNegativeAction.setOnClickListener {
listener.onActionTapped(parentFlashbarContainer.parentFlashbar)
}
}
internal fun showIcon(showIcon: Boolean) {
this.fbIcon.visibility = if (showIcon) VISIBLE else GONE
}
internal fun showIconScale(scale: Float, scaleType: ImageView.ScaleType?) {
this.fbIcon.scaleX = scale
this.fbIcon.scaleY = scale
this.fbIcon.scaleType = scaleType
}
fun setIconDrawable(icon: Drawable?) {
if (icon == null) return
this.fbIcon.setImageDrawable(icon)
}
fun setIconBitmap(bitmap: Bitmap?) {
if (bitmap == null) return
this.fbIcon.setImageBitmap(bitmap)
}
fun setIconColorFilter(colorFilter: Int?, filterMode: PorterDuff.Mode?) {
if (colorFilter == null) return
if (filterMode == null) {
this.fbIcon.setColorFilter(colorFilter)
} else {
this.fbIcon.setColorFilter(colorFilter, filterMode)
}
}
internal fun startIconAnimation(animator: FlashAnimIconBuilder?) {
animator?.withView(fbIcon)?.build()?.start()
}
internal fun stopIconAnimation() {
fbIcon.clearAnimation()
}
@SuppressLint("ClickableViewAccessibility")
internal fun enableSwipeToDismiss(enable: Boolean, callbacks: DismissCallbacks) {
if (enable) {
fbRoot.setOnTouchListener(SwipeDismissTouchListener(this, callbacks))
}
}
fun setProgressPosition(position: ProgressPosition?) {
if (position == null) return
when (position) {
LEFT -> {
fbLeftProgress.visibility = VISIBLE
fbRightProgress.visibility = GONE
}
RIGHT -> {
fbLeftProgress.visibility = GONE
fbRightProgress.visibility = VISIBLE
}
}
}
fun setProgressTint(progressTint: Int?, position: ProgressPosition?) {
if (position == null || progressTint == null) return
val progressBar =
when (position) {
LEFT -> fbLeftProgress
RIGHT -> fbRightProgress
}
progressBar.progressTintList = ColorStateList.valueOf(progressTint)
}
}
@@ -0,0 +1,200 @@
/*
* 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.flashbar
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.ValueAnimator
import android.view.MotionEvent
import android.view.VelocityTracker
import android.view.View
import android.view.ViewConfiguration
import kotlin.math.abs
internal class SwipeDismissTouchListener(
private val view: View,
private val callbacks: DismissCallbacks
) : View.OnTouchListener {
private val slop: Int
private val minFlingVelocity: Int
private val animationTime: Long
private var viewWidth = 1
private var downX: Float = 0.toFloat()
private var downY: Float = 0.toFloat()
private var swiping: Boolean = false
private var swipingSlop: Int = 0
private var velocityTracker: VelocityTracker? = null
private var translationX: Float = 0.toFloat()
init {
val vc = ViewConfiguration.get(view.context)
slop = vc.scaledTouchSlop
minFlingVelocity = vc.scaledMinimumFlingVelocity * 16
animationTime =
view.context.resources.getInteger(android.R.integer.config_shortAnimTime).toLong()
}
override fun onTouch(view: View, motionEvent: MotionEvent): Boolean {
motionEvent.offsetLocation(translationX, 0f)
if (viewWidth < 2) {
viewWidth = this.view.width
}
when (motionEvent.actionMasked) {
MotionEvent.ACTION_DOWN -> {
downX = motionEvent.rawX
downY = motionEvent.rawY
velocityTracker = VelocityTracker.obtain()
velocityTracker!!.addMovement(motionEvent)
return false
}
MotionEvent.ACTION_UP -> {
if (velocityTracker != null) {
val deltaX = motionEvent.rawX - downX
velocityTracker!!.addMovement(motionEvent)
velocityTracker!!.computeCurrentVelocity(1000)
val velocityX = velocityTracker!!.xVelocity
val absVelocityX = abs(velocityX)
val absVelocityY = abs(velocityTracker!!.yVelocity)
var dismiss = false
var dismissRight = false
if (abs(deltaX) > viewWidth / 2 && swiping) {
dismiss = true
dismissRight = deltaX > 0
} else if (minFlingVelocity <= absVelocityX && absVelocityY < absVelocityX && swiping) {
dismiss = velocityX < 0 == deltaX < 0
dismissRight = velocityTracker!!.xVelocity > 0
}
if (dismiss) {
this.view
.animate()
.translationX(if (dismissRight) viewWidth.toFloat() else -viewWidth.toFloat())
.alpha(0f)
.setDuration(animationTime)
.setListener(
object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
performDismiss()
}
}
)
} else if (swiping) {
this.view
.animate()
.translationX(0f)
.alpha(1f)
.setDuration(animationTime)
.setListener(null)
}
velocityTracker!!.recycle()
velocityTracker = null
translationX = 0f
downX = 0f
downY = 0f
swiping = false
callbacks.onSwipe(false)
}
}
MotionEvent.ACTION_CANCEL -> {
if (velocityTracker != null) {
this.view
.animate()
.translationX(0f)
.alpha(1f)
.setDuration(animationTime)
.setListener(null)
velocityTracker!!.recycle()
velocityTracker = null
translationX = 0f
downX = 0f
downY = 0f
swiping = false
callbacks.onSwipe(false)
}
}
MotionEvent.ACTION_MOVE -> {
if (velocityTracker != null) {
velocityTracker!!.addMovement(motionEvent)
val deltaX = motionEvent.rawX - downX
val deltaY = motionEvent.rawY - downY
if (abs(deltaX) > slop && abs(deltaY) < abs(deltaX) / 2) {
swiping = true
callbacks.onSwipe(true)
swipingSlop = if (deltaX > 0) slop else -slop
this.view.parent.requestDisallowInterceptTouchEvent(true)
val cancelEvent = MotionEvent.obtain(motionEvent)
cancelEvent.action =
MotionEvent.ACTION_CANCEL or
(motionEvent.actionIndex shl MotionEvent.ACTION_POINTER_INDEX_SHIFT)
this.view.onTouchEvent(cancelEvent)
cancelEvent.recycle()
}
if (swiping) {
translationX = deltaX
this.view.translationX = deltaX - swipingSlop
this.view.alpha = 0f.coerceAtLeast(1f.coerceAtMost(1f - 2f * abs(deltaX) / viewWidth))
return true
}
}
}
else -> {
view.performClick()
return false
}
}
return false
}
private fun performDismiss() {
val lp = view.layoutParams
val originalHeight = view.height
val animator = ValueAnimator.ofInt(originalHeight, 1).setDuration(animationTime)
animator.addListener(
object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
callbacks.onDismiss(view)
view.alpha = 1f
view.translationX = 0f
lp.height = originalHeight
view.layoutParams = lp
}
}
)
animator.addUpdateListener { valueAnimator ->
lp.height = valueAnimator.animatedValue as Int
view.layoutParams = lp
}
animator.start()
}
internal interface DismissCallbacks {
fun onSwipe(isSwiping: Boolean)
fun onDismiss(view: View)
}
}
@@ -0,0 +1,52 @@
package com.itsaky.androidide.flashbar.anim
import android.content.Context
import android.view.View
import android.view.animation.AccelerateDecelerateInterpolator
import android.view.animation.AccelerateInterpolator
import android.view.animation.AnimationUtils
import android.view.animation.DecelerateInterpolator
import android.view.animation.Interpolator
import androidx.annotation.CallSuper
import androidx.annotation.InterpolatorRes
import com.itsaky.androidide.flashbar.R
abstract class BaseFlashAnimBuilder(private val context: Context) {
private val DEFAULT_DURATION =
context.resources.getInteger(R.integer.default_animation_duration).toLong()
protected val DEFAULT_ALPHA_START = 0.2f
protected val DEFAULT_ALPHA_END = 1.0f
protected var view: View? = null
protected var duration = DEFAULT_DURATION
protected var interpolator: Interpolator? = null
protected var alpha: Boolean = false
@CallSuper
open fun duration(millis: Long) = apply {
require(duration >= 0) { "Duration must not be negative" }
this.duration = millis
}
@CallSuper open fun accelerate() = apply { this.interpolator = AccelerateInterpolator() }
@CallSuper open fun decelerate() = apply { this.interpolator = DecelerateInterpolator() }
@CallSuper
open fun accelerateDecelerate() = apply { this.interpolator = AccelerateDecelerateInterpolator() }
@CallSuper
open fun interpolator(interpolator: Interpolator) = apply { this.interpolator = interpolator }
@CallSuper
open fun interpolator(@InterpolatorRes id: Int) = apply {
this.interpolator = AnimationUtils.loadInterpolator(context, id)
}
@CallSuper open fun alpha() = apply { this.alpha = true }
@CallSuper internal open fun withView(view: View) = apply { this.view = view }
}
@@ -0,0 +1,56 @@
package com.itsaky.androidide.flashbar.anim
import android.animation.Animator
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.content.Context
class FlashAnim(private val compositeAnim: AnimatorSet) {
companion object {
@JvmStatic fun with(context: Context) = FlashAnimRetriever(context)
}
internal fun start(listener: InternalAnimListener? = null) {
if (listener != null) {
val primaryAnim = compositeAnim.childAnimations[0] as ObjectAnimator
primaryAnim.addListener(
object : Animator.AnimatorListener {
override fun onAnimationRepeat(animator: Animator) {}
override fun onAnimationEnd(animator: Animator) {
listener.onStop()
primaryAnim.removeAllListeners()
primaryAnim.removeAllUpdateListeners()
}
override fun onAnimationCancel(animator: Animator) {}
override fun onAnimationStart(animator: Animator) {
listener.onStart()
}
}
)
primaryAnim.addUpdateListener { listener.onUpdate(it.animatedFraction) }
}
compositeAnim.start()
}
internal interface InternalAnimListener {
fun onStart()
fun onUpdate(progress: Float)
fun onStop()
}
}
class FlashAnimRetriever(private val context: Context) {
/** Retrieves the builder for animating flashbar */
fun animateBar() = FlashAnimBarBuilder(context)
/** Retrieves the builder for animating the icon in the flashbar */
fun animateIcon() = FlashAnimIconBuilder(context)
}
@@ -0,0 +1,140 @@
package com.itsaky.androidide.flashbar.anim
import android.animation.Animator
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.content.Context
import android.view.View
import android.view.animation.AnticipateInterpolator
import android.view.animation.Interpolator
import android.view.animation.OvershootInterpolator
import androidx.annotation.InterpolatorRes
import com.itsaky.androidide.flashbar.Flashbar
import com.itsaky.androidide.flashbar.Flashbar.Gravity.BOTTOM
import com.itsaky.androidide.flashbar.Flashbar.Gravity.TOP
import com.itsaky.androidide.flashbar.anim.FlashAnimBarBuilder.Direction.LEFT
import com.itsaky.androidide.flashbar.anim.FlashAnimBarBuilder.Direction.RIGHT
import com.itsaky.androidide.flashbar.anim.FlashAnimBarBuilder.Type.ENTER
import com.itsaky.androidide.flashbar.anim.FlashAnimBarBuilder.Type.EXIT
class FlashAnimBarBuilder(context: Context) : BaseFlashAnimBuilder(context) {
private var type: Type? = null
private var gravity: Flashbar.Gravity? = null
private var direction: Direction? = null
/** Specifies the duration (in millis) for the animation */
override fun duration(millis: Long) = apply { super.duration(millis) }
/** Specifies accelerate interpolator for the animation */
override fun accelerate() = apply { super.accelerate() }
/** Specifies decelerate interpolator for the animation */
override fun decelerate() = apply { super.decelerate() }
/** Specifies accelerate-decelerate interpolator for the animation */
override fun accelerateDecelerate() = apply { super.accelerateDecelerate() }
/** Specifies custom interpolator for the animation */
override fun interpolator(interpolator: Interpolator) = apply { super.interpolator(interpolator) }
/** Specifies custom interpolator resource for the animation */
override fun interpolator(@InterpolatorRes id: Int) = apply { super.interpolator(id) }
/** Specifies that the animation should have alpha effect */
override fun alpha() = apply { super.alpha() }
/** Specifies that the bar should slide to/from the left */
fun slideFromLeft() = apply { this.direction = LEFT }
/** Specifies that the bar should slide to/from the right */
fun slideFromRight() = apply { this.direction = RIGHT }
/** Specifies overshoot interpolator for the animation */
fun overshoot() = apply { this.interpolator = OvershootInterpolator() }
/** Specifies overshoot-anticipate interpolator for the animation */
fun anticipateOvershoot() = apply { this.interpolator = AnticipateInterpolator() }
override fun withView(view: View) = apply { super.withView(view) }
internal fun enter() = apply { this.type = ENTER }
internal fun exit() = apply { this.type = EXIT }
internal fun fromTop() = apply { this.gravity = TOP }
internal fun fromBottom() = apply { this.gravity = BOTTOM }
internal fun build(): FlashAnim {
requireNotNull(view, { "Target view can not be null" })
val compositeAnim = AnimatorSet()
val animators = linkedSetOf<Animator>()
val translationAnim = ObjectAnimator()
// Slide from left/right animation is not specified, default top/bottom
// animation is applied
if (direction == null) {
translationAnim.setPropertyName("translationY")
when (type!!) {
ENTER ->
when (gravity!!) {
TOP -> translationAnim.setFloatValues(-view!!.height.toFloat(), 0f)
BOTTOM -> translationAnim.setFloatValues(view!!.height.toFloat(), 0f)
}
EXIT ->
when (gravity!!) {
TOP -> translationAnim.setFloatValues(0f, -view!!.height.toFloat())
BOTTOM -> translationAnim.setFloatValues(0f, view!!.height.toFloat())
}
}
} else {
translationAnim.setPropertyName("translationX")
when (type!!) {
ENTER ->
when (direction!!) {
LEFT -> translationAnim.setFloatValues(-view!!.width.toFloat(), 0f)
RIGHT -> translationAnim.setFloatValues(view!!.width.toFloat(), 0f)
}
EXIT ->
when (direction!!) {
LEFT -> translationAnim.setFloatValues(0f, -view!!.width.toFloat())
RIGHT -> translationAnim.setFloatValues(0f, view!!.width.toFloat())
}
}
}
translationAnim.target = view
animators.add(translationAnim)
if (alpha) {
val alphaAnim = ObjectAnimator()
alphaAnim.setPropertyName("alpha")
alphaAnim.target = view
when (type!!) {
ENTER -> alphaAnim.setFloatValues(DEFAULT_ALPHA_START, DEFAULT_ALPHA_END)
EXIT -> alphaAnim.setFloatValues(DEFAULT_ALPHA_END, DEFAULT_ALPHA_START)
}
animators.add(alphaAnim)
}
compositeAnim.playTogether(animators)
compositeAnim.duration = duration
compositeAnim.interpolator = interpolator
return FlashAnim(compositeAnim)
}
enum class Type {
ENTER,
EXIT
}
enum class Direction {
LEFT,
RIGHT
}
}
@@ -0,0 +1,90 @@
package com.itsaky.androidide.flashbar.anim
import android.animation.Animator
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.animation.PropertyValuesHolder
import android.animation.ValueAnimator.INFINITE
import android.animation.ValueAnimator.REVERSE
import android.content.Context
import android.view.View
import android.view.animation.Interpolator
import androidx.annotation.InterpolatorRes
class FlashAnimIconBuilder(context: Context) : BaseFlashAnimBuilder(context) {
private val DEFAULT_PULSE_START = 1.0f
private val DEFAULT_PULSE_END = 0.6f
private var pulse: Boolean = false
private var pulseStart = 0f
private var pulseEnd = 0f
/** Specifies the duration (in millis) for the animation */
override fun duration(millis: Long) = apply { super.duration(millis) }
/** Specifies accelerate interpolator for the animation */
override fun accelerate() = apply { super.accelerate() }
/** Specifies decelerate interpolator for the animation */
override fun decelerate() = apply { super.decelerate() }
/** Specifies accelerate-decelerate interpolator for the animation */
override fun accelerateDecelerate() = apply { super.accelerateDecelerate() }
/** Specifies custom interpolator for the animation */
override fun interpolator(interpolator: Interpolator) = apply { super.interpolator(interpolator) }
/** Specifies custom interpolator resource for the animation */
override fun interpolator(@InterpolatorRes id: Int) = apply { super.interpolator(id) }
/** Specifies that the animation should have alpha effect */
override fun alpha() = apply { super.alpha() }
/** Specifies that the animation should have pulse effect */
@JvmOverloads
fun pulse(start: Float = DEFAULT_PULSE_START, end: Float = DEFAULT_PULSE_END) = apply {
this.pulse = true
this.pulseStart = start
this.pulseEnd = end
}
override fun withView(view: View) = apply { super.withView(view) }
internal fun build(): FlashAnim {
requireNotNull(view, { "Target view can not be null" })
val compositeAnim = AnimatorSet()
val animators = linkedSetOf<Animator>()
if (pulse) {
val scaleAnim =
ObjectAnimator.ofPropertyValuesHolder(
view,
PropertyValuesHolder.ofFloat("scaleX", pulseStart, pulseEnd),
PropertyValuesHolder.ofFloat("scaleY", pulseStart, pulseEnd)
)
scaleAnim.repeatCount = INFINITE
scaleAnim.repeatMode = REVERSE
animators.add(scaleAnim)
}
if (alpha) {
val alpha = ObjectAnimator.ofFloat(view, "alpha", 1.0f, 0.6f)
alpha.repeatCount = INFINITE
alpha.repeatMode = REVERSE
animators.add(alpha)
}
compositeAnim.playTogether(animators)
compositeAnim.duration = duration
compositeAnim.interpolator = interpolator
return FlashAnim(compositeAnim)
}
}
@@ -0,0 +1,121 @@
package com.itsaky.androidide.flashbar.util
import android.app.Activity
import android.content.Context
import android.graphics.Point
import android.graphics.Rect
import android.os.Build
import android.util.DisplayMetrics
import android.view.Surface.ROTATION_0
import android.view.Surface.ROTATION_180
import android.view.Surface.ROTATION_270
import android.view.Surface.ROTATION_90
import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver
import android.view.Window
import android.view.WindowManager
import com.itsaky.androidide.flashbar.util.NavigationBarPosition.BOTTOM
import com.itsaky.androidide.flashbar.util.NavigationBarPosition.LEFT
import com.itsaky.androidide.flashbar.util.NavigationBarPosition.RIGHT
import com.itsaky.androidide.flashbar.util.NavigationBarPosition.TOP
import kotlin.math.roundToInt
internal fun Activity.getStatusBarHeightInPx(): Int {
val rectangle = Rect()
window.decorView.getWindowVisibleDisplayFrame(rectangle)
val statusBarHeight = rectangle.top
val contentViewTop = window.findViewById<View>(Window.ID_ANDROID_CONTENT).top
return if (contentViewTop == 0) { // Actionbar is not present
statusBarHeight
} else {
contentViewTop - statusBarHeight
}
}
internal fun Activity.getNavigationBarPosition(): NavigationBarPosition {
val display =
if (Build.VERSION.SDK_INT >= 30) {
display
} else {
@Suppress("DEPRECATION") windowManager.defaultDisplay
}
return when (display?.rotation) {
ROTATION_0 -> BOTTOM
ROTATION_90 -> RIGHT
ROTATION_180 -> TOP
ROTATION_270 -> LEFT
else -> BOTTOM
}
}
internal fun Activity.getNavigationBarSizeInPx(): Int {
val realScreenSize = getRealScreenSize()
val appUsableScreenSize = getAppUsableScreenSize()
val navigationBarPosition = getNavigationBarPosition()
return if (navigationBarPosition == LEFT || navigationBarPosition == RIGHT) {
realScreenSize.x - appUsableScreenSize.x
} else {
realScreenSize.y - appUsableScreenSize.y
}
}
internal fun Activity?.getRootView(): ViewGroup? {
if (this == null || window == null) {
return null
}
return window.decorView as ViewGroup
}
internal fun Context.convertDpToPx(dp: Int): Int {
return (dp * (resources.displayMetrics.xdpi / DisplayMetrics.DENSITY_DEFAULT)).roundToInt()
}
internal fun Context.convertPxToDp(px: Int): Int {
return (px / (resources.displayMetrics.xdpi / DisplayMetrics.DENSITY_DEFAULT)).roundToInt()
}
private fun Activity.getRealScreenSize(): Point {
val windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager
val defaultDisplay =
if (Build.VERSION.SDK_INT >= 30) {
display
} else {
@Suppress("DEPRECATION") windowManager.defaultDisplay
}
val size = Point()
defaultDisplay?.getRealSize(size)
return size
}
private fun Activity.getAppUsableScreenSize(): Point {
val windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager
val defaultDisplay = windowManager.defaultDisplay
val size = Point()
defaultDisplay.getSize(size)
return size
}
inline fun <T : View> T.afterMeasured(crossinline f: T.() -> Unit) {
viewTreeObserver.addOnGlobalLayoutListener(
object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
if (measuredWidth > 0 && measuredHeight > 0) {
viewTreeObserver.removeOnGlobalLayoutListener(this)
f()
}
}
}
)
}
internal enum class NavigationBarPosition {
BOTTOM,
RIGHT,
LEFT,
TOP
}
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/translucent_black" android:state_pressed="true" />
</selector>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:angle="90"
android:endColor="@color/shadow_color"
android:startColor="@android:color/transparent" />
</shape>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:angle="-90"
android:endColor="@color/shadow_color"
android:startColor="@android:color/transparent" />
</shape>
@@ -0,0 +1,142 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="true"
android:fitsSystemWindows="true"
android:focusable="true"
app:cardBackgroundColor="?colorPrimaryContainer"
app:cardElevation="@dimen/fb_card_elevation"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Material3.Corner.Bottom">
<LinearLayout
android:id="@+id/fbRoot"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/fb_content_padding_all"
android:orientation="vertical">
<LinearLayout
android:id="@+id/fbContent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
tools:ignore="UselessParent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical">
<com.google.android.material.progressindicator.CircularProgressIndicator
android:id="@+id/fbLeftProgress"
android:layout_width="@dimen/fb_progress_size"
android:layout_height="@dimen/fb_progress_size"
android:layout_marginEnd="@dimen/fb_margin_progress"
android:layout_marginRight="@dimen/fb_margin_progress"
android:layout_gravity="center_vertical"
android:indeterminate="true"
android:visibility="gone" />
<ImageView
android:id="@+id/fbIcon"
android:layout_width="@dimen/fb_icon_size"
android:layout_height="@dimen/fb_icon_size"
android:layout_marginEnd="@dimen/fb_margin_right_icon"
android:layout_marginRight="@dimen/fb_margin_right_icon"
android:layout_gravity="center_vertical"
android:importantForAccessibility="no"
android:src="@drawable/ic_info"
android:visibility="gone"
app:tint="?colorOnPrimaryContainer"
tools:visibility="visible" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="8"
android:orientation="vertical">
<TextView
android:id="@+id/fbTitle"
android:textStyle="bold"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/fb_title_message_margin"
android:textAppearance="@style/TextAppearance.Material3.TitleMedium"
android:textColor="?colorOnPrimaryContainer"
android:visibility="gone"
tools:text="Title"
tools:visibility="visible" />
<TextView
android:id="@+id/fbMessage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/fb_title_message_margin"
android:justificationMode="inter_word"
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
android:visibility="gone"
tools:text="A quick brown fox jumps of the lazy dog."
tools:visibility="visible" />
</LinearLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/fbPrimaryAction"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAllCaps="false"
android:visibility="gone"
tools:text="CLICK"
tools:visibility="visible" />
<com.google.android.material.progressindicator.CircularProgressIndicator
android:id="@+id/fbRightProgress"
android:layout_width="@dimen/fb_progress_size"
android:layout_height="@dimen/fb_progress_size"
android:layout_marginStart="@dimen/fb_margin_progress"
android:layout_marginLeft="@dimen/fb_margin_progress"
android:layout_gravity="center_vertical"
android:indeterminate="true"
android:visibility="gone" />
</LinearLayout>
<LinearLayout
android:id="@+id/fbSecondaryActionContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/fb_margin_secondary_action_container"
android:gravity="end"
android:visibility="gone">
<com.google.android.material.button.MaterialButton
android:id="@+id/fbNegativeAction"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
tools:text="CANCEL"
tools:visibility="gone" />
<com.google.android.material.button.MaterialButton
android:id="@+id/fbPositiveAction"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/fb_margin_secondary_action"
android:layout_marginLeft="@dimen/fb_margin_secondary_action"
android:visibility="gone"
tools:text="OK"
tools:visibility="gone" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="FbProgress">
<attr name="fbp_progressIndeterminate" format="boolean" />
<attr name="fbp_barColor" format="color" />
<attr name="fbp_rimColor" format="color" />
<attr name="fbp_rimWidth" format="dimension" />
<attr name="fbp_spinSpeed" format="float" />
<attr name="fbp_barSpinCycleTime" format="integer" />
<attr name="fbp_circleRadius" format="dimension" />
<attr name="fbp_fillRadius" format="boolean" />
<attr name="fbp_barWidth" format="dimension" />
<attr name="fbp_linearProgress" format="boolean" />
</declare-styleable>
</resources>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="warm_grey">#8e8e8e</color>
<color name="white">#fff</color>
<color name="shadow_color">#94444444</color>
<color name="translucent_black">#22000000</color>
<color name="modal">#80272727</color>
<color name="slate_black">#FF333333</color>
<color name="yellow">#FFEB3B</color>
</resources>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="fb_card_elevation">8dp</dimen>
<dimen name="fb_content_padding_all">16dp</dimen>
<dimen name="fb_title_text_size">16sp</dimen>
<dimen name="fb_message_text_size">14sp</dimen>
<dimen name="fb_title_message_margin">2dp</dimen>
<dimen name="fb_icon_size">28dp</dimen>
<dimen name="fb_margin_right_icon">16dp</dimen>
<dimen name="fb_button_text_size">16sp</dimen>
<dimen name="fb_top_compensation_margin">16dp</dimen>
<dimen name="fb_bottom_compensation_margin">30dp</dimen>
<dimen name="fb_progress_size">20dp</dimen>
<dimen name="fb_margin_secondary_action">12dp</dimen>
<dimen name="fb_margin_secondary_action_container">12dp</dimen>
<dimen name="fb_margin_progress">20dp</dimen>
</resources>
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<integer name="default_animation_duration">250</integer>
</resources>
+2
View File
@@ -0,0 +1,2 @@
/libs/android.jar
/build
@@ -0,0 +1,29 @@
/*
* 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/>.
*/
@Suppress("JavaPluginLanguageLevel")
plugins {
id("java-library")
}
groupConfig {
groupIdSuffix.set("utils")
}
dependencies {
compileOnlyApi(files ("libs/android.jar"))
}
+1
View File
@@ -0,0 +1 @@
/build
+41
View File
@@ -0,0 +1,41 @@
/*
* 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/>.
*/
@Suppress("JavaPluginLanguageLevel")
plugins {
id("java-library")
id("org.jetbrains.kotlin.jvm")
id("kotlin-kapt")
}
groupConfig {
groupIdSuffix.set("utils")
}
dependencies {
kapt(libs.google.auto.service)
implementation(projects.logging.logger)
implementation(projects.utilities.shared)
implementation(libs.common.jkotlin)
implementation(libs.google.auto.service.annotations)
implementation(libs.google.guava)
testImplementation(libs.tests.junit)
testImplementation(libs.tests.google.truth)
}
@@ -0,0 +1,25 @@
/*
* 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.lookup
/**
* Forwards all calls to the provided lookup instance.
*
* @author Akash Yadav
*/
open class ForwardingLookup(lookup: Lookup) : Lookup by lookup
@@ -0,0 +1,123 @@
/*
* 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.lookup;
import org.jetbrains.annotations.Nullable;
/**
* <strong>Lookup</strong> provides API to look for registered services.
*
* @author Akash Yadav
*/
public interface Lookup {
/**
* Get the default implementation of {@link Lookup} service.
*
* @return The default implementation.
*/
static Lookup getDefault() {
return LookupProvider.lookupService();
}
/**
* Register the service for the given class.
*
* @param klass The class to register the service for.
* @param instance The implementation of the service.
* @param <T> The type of service.
*/
<T> void register(Class<T> klass, T instance);
/**
* Updates the service for the given class. Registers if not already registered.
*
* @param klass The class to update the service for.
* @param instance The instance of the service.
* @param <T> The type of the service.
*/
<T> void update(Class<T> klass, T instance);
/**
* Unregister the service for given class.
*
* @param klass The class of service to unregister.
* @param <T> The type of service.
*/
<T> void unregister(Class<T> klass);
/**
* Lookup the service for the given class.
*
* @param klass The Class of the service to look for.
* @param <T> The type of service.
* @return The instance of the registered service or <code>null</code>.
*/
@Nullable
<T> T lookup(Class<T> klass);
/**
* Register the service for the given key.
*
* @param key The class to register the service for.
* @param instance The implementation of the service.
* @param <T> The type of service.
*/
<T> void register(Key<T> key, T instance);
/**
* Unregister the service for given key.
*
* @param key The key of service to unregister.
* @param <T> The type of service.
*/
<T> void unregister(Key<T> key);
/**
* Lookup the service for the given key.
*
* @param key The key of the service to look for.
* @param <T> The type of service.
* @return The instance of the registered service or <code>null</code>.
*/
@Nullable
<T> T lookup(Key<T> key);
/**
* Updates the service for the given key. Registers if not already registered.
*
* @param key The key to update the service for.
* @param instance The instance of the service.
* @param <T> The type of the service.
*/
<T> void update(Key<T> key, T instance);
/**
* Unregister all registered services.
*/
void unregisterAll();
/**
* The key that is used to register services in the table.
*
* @param <T> The type of the service.
*/
class Key<T> {
// we inherit identity equality from Object
}
}
@@ -0,0 +1,48 @@
/*
* 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.lookup;
import com.itsaky.androidide.lookup.internal.DefaultLookup;
import com.itsaky.androidide.utils.ServiceLoader;
import com.itsaky.androidide.utils.VMUtils;
/**
* Provides instance of {@link Lookup}.
*
* @author Akash Yadav
*/
class LookupProvider {
private static Lookup sLookup;
private static final Object lock = new Object();
static synchronized Lookup lookupService() {
synchronized (lock) {
if (sLookup != null) {
return sLookup;
}
if (VMUtils.isJvm()) {
// When in a test environment, load the default lookup
return sLookup = new DefaultLookup();
}
return sLookup = ServiceLoader.load(Lookup.class).findFirstOrThrow();
}
}
}
@@ -0,0 +1,25 @@
/*
* 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.lookup;
/**
* Thrown when a service trying to register a service and it is already registered.
*
* @author Akash Yadav
*/
public class ServiceRegisteredException extends RuntimeException {}
@@ -0,0 +1,110 @@
/*
* 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.lookup.internal;
import com.google.auto.service.AutoService;
import com.itsaky.androidide.lookup.Lookup;
import com.itsaky.androidide.lookup.ServiceRegisteredException;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Default implementation of {@link Lookup}.
*
* @author Akash Yadav
*/
@AutoService(Lookup.class)
public final class DefaultLookup implements Lookup {
private final Map<Class<?>, Key<?>> keyTable = new ConcurrentHashMap<>();
private final Map<Key<?>, Object> services = new ConcurrentHashMap<>();
@Override
public <T> void register(final Class<T> klass, final T instance) {
final Key<T> key = key(klass);
keyTable.put(klass, key);
register(key, instance);
}
@Override
public <T> void update(final Class<T> klass, final T instance) {
update(key(klass), instance);
}
@Override
public <T> void unregister(final Class<T> klass) {
unregister(key(klass));
}
@Nullable
@Override
public <T> T lookup(final Class<T> klass) {
return lookup(key(klass));
}
@Override
public <T> void register(final Key<T> key, final T instance) {
registerOrUpdate(key, instance, false);
}
@Override
public <T> void unregister(final Key<T> key) {
services.remove(key);
}
@SuppressWarnings("unchecked")
@Nullable
@Override
public <T> T lookup(final Key<T> key) {
return (T) services.get(key);
}
@Override
public <T> void update(final Key<T> key, final T instance) {
registerOrUpdate(key, instance, true);
}
@Override
public void unregisterAll() {
services.clear();
}
@SuppressWarnings("unchecked")
private <T> void registerOrUpdate(final Key<T> key, final T instance, boolean update) {
final T existing = (T) services.put(key, instance);
if (existing != null && !update) {
services.put(key, existing);
throw new ServiceRegisteredException();
}
}
@SuppressWarnings("unchecked")
@NotNull
private <T> Key<T> key(Class<T> klass) {
final var key = keyTable.get(klass);
if (key == null) {
// Returning a new key instance will always make the lookup methods above return null
return new Key<>();
}
return (Key<T>) key;
}
}
@@ -0,0 +1,95 @@
/*
* 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.lookup
import com.google.common.truth.Truth.assertThat
import com.itsaky.androidide.lookup.Lookup.Key
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
/** @author Akash Yadav */
@RunWith(JUnit4::class)
class LookupTest {
@Test
fun `test with class`() {
val lookup = Lookup.getDefault()
// Test register
var service = TestServiceImpl()
lookup.register(TestService::class.java, service)
lookup.lookup(TestService::class.java).apply {
assertThat(this).isNotNull()
assertThat(this).isEqualTo(service)
}
// Test update
service = TestServiceImpl()
lookup.update(TestService::class.java, service)
lookup.lookup(TestService::class.java).apply {
assertThat(this).isNotNull()
assertThat(this).isEqualTo(service)
}
lookup.unregister(TestService::class.java)
lookup.lookup(TestService::class.java).apply { assertThat(this).isNull() }
}
@Test
fun `test with key`() {
val lookup = Lookup.getDefault()
val key = Key<TestService>()
// Test register
var service = TestServiceImpl()
lookup.register(key, service)
lookup.lookup(key).apply {
assertThat(this).isNotNull()
assertThat(this).isEqualTo(service)
}
// Service not registered with the Class object. Must return null.
lookup.lookup(TestService::class.java).apply { assertThat(this).isNull() }
// Test update
service = TestServiceImpl()
lookup.update(key, service)
lookup.lookup(key).apply {
assertThat(this).isNotNull()
assertThat(this).isEqualTo(service)
}
// Service not registered with the Class object. Must return null.
lookup.lookup(TestService::class.java).apply { assertThat(this).isNull() }
lookup.unregister(key)
lookup.lookup(key).apply { assertThat(this).isNull() }
}
interface TestService {
fun doTest()
}
class TestServiceImpl : TestService {
override fun doTest() {
println("From test service")
}
}
}
+85
View File
@@ -0,0 +1,85 @@
# Built application files
*.apk
*.aar
*.ap_
*.aab
# Files for the ART/Dalvik VM
*.dex
# Java class files
*.class
# Generated files
/bin/
/gen/
/out/
# Uncomment the following line in case you need and you don't have the release build type files in your app
# release/
# Gradle files
.gradle/
/build/
# Local configuration file (sdk path, etc)
local.properties
# Proguard folder generated by Eclipse
proguard/
# Log Files
*.log
# Android Studio Navigation editor temp files
.navigation/
# Android Studio captures folder
captures/
# IntelliJ
*.iml
.idea/workspace.xml
.idea/tasks.xml
.idea/gradle.xml
.idea/assetWizardSettings.xml
.idea/dictionaries
.idea/libraries
# Android Studio 3 in .gitignore file.
.idea/caches
.idea/modules.xml
# Comment next line if keeping position of elements in Navigation Editor is relevant for you
.idea/navEditor.xml
# Keystore files
# Uncomment the following lines if you do not want to check your keystore files in.
#*.jks
#*.keystore
# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
.cxx/
# Google Services (e.g. APIs or Firebase)
# google-services.json
# Freeline
freeline.py
freeline/
freeline_project_description.json
# fastlane
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots
fastlane/test_output
fastlane/readme.md
# Version control
vcs.xml
# lint
lint/intermediates/
lint/generated/
lint/outputs/
lint/tmp/
# lint/reports/
+43
View File
@@ -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/>.
*/
import com.itsaky.androidide.build.config.BuildConfig
plugins {
id("com.android.library")
id("kotlin-android")
id("kotlin-parcelize")
}
groupConfig {
groupIdSuffix.set("utils")
}
android {
namespace = "${BuildConfig.packageName}.preferences"
}
dependencies {
implementation(projects.core.common)
implementation(projects.core.resources)
implementation(projects.event.eventbusEvents)
implementation(projects.utilities.shared)
implementation(libs.androidx.annotation)
implementation(libs.androidx.preference)
implementation(libs.google.material)
}
+21
View File
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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/>.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>
@@ -0,0 +1,36 @@
/*
* 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.preferences
import android.content.Context
import androidx.preference.Preference
sealed class BaseIDEPreferences : IPreferenceScreen() {
override val key: String
get() = throw UnsupportedOperationException()
override val title: Int
get() = throw UnsupportedOperationException()
override val summary: Int
get() = throw UnsupportedOperationException()
override val icon: Int
get() = throw UnsupportedOperationException()
override fun onCreateView(context: Context): Preference {
throw UnsupportedOperationException()
}
}
@@ -0,0 +1,63 @@
/*
* 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.preferences
import android.content.Context
import android.graphics.PorterDuff.Mode.SRC_ATOP
import android.graphics.PorterDuffColorFilter
import androidx.core.content.ContextCompat
import androidx.preference.Preference
import com.itsaky.androidide.utils.resolveAttr
/**
* Base class for preferences.
*
* @author Akash Yadav
*/
abstract class BasePreference : IPreference() {
abstract fun onCreatePreference(context: Context): Preference
override fun onCreateView(context: Context): Preference {
val pref = onCreatePreference(context)
pref.key = this.key
pref.title = context.getString(this.title)
this.summary?.let { pref.summary = context.getString(it) }
pref.isIconSpaceReserved = this.icon != null
this.icon?.let {
pref.icon =
ContextCompat.getDrawable(context, it)?.apply {
colorFilter =
PorterDuffColorFilter(context.resolveAttr(R.attr.colorOnPrimaryContainer), SRC_ATOP)
}
}
pref.setOnPreferenceClickListener { onPreferenceClick(pref) }
pref.setOnPreferenceChangeListener(this::onPreferenceChanged)
return pref
}
protected open fun onPreferenceChanged(preference: Preference, newValue: Any?): Boolean {
return false
}
protected open fun onPreferenceClick(preference: Preference): Boolean {
return false
}
}
@@ -0,0 +1,75 @@
/*
* 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.preferences
import androidx.annotation.CallSuper
import androidx.preference.Preference
import com.google.android.material.dialog.MaterialAlertDialogBuilder
/**
* Base class for dialog preferences which allows users to choose from multiple items. Subclasses
* must call [onSelectionChanged] to notify about selection changes.
*
* @author Akash Yadav
*/
abstract class ChoiceBasedDialogPreference : DialogPreference(), PreferenceChoices {
private var choices = emptyArray<PreferenceChoices.Entry>()
final override fun onConfigureDialog(preference: Preference, dialog: MaterialAlertDialogBuilder) {
choices = getEntries(preference)
val selections = BooleanArray(choices.size) { choices[it].isChecked }
onConfigureDialogChoices(preference, dialog, choices, selections)
dialog.setPositiveButton(android.R.string.ok) { dialogInterface, _ ->
dialogInterface.dismiss()
onChoicesConfirmed(preference, choices)
}
dialog.setNegativeButton(android.R.string.cancel) { dialogInterface, _ ->
dialogInterface.dismiss()
onChoicesCancelled(preference)
}
}
@CallSuper
override fun onSelectionChanged(
preference: Preference,
entry: PreferenceChoices.Entry,
position: Int,
isSelected: Boolean
) {
entry._isChecked = isSelected
}
/**
* Configure the dialog choices.
*/
protected abstract fun onConfigureDialogChoices(
preference: Preference,
dialog: MaterialAlertDialogBuilder,
entries: Array<PreferenceChoices.Entry>,
selections: BooleanArray
)
override fun onChoicesConfirmed(preference: Preference, entries: Array<PreferenceChoices.Entry>) {
}
override fun onChoicesCancelled(preference: Preference) {}
}
@@ -0,0 +1,50 @@
/*
* 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.preferences
import androidx.preference.Preference
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.itsaky.androidide.utils.DialogUtils
/**
* A preference which shows a dialog when clicked.
*
* @author Akash Yadav
*/
abstract class DialogPreference : SimplePreference() {
open val dialogTitle: Int
get() = this.title
open val dialogMessage: Int? = null
open val dialogCancellable: Boolean = false
override fun onPreferenceClick(preference: Preference): Boolean {
val dialog = DialogUtils.newMaterialDialogBuilder(preference.context)
dialog.setTitle(this.dialogTitle)
dialogMessage?.let { dialog.setMessage(it) }
dialog.setCancelable(this.dialogCancellable)
onConfigureDialog(preference, dialog)
dialog.show()
return true
}
protected open fun onConfigureDialog(preference: Preference,
dialog: MaterialAlertDialogBuilder) {
}
}
@@ -0,0 +1,46 @@
/*
* 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.preferences
import android.view.LayoutInflater
import androidx.preference.Preference
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.textfield.TextInputLayout
import com.itsaky.androidide.preferences.databinding.LayoutDialogTextInputBinding
/**
* A preference which shows an edittext
*
* @author Akash Yadav
*/
abstract class EditTextPreference : DialogPreference() {
override fun onConfigureDialog(preference: Preference, dialog: MaterialAlertDialogBuilder) {
super.onConfigureDialog(preference, dialog)
val binding = LayoutDialogTextInputBinding.inflate(LayoutInflater.from(dialog.context))
onConfigureTextInput(binding.name)
dialog.setView(binding.root)
dialog.setPositiveButton(android.R.string.ok) { iface, _ ->
iface.dismiss()
onPreferenceChanged(preference, binding.name.editText?.text?.toString()?.trim())
}
dialog.setNegativeButton(android.R.string.cancel) { iface, _ -> iface.dismiss() }
}
protected open fun onConfigureTextInput(input: TextInputLayout) {}
}
@@ -0,0 +1,44 @@
/*
* 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.preferences
import android.os.Parcel
import android.os.Parcelable
/**
* The preferences for the IDE.
*
* @author Akash Yadav
*/
data object IDEPreferences : BaseIDEPreferences() {
override val children: List<IPreference> = mutableListOf()
override fun describeContents(): Int = 0
override fun writeToParcel(dest: Parcel, flags: Int) {}
@JvmField
val CREATOR = object : Parcelable.Creator<IDEPreferences> {
override fun createFromParcel(source: Parcel?): IDEPreferences {
return IDEPreferences
}
override fun newArray(size: Int): Array<IDEPreferences> {
return Array(size) { IDEPreferences }
}
}
}
@@ -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/>.
*/
package com.itsaky.androidide.preferences
import android.content.Context
import android.os.Parcelable
import androidx.preference.Preference
import kotlinx.parcelize.Parcelize
/**
* A preference shown on the preference screen.
*
* @author Akash Yadav
*/
abstract class IPreference : Parcelable {
/** Icon resource for this preference. */
open val icon: Int? = null
/** Key that will be used to store the value of this preference in shared preferences. */
abstract val key: String
/** The title of the preference. */
abstract val title: Int
/** The summary of the preference. */
open val summary: Int? = null
abstract fun onCreateView(context: Context): Preference
}
@@ -0,0 +1,52 @@
/*
* 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.preferences
import android.content.Context
import androidx.preference.Preference
import androidx.preference.PreferenceCategory
/**
* A group of preferences.
*
* @author Akash Yadav
*/
abstract class IPreferenceGroup : BasePreference() {
/** The preferences. */
abstract val children: List<IPreference>
/** Adds the given preference to the preferences list. */
fun addPreference(preference: IPreference) {
(children as MutableList).add(preference)
}
/** Removes the given preference. */
fun removePreference(preference: IPreference) {
(children as MutableList).remove(preference)
}
/** Removes the preference at the given index. */
fun removePreference(index: Int) {
(children as MutableList).removeAt(index)
}
override fun onCreatePreference(context: Context): Preference {
return PreferenceCategory(context)
}
}
@@ -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.preferences
import androidx.preference.Preference
/**
* Converts [IPreference] to [Preference].
*
* @author Akash Yadav
*/
interface IPreferenceInflater {
/**
* Uses the given [IPreference] to create a [Preference] view.
*/
fun inflate(preference: IPreference) : Preference
}
@@ -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.preferences
import android.content.Context
import androidx.preference.Preference
/**
* A preference screen which will be shown in a fragment.
*
* @author Akash Yadav
*/
abstract class IPreferenceScreen : IPreferenceGroup() {
override fun onCreatePreference(context: Context): Preference {
return Preference(context)
}
}
@@ -0,0 +1,50 @@
/*
* 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.preferences
import androidx.preference.Preference
import com.google.android.material.dialog.MaterialAlertDialogBuilder
/**
* A preference with multiple choices to select from.
*
* The [onSelectionChanged] is called whenever the
* [DialogInterface.OnMultiChoiceClickListener][android.content.DialogInterface.OnMultiChoiceClickListener]
* is called.
*
* @author Akash Yadav
*/
abstract class MultiChoicePreference : ChoiceBasedDialogPreference(), PreferenceChoices {
override fun onConfigureDialogChoices(
preference: Preference,
dialog: MaterialAlertDialogBuilder,
entries: Array<PreferenceChoices.Entry>,
selections: BooleanArray
) {
val labels = Array(entries.size) { entries[it].label }
dialog.setMultiChoiceItems(
labels,
selections
) { _, which, checked ->
onSelectionChanged(preference, entries[which], which, checked)
}
}
}
@@ -0,0 +1,70 @@
/*
* 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.preferences
import android.text.method.DigitsKeyListener
import android.view.LayoutInflater
import android.view.inputmethod.EditorInfo
import androidx.preference.Preference
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.textfield.TextInputLayout
import com.itsaky.androidide.preferences.databinding.LayoutDialogTextInputBinding
/**
* A preference which shows an edittext
*
* @author Akash Yadav
*/
abstract class NumberInputEditTextPreference(
val hint: Int? = null,
val setValue: ((Int) -> Unit)? = null,
val getValue: (() -> Int)? = null
) : DialogPreference() {
override fun onConfigureDialog(preference: Preference, dialog: MaterialAlertDialogBuilder) {
super.onConfigureDialog(preference, dialog)
val binding = LayoutDialogTextInputBinding.inflate(LayoutInflater.from(dialog.context))
onConfigureTextInput(binding.name)
dialog.setView(binding.root)
dialog.setPositiveButton(android.R.string.ok) { iface, _ ->
iface.dismiss()
onPreferenceChanged(preference, binding.name.editText?.text?.toString()?.trim())
}
dialog.setNegativeButton(android.R.string.cancel) { iface, _ -> iface.dismiss() }
}
protected open fun onConfigureTextInput(input: TextInputLayout) {
input.startIconDrawable = null
hint?.let { input.setHint(it) } ?: run { input.hint = "" }
input.editText?.apply {
inputType = EditorInfo.TYPE_NUMBER_FLAG_SIGNED
imeOptions = EditorInfo.IME_ACTION_DONE
keyListener = DigitsKeyListener.getInstance("0123456789")
setText(prefValue())
}
}
override fun onPreferenceChanged(preference: Preference, newValue: Any?): Boolean {
setValue?.let { it((newValue as String?)?.toInt() ?: 0) }
return true
}
private fun prefValue(): String {
return getValue?.let { it() }?.toString() ?: ""
}
}
@@ -0,0 +1,83 @@
/*
* 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.preferences
import androidx.annotation.RestrictTo
import androidx.preference.Preference
/**
* A preference with choices.
*
* @author Akash Yadav
*/
interface PreferenceChoices {
/**
* Get the entries for this preference.
*/
fun getEntries(preference: Preference): Array<Entry>
/**
* Called when an item is selected from the single choice list.
*
* @param position The position of the selected item.
* @param isSelected Whether the item is selected.
*/
fun onSelectionChanged(preference: Preference, entry: Entry, position: Int, isSelected: Boolean)
/**
* Called when the user confirms the selections.
*
* @param entries The entries.
*/
fun onChoicesConfirmed(preference: Preference, entries: Array<Entry>)
/**
* Called when the user cancels the selections.
*/
fun onChoicesCancelled(preference: Preference)
/**
* Entry in [PreferenceChoices].
*
* @property label The label for the entry.
* @property isChecked Whether the item is checked or not.
* @property data The data object for the value.
*/
data class Entry(
val label: CharSequence,
@Suppress("PropertyName") internal var _isChecked: Boolean,
val data: Any,
) {
val isChecked: Boolean
get() = _isChecked
companion object {
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
val EMPTY = Entry("", false, 0)
}
}
}
/**
* Map this [PreferenceChoices.Entry] array to entry labels.
*/
internal val Array<PreferenceChoices.Entry>.labels: Array<CharSequence>
get() = Array(size) { this[it].label }
@@ -0,0 +1,44 @@
/*
* 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.preferences
/**
* The type of value a preference holds.
*
* @author Akash Yadav
*/
enum class PreferenceValueType {
/** A string preference. */
STRING,
/** An integer preference. */
INT,
/** A float preference. */
FLOAT,
/** A double preference. */
DOUBLE,
/** A boolean preference. */
BOOLEAN,
/** No preference value is required. */
NONE
}
@@ -0,0 +1,42 @@
/*
* 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.preferences
import androidx.preference.Preference
import kotlinx.parcelize.Parcelize
/**
* A simple preference which is expected to be clickable only.
*
* @author Akash Yadav
*/
@Parcelize
class SimpleClickablePreference
@JvmOverloads
constructor(
override val key: String,
override val title: Int,
override val summary: Int? = null,
override val icon: Int? = null,
private val onClick: ((Preference) -> Boolean)? = { false }
) : SimplePreference() {
override fun onPreferenceClick(preference: Preference): Boolean {
return onClick?.let { it(preference) } ?: false
}
}
@@ -0,0 +1,32 @@
/*
* 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.preferences
import android.content.Context
import androidx.preference.Preference
/**
* Abstract simple preference.
*
* @author Akash Yadav
*/
abstract class SimplePreference() : BasePreference() {
override fun onCreatePreference(context: Context): Preference {
return Preference(context)
}
}
@@ -0,0 +1,79 @@
/*
* 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.preferences
import androidx.preference.Preference
import com.google.android.material.dialog.MaterialAlertDialogBuilder
/**
* A preference which allows selecting a single value from a list of values.
*
* The [onSelectionChanged] method is called exactly two times when the user changes the selection, first call for the previously
* selected item and second call for the newly selected item.
*
* The [onChoicesConfirmed] is always called with a singleton list.
*
* @author Akash Yadav
*/
abstract class SingleChoicePreference : ChoiceBasedDialogPreference(), PreferenceChoices {
/**
* The currently selected item in the dialog.
*/
protected open var currentSelection: Int = -1
override fun onConfigureDialogChoices(
preference: Preference,
dialog: MaterialAlertDialogBuilder,
entries: Array<PreferenceChoices.Entry>,
selections: BooleanArray
) {
currentSelection = entries.indexOfFirst { it.isChecked }
dialog.setSingleChoiceItems(
entries.labels,
currentSelection
)
{ _, position ->
if (currentSelection != -1) {
onSelectionChanged(preference, entries[currentSelection], currentSelection, false)
}
currentSelection = position
onSelectionChanged(preference, entries[currentSelection], position, true)
}
}
override fun onChoicesConfirmed(
preference: Preference,
entries: Array<PreferenceChoices.Entry>
) {
if (currentSelection < 0 || currentSelection > entries.lastIndex) {
onChoiceConfirmed(preference, null, currentSelection)
} else {
onChoiceConfirmed(preference, entries[currentSelection], currentSelection)
}
}
protected open fun onChoiceConfirmed(
preference: Preference,
entry: PreferenceChoices.Entry?,
position: Int
) {
}
}
@@ -0,0 +1,50 @@
/*
* 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.preferences
import android.content.Context
import androidx.preference.Preference
import kotlin.reflect.KMutableProperty0
/**
* A switch preference.
*
* @author Akash Yadav
*/
abstract class SwitchPreference
@JvmOverloads
constructor(val setValue: ((Boolean) -> Unit)? = null, val getValue: (() -> Boolean)? = null) :
BasePreference() {
constructor(property: KMutableProperty0<Boolean>) : this(property::set, property::get)
override fun onCreatePreference(context: Context): Preference {
val pref = androidx.preference.SwitchPreference(context)
pref.isChecked = prefValue()
return pref
}
override fun onPreferenceChanged(preference: Preference, newValue: Any?): Boolean {
setValue?.let { it(newValue as Boolean? ?: prefValue()) }
return true
}
private fun prefValue(): Boolean {
return getValue?.let { it() } ?: false
}
}
@@ -0,0 +1,113 @@
/*
* 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.preferences.internal
/**
* @author Akash Yadav
*/
@Suppress("MemberVisibilityCanBePrivate")
object BuildPreferences {
const val STACKTRACE = "idepref_gradleCmd_stacktrace"
const val DEBUG = "idepref_gradleCmd_debug"
const val SCAN = "idepref_gradleCmd_scan"
const val INFO = "idepref_gradleCmd_info"
const val WARNING_MODE = "idepref_gradleCmd_warningMode"
const val BUILD_CACHE = "idepref_gradleCmd_buildCache"
const val OFFLINE_MODE = "idepref_gradleCmd_offlineMode"
const val GRADLE_COMMANDS = "idepref_build_gradleCommands"
const val GRADLE_CLEAR_CACHE = "idepref_build_gradleClearCache"
const val CUSTOM_GRADLE_INSTALLATION = "idepref_build_customGradleInstallation"
const val LAUNCH_APP_AFTER_INSTALL = "ide.build.run.launchAppAfterInstall"
const val PREF_JAVA_HOME = "ide.build.javaHome"
/** Switch for Gradle `--debug` option. */
var isDebugEnabled: Boolean
get() = prefManager.getBoolean(DEBUG)
set(enabled) {
prefManager.putBoolean(DEBUG, enabled)
}
/** Switch for Gradle `--scan` option. */
var isScanEnabled: Boolean
get() = prefManager.getBoolean(SCAN)
set(enabled) {
prefManager.putBoolean(SCAN, enabled)
}
/** Switch for Gradle `--warning-mode all` option. */
var isWarningModeAllEnabled: Boolean
get() = prefManager.getBoolean(WARNING_MODE)
set(enabled) {
prefManager.putBoolean(WARNING_MODE, enabled)
}
/** Switch for Gradle `--build-cache` option. */
var isBuildCacheEnabled: Boolean
get() = prefManager.getBoolean(BUILD_CACHE)
set(enabled) {
prefManager.putBoolean(BUILD_CACHE, enabled)
}
/** Switch for Gradle `--offline` option. */
var isOfflineEnabled: Boolean
get() = prefManager.getBoolean(OFFLINE_MODE)
set(enabled) {
prefManager.putBoolean(OFFLINE_MODE, enabled)
}
/** Switch for Gradle `--stacktrace` option. */
var isStacktraceEnabled: Boolean
get() = prefManager.getBoolean(STACKTRACE)
set(value) {
prefManager.putBoolean(STACKTRACE, value)
}
/** Switch for Gradle `--info` option. */
var isInfoEnabled: Boolean
get() = prefManager.getBoolean(INFO, GeneralPreferences.isFirstBuild)
set(enabled) {
prefManager.putBoolean(INFO, enabled)
}
/** Custom Gradle installation directory path. */
var gradleInstallationDir: String
get() = prefManager.getString(CUSTOM_GRADLE_INSTALLATION, "")
set(value) {
prefManager.putString(CUSTOM_GRADLE_INSTALLATION, value)
}
/**
* Whether the app should be launched automatically after installation (after build).
*/
var launchAppAfterInstall: Boolean
get() = prefManager.getBoolean(LAUNCH_APP_AFTER_INSTALL, false)
set(value) {
prefManager.putBoolean(LAUNCH_APP_AFTER_INSTALL, value)
}
/**
* The selected Java installation.
*/
var javaHome: String
get() = prefManager.getString(PREF_JAVA_HOME, "")
set(value) {
prefManager.putString(PREF_JAVA_HOME, value)
}
}
@@ -0,0 +1,42 @@
/*
* 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.preferences.internal
/**
* @author Akash Yadav
*/
@Suppress("MemberVisibilityCanBePrivate")
object DevOpsPreferences {
const val KEY_DEVOPTS = "ide.prefs.developerOptions"
const val KEY_DEVOPTS_DEBUGGING = "${KEY_DEVOPTS}.debugging"
const val KEY_DEVOPTS_DEBUGGING_DUMPLOGS = "${KEY_DEVOPTS_DEBUGGING}.dumpLogs"
const val KEY_DEVOPTS_DEBUGGING_ENABLE_LOGSENDER = "${KEY_DEVOPTS_DEBUGGING}.enableLogsender"
var dumpLogs: Boolean
get() = prefManager.getBoolean(KEY_DEVOPTS_DEBUGGING_DUMPLOGS, false)
set(value) {
prefManager.putBoolean(KEY_DEVOPTS_DEBUGGING_DUMPLOGS, value)
}
var logsenderEnabled: Boolean
get() = prefManager.getBoolean(KEY_DEVOPTS_DEBUGGING_ENABLE_LOGSENDER, true)
set(value) {
prefManager.putBoolean(KEY_DEVOPTS_DEBUGGING_ENABLE_LOGSENDER, value)
}
}
@@ -0,0 +1,177 @@
/*
* 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.preferences.internal
/**
* Preferences for the code editor.
*
* @author Akash Yadav
*/
@Suppress("MemberVisibilityCanBePrivate")
object EditorPreferences {
const val COMPLETIONS_MATCH_LOWER = "idepref_editor_completions_matchLower"
const val FLAG_WS_LEADING = "idepref_editor_wsLeading"
const val FLAG_WS_TRAILING = "idepref_editor_wsTrailing"
const val FLAG_WS_INNER = "idepref_editor_wsInner"
const val FLAG_WS_EMPTY_LINE = "idepref_editor_wsEmptyLine"
const val FLAG_LINE_BREAK = "idepref_editor_lineBreak"
const val FONT_SIZE = "idepref_editor_fontSize"
const val PRINTABLE_CHARS = "idepref_editor_nonPrintableFlags"
const val TAB_SIZE = "idepref_editor_tabSize"
const val AUTO_SAVE = "idepref_editor_autoSave"
const val FONT_LIGATURES = "idepref_editor_fontLigatures"
const val FLAG_PASSWORD = "idepref_editor_flagPassword"
const val WORD_WRAP = "idepref_editor_word_wrap"
const val USE_MAGNIFER = "idepref_editor_use_magnifier"
const val USE_ICU = "idepref_editor_useIcu"
const val USE_SOFT_TAB = "idepref_editor_useSoftTab"
const val USE_CUSTOM_FONT = "idepref_editor_useCustomFont"
const val DELETE_EMPTY_LINES = "idepref_editor_deleteEmptyLines"
const val DELETE_TABS_ON_BACKSPACE = "idepref_editor_deleteTab"
const val STICKY_SCROLL_ENABLED = "idepref_editor_stickyScrollEnabled"
const val PIN_LINE_NUMBERS = "idepref_editor_pinLineNumbers"
const val COLOR_SCHEME = "idepref_editor_colorScheme"
const val DEFAULT_COLOR_SCHEME = "default"
var completionsMatchLower: Boolean
get() = prefManager.getBoolean(COMPLETIONS_MATCH_LOWER, false)
set(value) {
prefManager.putBoolean(COMPLETIONS_MATCH_LOWER, value)
}
var drawLeadingWs: Boolean
get() = prefManager.getBoolean(FLAG_WS_LEADING, false)
set(value) {
prefManager.putBoolean(FLAG_WS_LEADING, value)
}
var drawTrailingWs: Boolean
get() = prefManager.getBoolean(FLAG_WS_TRAILING, false)
set(value) {
prefManager.putBoolean(FLAG_WS_TRAILING, value)
}
var drawInnerWs: Boolean
get() = prefManager.getBoolean(FLAG_WS_INNER, false)
set(value) {
prefManager.putBoolean(FLAG_WS_INNER, value)
}
var drawEmptyLineWs: Boolean
get() = prefManager.getBoolean(FLAG_WS_EMPTY_LINE, false)
set(value) {
prefManager.putBoolean(FLAG_WS_EMPTY_LINE, value)
}
var drawLineBreak: Boolean
get() = prefManager.getBoolean(FLAG_LINE_BREAK, false)
set(value) {
prefManager.putBoolean(FLAG_LINE_BREAK, value)
}
var fontSize: Float
get() = prefManager.getFloat(FONT_SIZE, 14f)
set(value) {
prefManager.putFloat(FONT_SIZE, value)
}
var tabSize: Int
get() = prefManager.getInt(TAB_SIZE, 4)
set(value) {
prefManager.putInt(TAB_SIZE, value)
}
var autoSave: Boolean
get() = prefManager.getBoolean(AUTO_SAVE, false)
set(value) {
prefManager.putBoolean(AUTO_SAVE, value)
}
var fontLigatures: Boolean
get() = prefManager.getBoolean(FONT_LIGATURES, true)
set(value) {
prefManager.putBoolean(FONT_LIGATURES, value)
}
var visiblePasswordFlag: Boolean
get() = prefManager.getBoolean(FLAG_PASSWORD, true)
set(value) {
prefManager.putBoolean(FLAG_PASSWORD, value)
}
var wordwrap: Boolean
get() = prefManager.getBoolean(WORD_WRAP, false)
set(value) {
prefManager.putBoolean(WORD_WRAP, value)
}
var useMagnifier: Boolean
get() = prefManager.getBoolean(USE_MAGNIFER, true)
set(value) {
prefManager.putBoolean(USE_MAGNIFER, value)
}
var useIcu: Boolean
get() = prefManager.getBoolean(USE_ICU, false)
set(value) {
prefManager.putBoolean(USE_ICU, value)
}
var useSoftTab: Boolean
get() = prefManager.getBoolean(USE_SOFT_TAB, true)
set(value) {
prefManager.putBoolean(USE_SOFT_TAB, value)
}
var useCustomFont: Boolean
get() = prefManager.getBoolean(USE_CUSTOM_FONT, false)
set(value) {
prefManager.putBoolean(USE_CUSTOM_FONT, value)
}
var colorScheme: String
get() = prefManager.getString(COLOR_SCHEME, DEFAULT_COLOR_SCHEME)
set(value) {
prefManager.putString(COLOR_SCHEME, value)
}
var deleteEmptyLines: Boolean
get() = prefManager.getBoolean(DELETE_EMPTY_LINES, true)
set(value) {
prefManager.putBoolean(DELETE_EMPTY_LINES, value)
}
var deleteTabsOnBackspace: Boolean
get() = prefManager.getBoolean(DELETE_TABS_ON_BACKSPACE, true)
set(value) {
prefManager.putBoolean(DELETE_TABS_ON_BACKSPACE, value)
}
var stickyScrollEnabled: Boolean
get() = prefManager.getBoolean(STICKY_SCROLL_ENABLED, false)
set(value) {
prefManager.putBoolean(STICKY_SCROLL_ENABLED, value)
}
var pinLineNumbers: Boolean
get() = prefManager.getBoolean(PIN_LINE_NUMBERS, true)
set(value) = prefManager.putBoolean(PIN_LINE_NUMBERS, value)
}
@@ -0,0 +1,98 @@
/*
* 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.preferences.internal
import androidx.appcompat.app.AppCompatDelegate
import com.itsaky.androidide.resources.localization.LocaleProvider
/**
* @author Akash Yadav
*/
@Suppress("MemberVisibilityCanBePrivate")
object GeneralPreferences {
const val IS_FIRST_PROJECT_BUILD = "project_isFirstBuild"
const val UI_MODE = "idepref_general_uiMode"
const val SELECTED_THEME = "idpref_general_theme"
const val SELECTED_LOCALE = "idpref_general_locale"
const val OPEN_PROJECTS = "idepref_general_autoOpenProjects"
const val CONFIRM_PROJECT_OPEN = "idepref_general_confirmProjectOpen"
const val TERMINAL_USE_SYSTEM_SHELL = "idepref_general_terminalShell"
const val LAST_OPENED_PROJECT = "ide_last_project"
const val NO_OPENED_PROJECT = "<NO_OPENED_PROJECT>"
var uiMode: Int
get() = prefManager.getInt(UI_MODE, AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
set(value) {
prefManager.putInt(UI_MODE, value)
}
var selectedTheme: String?
get() = prefManager.getString(SELECTED_THEME, null)
set(value) {
prefManager.putString(SELECTED_THEME, value)
}
var selectedLocale: String?
get() = prefManager.getString(SELECTED_LOCALE, null).let { locale ->
// if the locale is set to a locale key that is not supported,
// fall back to 'System default'
if (LocaleProvider.getLocale(locale) == null) {
null
} else {
locale
}
}
set(value) {
prefManager.putString(SELECTED_LOCALE, value)
}
var isFirstBuild: Boolean
get() = prefManager.getBoolean(IS_FIRST_PROJECT_BUILD, true)
set(value) {
prefManager.putBoolean(IS_FIRST_PROJECT_BUILD, value)
}
var autoOpenProjects: Boolean
get() = prefManager.getBoolean(OPEN_PROJECTS, true)
set(value) {
prefManager.putBoolean(OPEN_PROJECTS, value)
}
var confirmProjectOpen: Boolean
get() = prefManager.getBoolean(CONFIRM_PROJECT_OPEN, false)
set(value) {
prefManager.putBoolean(CONFIRM_PROJECT_OPEN, value)
}
var useSystemShell: Boolean
get() = prefManager.getBoolean(TERMINAL_USE_SYSTEM_SHELL, false)
set(value) {
prefManager.putBoolean(TERMINAL_USE_SYSTEM_SHELL, value)
}
var lastOpenedProject: String
get() = prefManager.getString(LAST_OPENED_PROJECT, NO_OPENED_PROJECT)
set(value) {
prefManager.putString(LAST_OPENED_PROJECT, value)
}
}
@@ -0,0 +1,42 @@
/*
* 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.preferences.internal
/**
* @author Akash Yadav
*/
@Suppress("MemberVisibilityCanBePrivate")
object JavaPreferences {
const val GOOGLE_CODE_STYLE = "idepref_editor_java_googleCodeStyle"
const val JAVA_DIAGNOSTICS_ENABLED = "idepref_editor_java_diagnosticsEnabled"
var googleCodeStyle: Boolean
get() = prefManager.getBoolean(GOOGLE_CODE_STYLE, false)
set(value) {
prefManager.putBoolean(GOOGLE_CODE_STYLE, value)
}
/** Whether diagnostics are enabled for the Java source files. */
var isJavaDiagnosticsEnabled: Boolean
get() = prefManager.getBoolean(JAVA_DIAGNOSTICS_ENABLED, true)
set(value) {
prefManager.putBoolean(JAVA_DIAGNOSTICS_ENABLED, value)
}
}
@@ -0,0 +1,47 @@
/*
* 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.preferences.internal
/**
* @author Akash Yadav
*/
@Suppress("MemberVisibilityCanBePrivate")
object StatPreferences {
const val STAT_COLLECTION_CONSENT_SHOWN = "ide.stats.consentShown"
const val STAT_OPT_IN = "ide.stats.optIn"
const val STAT_LAST_REPORTED = "ide.stats.lastReported"
var statConsentDialogShown: Boolean
get() = prefManager.getBoolean(STAT_COLLECTION_CONSENT_SHOWN, false)
set(value) {
prefManager.putBoolean(STAT_COLLECTION_CONSENT_SHOWN, value)
}
var statOptIn: Boolean
get() = prefManager.getBoolean(STAT_OPT_IN, true)
set(value) {
prefManager.putBoolean(STAT_OPT_IN, value)
}
var statLastReported: Long
get() = prefManager.getLong(STAT_LAST_REPORTED, 0L)
set(value) {
prefManager.putLong(STAT_LAST_REPORTED, value)
}
}
@@ -0,0 +1,131 @@
/*
* 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.preferences.internal
/**
* @author Akash Yadav
*/
@Suppress("MemberVisibilityCanBePrivate")
object XmlPreferences {
const val TRIM_FINAL_NEW_LINE = "idepref_xml_trimFinalNewLine"
const val INSERT_FINAL_NEW_LINE = "idepref_xml_insertFinalNewLine"
const val SPLIT_ATTRIBUTES = "idepref_xml_splitAttributes"
const val JOIN_CDATA_LINES = "idepref_xml_joinCDataLines"
const val JOIN_COMMENT_LINES = "idepref_xml_joinCommentLines"
const val JOIN_CONTENT_LINES = "idepref_xml_joinContentLines"
const val SPACE_BEFORE_EMPTY_CLOSE_TAG = "idepref_xml_spaceBeforeEmptyCloseTag"
const val PRESERVE_EMPTY_CONTENT = "idepref_xml_preserveEmptyContent"
const val PRESERVE_ATTRIBUTE_LINE_BREAKS = "idepref_xml_preserveAttributeLineBreaks"
const val CLOSING_BRACKET_NEW_LINE = "idepref_xml_closingBracketNewLine"
const val TRIM_TRAILING_WHITESPACE = "idepref_xml_trimTrailingWhitespace"
const val MAX_LINE_WIDTH = "idepref_xml_maxLineWidth"
const val PRESERVED_NEW_LINES = "idepref_xml_preservedNewLines"
const val SPLIT_ATTRIBUTES_INDENT_SIZE = "idepref_xml_splitAttributesIndentSize"
const val EMPTY_ELEMENTS_BEHAVIOR = "idepref_xml_emptyElementsBehavior"
var trimFinalNewLine: Boolean
get() = prefManager.getBoolean(TRIM_FINAL_NEW_LINE, false)
set(value) {
prefManager.putBoolean(TRIM_FINAL_NEW_LINE, value)
}
var insertFinalNewLine: Boolean
get() = prefManager.getBoolean(INSERT_FINAL_NEW_LINE, true)
set(value) {
prefManager.putBoolean(INSERT_FINAL_NEW_LINE, value)
}
var splitAttributes: Boolean
get() = prefManager.getBoolean(SPLIT_ATTRIBUTES, true)
set(value) {
prefManager.putBoolean(SPLIT_ATTRIBUTES, value)
}
var joinCDataLines: Boolean
get() = prefManager.getBoolean(JOIN_CDATA_LINES, false)
set(value) {
prefManager.putBoolean(JOIN_CDATA_LINES, value)
}
var joinCommentLines: Boolean
get() = prefManager.getBoolean(JOIN_COMMENT_LINES, true)
set(value) {
prefManager.putBoolean(JOIN_COMMENT_LINES, value)
}
var joinContentLines: Boolean
get() = prefManager.getBoolean(JOIN_CONTENT_LINES, false)
set(value) {
prefManager.putBoolean(JOIN_CONTENT_LINES, value)
}
var spaceBeforeEmptyCloseTag: Boolean
get() = prefManager.getBoolean(SPACE_BEFORE_EMPTY_CLOSE_TAG, true)
set(value) {
prefManager.putBoolean(SPACE_BEFORE_EMPTY_CLOSE_TAG, value)
}
var preserveEmptyContent: Boolean
get() = prefManager.getBoolean(PRESERVE_EMPTY_CONTENT, true)
set(value) {
prefManager.putBoolean(PRESERVE_EMPTY_CONTENT, value)
}
var preserveAttributeLineBreaks: Boolean
get() = prefManager.getBoolean(PRESERVE_ATTRIBUTE_LINE_BREAKS, true)
set(value) {
prefManager.putBoolean(PRESERVE_ATTRIBUTE_LINE_BREAKS, value)
}
var closingBracketNewLine: Boolean
get() = prefManager.getBoolean(CLOSING_BRACKET_NEW_LINE, false)
set(value) {
prefManager.putBoolean(CLOSING_BRACKET_NEW_LINE, value)
}
var trimTrailingWhitespace: Boolean
get() = prefManager.getBoolean(TRIM_TRAILING_WHITESPACE, true)
set(value) {
prefManager.putBoolean(TRIM_TRAILING_WHITESPACE, value)
}
var maxLineWidth: Int
get() = prefManager.getInt(MAX_LINE_WIDTH, 80)
set(value) {
prefManager.putInt(MAX_LINE_WIDTH, value)
}
var preservedNewLines: Int
get() = prefManager.getInt(PRESERVED_NEW_LINES, 2)
set(value) {
prefManager.putInt(PRESERVED_NEW_LINES, value)
}
var splitAttributesIndentSize: Int
get() = prefManager.getInt(SPLIT_ATTRIBUTES_INDENT_SIZE, EditorPreferences.tabSize / 2)
set(value) {
prefManager.putInt(SPLIT_ATTRIBUTES_INDENT_SIZE, value)
}
var emptyElementsBehavior: String
get() = prefManager.getString(EMPTY_ELEMENTS_BEHAVIOR, "Collapse")
set(value) {
prefManager.putString(EMPTY_ELEMENTS_BEHAVIOR, value)
}
}
@@ -0,0 +1,27 @@
/*
* 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.preferences.internal
import com.itsaky.androidide.app.BaseApplication
import com.itsaky.androidide.managers.PreferenceManager
/**
* The global preference manager instance.
*/
val prefManager: PreferenceManager
get() = BaseApplication.getBaseInstance().prefManager
@@ -0,0 +1,46 @@
/*
* 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.preferences.utils
import com.itsaky.androidide.preferences.internal.EditorPreferences
/** @author Akash Yadav */
/**
* The indentation character to use. If [useSoftTab] is enabled, the character is a space, otherwise
* '\t'.
*/
val indentationChar: Char
get() = if (EditorPreferences.useSoftTab) ' ' else '\t'
/** Get the string which should be used as indentation while generating code. */
val indentationString: String
get() = if (EditorPreferences.useSoftTab) " ".repeat(EditorPreferences.tabSize) else "\t"
/**
* Creates the indentation string for the given number of spaces. The result is simply
* [indentationChar] repeated [spaceCount] times if [useSoftTab] is enabled, otherwise `spaceCount /
* tabSize` times.
*
* @param spaceCount The number of spaces to indent.
* @return The indentation string.
*/
fun indentationString(spaceCount: Int): String {
val count = if (EditorPreferences.useSoftTab) spaceCount else spaceCount / EditorPreferences.tabSize
return indentationChar.toString().repeat(count)
}
@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ 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/>.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/name"
style="@style/Widget.Material3.TextInputEditText.OutlinedBox.Dense"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
tools:hint="Type something">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:singleLine="true" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
+1
View File
@@ -0,0 +1 @@
/build
+39
View File
@@ -0,0 +1,39 @@
/*
* 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/>.
*/
@Suppress("JavaPluginLanguageLevel")
plugins {
id("java-library")
id("org.jetbrains.kotlin.jvm")
}
groupConfig {
groupIdSuffix.set("utils")
}
dependencies {
implementation(libs.androidx.annotation)
implementation(libs.androidx.collection)
implementation(libs.google.guava)
implementation(libs.google.gson)
implementation(projects.logging.logger)
implementation(projects.utilities.buildInfo)
testImplementation(libs.tests.google.truth)
testImplementation(libs.tests.junit)
}
View File
+21
View File
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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/>.
-->
<manifest/>
@@ -0,0 +1,235 @@
/*
* 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.models
import com.google.gson.annotations.SerializedName
import java.nio.file.Path
data class Location(var file: Path, var range: Range)
data class Position @JvmOverloads constructor(
@SerializedName("line") var line: Int,
@SerializedName("column") var column: Int,
@SerializedName("index") var index: Int = -1
) : Comparable<Position> {
fun requireIndex(): Int {
if (index == -1) {
throw IllegalArgumentException("No index provided")
}
return index
}
/** Makes the indices 0 if they are negative. */
fun zeroIfNegative() {
if (line < 0) {
line = 0
}
if (column < 0) {
column = 0
}
}
companion object {
@JvmField
val NONE = Position(-1, -1)
}
override fun compareTo(other: Position): Int {
val byLine =
when {
line < other.line -> -1
line > other.line -> 1
else -> 0
}
if (byLine != 0) {
return byLine
}
return when {
column < other.column -> -1
column > other.column -> 1
else -> 0
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Position) return false
if (line != other.line) return false
if (column != other.column) return false
return true
}
override fun hashCode(): Int {
var result = line
result = 31 * result + column
return result
}
}
open class Range
@JvmOverloads
constructor(
@SerializedName("start") var start: Position = Position(0, 0),
@SerializedName("end") var end: Position = Position(0, 0)
) : Comparable<Range> {
constructor(src: Range) : this(Position(src.start.line, src.start.column),
Position(src.end.line, src.end.column))
companion object {
@JvmField
val NONE = Range(Position.NONE, Position.NONE)
@JvmStatic
fun pointRange(line: Int, column: Int): Range {
return pointRange(Position(line, column))
}
@JvmStatic
fun pointRange(position: Position): Range {
return Range(position, position)
}
}
/**
* Validate the start and end positions.
* @see Position.zeroIfNegative()
*/
fun validate() {
start.zeroIfNegative()
end.zeroIfNegative()
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Range) return false
if (start != other.start) return false
if (end != other.end) return false
return true
}
override fun hashCode(): Int {
var result = start.hashCode()
result = 31 * result + end.hashCode()
return result
}
override fun compareTo(other: Range): Int = start.compareTo(other.start)
fun compareByEnd(other: Range): Int = end.compareTo(other.end)
fun contains(position: Position): Boolean {
if (position.line < start.line || position.line > end.line) {
return false
}
if (start.line == end.line) {
return position.column >= start.column && position.column <= end.column
}
return false
}
/**
* Check if this range contains the given position or not. Return an integer result indicating
* where the index of the range containing this position might be.
*
* The return value will be used in a binary search.
*/
fun containsForBinarySearch(position: Position): Int {
// The position might appear before this range
if (position.line < start.line) {
return -1
}
// The position might appear after this range
if (position.line > end.line) {
return 1
}
// If start and end lines are same, compare by column indexes
if (start.line == end.line) {
if (position.column < start.column) {
return -1
}
if (position.column > end.column) {
return 1
}
}
// This range definitely contains the position.
return 0
}
fun containsLine(line: Int): Boolean {
return start.line <= line && end.line >= line
}
fun containsColumn(column: Int): Boolean {
return start.column <= column && end.column >= column
}
fun containsRange(other: Range): Boolean {
if (!containsLine(other.start.line) || !containsLine(other.end.line)) {
return false
}
return containsColumn(other.start.column) && containsColumn(other.end.column)
}
fun isSmallerThan(other: Range): Boolean {
return other.isBiggerThan(this)
}
fun isBiggerThan(other: Range): Boolean {
if (equals(other)) {
return false
}
if (start.line < other.start.line && end.line > other.end.line) {
return true
}
if (start.line == other.start.line && end.line == other.end.line) {
if (start.column <= other.start.column && end.column >= other.end.column) {
return true
}
}
return false
}
override fun toString(): String {
return "Range(start=$start, end=$end)"
}
}
@@ -0,0 +1,150 @@
/*
* 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.models
import com.itsaky.androidide.utils.DefaultRecyclable
import com.itsaky.androidide.utils.ILogger
import com.itsaky.androidide.utils.LogTagUtils
import com.itsaky.androidide.utils.newRecyclableObjectPool
class LogLine private constructor() : DefaultRecyclable() {
var unformatted: String? = null
var date: String? = null
var time: String? = null
var pid: String? = null
var tid: String? = null
var tag: String? = null
var message: String? = null
var level: ILogger.Level? = null
var formatted = false
// For JSONRpc and Recyclable
init {
resetToDefault()
}
fun toSimpleString(): String {
return if (formatted) String.format(
"%-25s %-2s %s", LogTagUtils.trimTagIfNeeded(tag, 25), level?.levelChar ?: 'U',
message) else unformatted!!
}
fun formattedTagAndMessage(): String {
return if (formatted) String.format("%-25s %-2s", LogTagUtils.trimTagIfNeeded(tag, 25),
message) else unformatted!!
}
private fun resetToDefault() {
message = null
tag = null
tid = null
pid = null
time = null
date = null
unformatted = null
level = ILogger.Level.DEBUG
formatted = false
}
override fun recycle() {
resetToDefault()
logLinePool.recycle(this)
}
override fun toString(): String {
return if (formatted) String.format(
"%s %s %s %s %-2s %-25s %s",
date, time, pid, tid, level?.levelChar ?: 'U', LogTagUtils.trimTagIfNeeded(tag, 25),
message) else unformatted!!
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is LogLine) return false
if (unformatted != other.unformatted) return false
if (date != other.date) return false
if (time != other.time) return false
if (pid != other.pid) return false
if (tid != other.tid) return false
if (tag != other.tag) return false
if (message != other.message) return false
if (level != other.level) return false
if (formatted != other.formatted) return false
return true
}
override fun hashCode(): Int {
var result = unformatted?.hashCode() ?: 0
result = 31 * result + (date?.hashCode() ?: 0)
result = 31 * result + (time?.hashCode() ?: 0)
result = 31 * result + (pid?.hashCode() ?: 0)
result = 31 * result + (tid?.hashCode() ?: 0)
result = 31 * result + (tag?.hashCode() ?: 0)
result = 31 * result + (message?.hashCode() ?: 0)
result = 31 * result + (level?.hashCode() ?: 0)
result = 31 * result + formatted.hashCode()
return result
}
companion object {
// do not cache too many LogLine items
// LogLines should be recycled as soon as they are appended to the log view
private val logLinePool = newRecyclableObjectPool(
capacity = 16,
factory = ::LogLine
)
@JvmOverloads
@JvmStatic
fun obtain(level: ILogger.Level?, tag: String?, message: String?,
formatted: Boolean = true): LogLine {
val logLine = logLinePool.obtain()
logLine.level = level
logLine.tag = tag
logLine.message = message
logLine.formatted = formatted
return logLine
}
@JvmStatic
fun forLogString(log: String?): LogLine? {
if (log == null) {
return null
}
val logLine = logLinePool.obtain()
try {
val split = log.split("\\s".toRegex(), limit = 7).toTypedArray()
logLine.level = ILogger.Level.forChar(split[4][0])
logLine.date = split[0]
logLine.time = split[1] // time
logLine.pid = split[2] // process id
logLine.tid = split[3] // thread id
logLine.tag = split[5] // tag
logLine.message = split[6] // message
logLine.formatted = true
} catch (th: Throwable) { // do not log the exception with ILogger
logLine.unformatted = log
logLine.formatted = false
}
return logLine
}
}
}
@@ -0,0 +1,81 @@
/*
* 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.progress
import java.util.concurrent.CancellationException
import java.util.concurrent.atomic.AtomicBoolean
/**
* Check whether a process is cancelled.
*
* @author Akash Yadav
*/
interface ICancelChecker {
/**
* Cancel this process.
*/
fun cancel()
/**
* Check whether this process has been cancelled or not.
*
* @return Whether the process has been cancelled.
*/
fun isCancelled(): Boolean
/**
* Throw [CancellationException] if this process has been cancelled.
*/
@Throws(CancellationException::class)
fun abortIfCancelled()
open class Default(cancelled: Boolean = false) : ICancelChecker {
private val cancelled = AtomicBoolean(cancelled)
override fun cancel() {
cancelled.set(true)
}
override fun isCancelled(): Boolean {
return cancelled.get()
}
override fun abortIfCancelled() {
if (isCancelled()) {
throw CancellationException()
}
}
}
companion object {
/**
* A no-op cancel checker. The task is never cancelled.
*/
@JvmField
val NOOP = Default(false)
/**
* An already cancelled cancel checker.
*/
@JvmField
val CANCELLED = Default(true)
}
}
@@ -0,0 +1,61 @@
/*
* 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.progress
import com.itsaky.androidide.progress.ICancelChecker.Default
import java.util.WeakHashMap
import java.util.concurrent.CancellationException
/**
* @author Akash Yadav
*/
class ProgressManager private constructor() {
private val threads = WeakHashMap<Thread, ICancelChecker>()
companion object {
val instance by lazy {
ProgressManager()
}
@JvmStatic
fun abortIfCancelled() {
instance.abortIfCancelled()
}
}
fun cancel(thread: Thread) {
var checker = threads[thread]
if (checker == null) {
checker = Default()
}
checker.cancel()
threads[thread] = checker
}
@JvmName("internalAbortIfCancelled")
private fun abortIfCancelled() {
val thisThread = Thread.currentThread()
val checker = threads[thisThread]
if (checker != null && checker.isCancelled()) {
threads.remove(thisThread)
throw CancellationException()
}
}
}
@@ -0,0 +1,285 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* 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 com.itsaky.androidide.buildinfo.BuildInfo
import java.util.Objects
/**
* Represents a version of the Android Gradle Plugin
*
* Designed for plugin authors to compare the currently running plugin version, so implements
* comparable.
*
* For example `AndroidPluginVersion(7, 0)` refers to Android Gradle Plugin version 7.0.0,
* and `AndroidPluginVersion(7, 0, 1)` refers to Android Gradle Plugin version 7.0.1.
*
* The internal representation is normalized, so that `AndroidPluginVersion(7, 0)` equals
* `AndroidPluginVersion(7, 0, 0)`
*
* Also supports preview versions through methods [alpha], [beta] and [rc] that return the more
* specific version. For example `AndroidPluginVersion(7, 0).alpha(5)` refers to Android Gradle
* Plugin version 7.0.0-alpha05. This is for use when developing using incubating APIs that have
* changed between preview releases of the Android Gradle Plugin. Once those APIs are stable in a
* stable version of Android Gradle Plugin, it's recommended to drop support for the preview versions.
* For example, if a new API was introduced in 7.0.0-alpha05, you can test for that using
*
* ```if (androidComponents.pluginVersion >= AndroidPluginVersion(7.0).alpha(5)) { ... }```
*
* If that API is marked as stable in 7.0.0, drop support for the preview versions before it by
* updating your condition to:
*
* ```if (androidComponents.pluginVersion >= AndroidPluginVersion(7.0)) { ... }```
*/
class AndroidPluginVersion private constructor(
/**
* The major version.
*
* e.g. 7 for Android Gradle Plugin Version 7.0.1
*/
val major: Int,
/**
* The minor version.
*
* e.g. 0 for Android Gradle Plugin Version 7.0.1
*/
val minor: Int,
/**
* The micro, or patch version.
*
* e.g. 1 for Android Gradle Plugin Version 7.0.1
*/
val micro: Int,
private val _previewType: PreviewType,
/**
* The preview version.
*
* e.g. 5 for Android Gradle Plugin Version 7.0.0-alpha05
*/
val preview: Int,
) : Comparable<AndroidPluginVersion> {
init {
require(
major >= 0 && minor >= 0 && micro >= 0) { "Versions of the Android Gradle Plugin must not be negative" }
}
/**
* The type of preview version.
*
* Null in the case of a stable version. One of 'alpha', 'beta', 'rc', 'dev' for preview versions.
*
* e.g. 'alpha' for Android Gradle Plugin Version 7.0.0-alpha05
*/
val previewType: String? get() = _previewType.publicName
/**
* Create an AndroidPluginVersion with the given major and minor version.
*
* For example `AndroidPluginVersion(7, 0)` refers to Android Gradle Plugin version 7.0.0.
*/
constructor(major: Int, minor: Int) : this(major, minor, 0, PreviewType.FINAL, 0)
/**
* Create an AndroidPluginVersion with the given major, minor and micro version.
*
* For example `AndroidPluginVersion(7, 0, 1)` refers to Android Gradle Plugin version 7.0.1.
*/
constructor(major: Int, minor: Int, micro: Int) : this(major, minor, micro, PreviewType.FINAL, 0)
private enum class PreviewType(val publicName: String?) {
ALPHA("alpha"),
BETA("beta"),
RC("rc"),
DEV("dev"),
FINAL(null);
companion object {
@JvmStatic
fun find(publicName: String?): PreviewType {
if (publicName.isNullOrBlank()) {
return FINAL
}
return when (publicName) {
ALPHA.publicName -> ALPHA
BETA.publicName -> BETA
RC.publicName -> RC
DEV.publicName -> DEV
else -> throw IllegalArgumentException("Unknown preview type '$publicName'")
}
}
}
}
/**
* From a stable [AndroidPluginVersion] returns an alpha version.
*
* For example `AndroidPluginVersion(7, 0).alpha(5)` refers to Android Gradle Plugin version 7.0.0-alpha05.
*/
fun alpha(alpha: Int): AndroidPluginVersion {
require(
_previewType == PreviewType.FINAL) { "alpha(int) only expected to be called on final versions" }
require(alpha >= 1) { "Alpha version must be at least 1" }
return AndroidPluginVersion(major, minor, micro, PreviewType.ALPHA, preview = alpha)
}
/**
* From a stable [AndroidPluginVersion] returns a beta version.
*
* For example `AndroidPluginVersion(7, 0).beta(2)` refers to Android Gradle Plugin version 7.0.0-beta02.
*/
fun beta(beta: Int): AndroidPluginVersion {
require(
_previewType == PreviewType.FINAL) { "beta(int) only expected to be called on final versions" }
require(beta >= 1) { "Beta version must be at least 1" }
return AndroidPluginVersion(major, minor, micro, PreviewType.BETA, preview = beta)
}
/**
* From a stable [AndroidPluginVersion] returns a release candidate version.
*
* For example `AndroidPluginVersion(7, 0).rc(1)` refers to Android Gradle Plugin version 7.0.0-rc01.
*/
fun rc(rc: Int): AndroidPluginVersion {
require(
_previewType == PreviewType.FINAL) { "rc(int) only expected to be called on final versions" }
require(rc >= 1) { "Release candidate version must be at least 1" }
return AndroidPluginVersion(major, minor, micro, PreviewType.RC, preview = rc)
}
/**
* From a stable [AndroidPluginVersion] specify an internal development version.
*
* `-dev` versions are never publicly released, but this can be useful if you are
* building the Android Gradle Plugin from source.
*
* For example `AndroidPluginVersion(7, 0).dev()` refers to Android Gradle Plugin version 7.0.0-dev.
*/
fun dev(): AndroidPluginVersion {
return AndroidPluginVersion(major, minor, micro, PreviewType.DEV, preview = 0)
}
/**
* Creates a new [AndroidPluginVersion] from this version with the given values updated.
*/
fun update(
major: Int = this.major,
minor: Int = this.minor,
micro: Int = this.micro,
previewType: String? = this.previewType,
preview: Int = this.preview
): AndroidPluginVersion {
return AndroidPluginVersion(major, minor, micro, PreviewType.find(previewType), preview)
}
override fun compareTo(other: AndroidPluginVersion): Int {
return comparator.compare(this, other)
}
override fun equals(other: Any?): Boolean {
return other is AndroidPluginVersion &&
major == other.major &&
minor == other.minor &&
micro == other.micro &&
_previewType == other._previewType &&
preview == other.preview
}
override fun hashCode(): Int {
return Objects.hash(major, minor, micro, _previewType, preview)
}
override fun toString(): String =
"Android Gradle Plugin version ${toStringSimple()}"
fun toStringSimple(): String = "$major.$minor.$micro" +
(if (previewType != null) "-$previewType" else "") +
(if (preview > 0) if(preview < 10) "0$preview" else preview else "")
companion object {
private val comparator: Comparator<AndroidPluginVersion> =
Comparator.comparingInt<AndroidPluginVersion> { it.major }
.thenComparingInt { it.minor }
.thenComparingInt { it.micro }
.thenComparingInt { it._previewType.ordinal }
.thenComparingInt { it.preview }
/**
* The minimum version of Android Gradle Plugin that is supported by AndroidIDE>
*/
@JvmField
val MINIMUM_SUPPORTED = parse(BuildInfo.AGP_VERSION_MININUM)
/**
* The version of Android Gradle Plugin that has been known to work with AndroidIDE>
*/
@JvmField
val LATEST_TESTED = parse(BuildInfo.AGP_VERSION_LATEST)
/**
* Parses the given AGP version and returns the [AndroidPluginVersion] representing the same.
*/
@JvmStatic
fun parse(version: String): AndroidPluginVersion {
require(version.isNotBlank()) {
"Invalid input '$version'"
}
val regex = """^(\d+)(?:\.(\d+)(?:\.(\d+)(?:-([a-z]+)(\d+)?)?)?)?${'$'}""".toRegex()
val match = checkNotNull(regex.find(version)) {
"Failed to match input '$version'"
}
return match.groupValues.run {
val int = fun(index: Int, default: Int): Int =
if (lastIndex >= index) this[index].let { if (it.isBlank()) 0 else it.toInt() } else default
val str = fun(index: Int, default: String) =
if (lastIndex >= index) this[index] else default
val major = this[1].toInt()
val minor = int(2, 0)
val micro = int(3, 0)
val previewType = str(4, "")
val preview = int(5, 0)
return@run AndroidPluginVersion(major, minor, micro, PreviewType.find(previewType), preview)
}
}
}
}
@@ -0,0 +1,143 @@
/*
* 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.lang.reflect.Method;
import java.nio.file.Path;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Cache maps a file + an arbitrary key to a value. When the file is modified, the mapping expires.
*/
public class Cache<K, V> {
private static final Logger LOG = LoggerFactory.getLogger(Cache.class);
// Cannot access FileManager from this module
private static Object FileManager_INSTANCE;
private static Method FileManager_getLastModified;
private final Map<Key<K>, Value> map = new HashMap<>();
public boolean has(Path file, K k) {
return !needs(file, k);
}
public boolean needs(Path file, K k) {
// If key is not in map, it needs to be loaded
Key<K> key = new Key<>(file, k);
if (!map.containsKey(key)) {
return true;
}
// If key was loaded before file was last modified, it needs to be reloaded
Value value = map.get(key);
if (value == null) {
return true;
}
Instant modified = getLastModified(file);
// TODO remove all keys associated with file when file changes
return value.created.isBefore(modified);
}
private static Instant getLastModified(Path file) {
initProjectManagerInstance();
try {
return (Instant) FileManager_getLastModified.invoke(FileManager_INSTANCE, file);
} catch (Throwable err) {
LOG.error("Cannot get last modified from ProjectManager", err);
return Instant.now();
}
}
private static void initProjectManagerInstance() {
if (FileManager_INSTANCE == null) {
try {
final var klass = Class.forName("com.itsaky.androidide.projects.FileManager");
final var field = klass.getDeclaredField("INSTANCE");
if (!field.isAccessible()) {
field.setAccessible(true);
}
FileManager_INSTANCE = field.get(null);
FileManager_getLastModified = klass.getDeclaredMethod("getLastModified", Path.class);
if (!FileManager_getLastModified.isAccessible()) {
FileManager_getLastModified.setAccessible(true);
}
} catch (Throwable err) {
LOG.error("Cannot reflect ProjectManager INSTANCE", err);
throw new RuntimeException(err);
}
}
}
public void load(Path file, K k, V v) {
// TODO limit total size of cache
Key<K> key = new Key<>(file, k);
Value value = new Value(v);
map.put(key, value);
}
public V get(Path file, K k) {
final Key<K> key = new Key<>(file, k);
final var val = map.get(key);
if (val == null) {
throw new IllegalArgumentException(k + " is not in map " + map);
}
return val.value;
}
private static class Key<K> {
final Path file;
final K key;
Key(Path file, K key) {
this.file = file;
this.key = key;
}
@Override
public int hashCode() {
return Objects.hash(file, key);
}
@SuppressWarnings("rawtypes")
@Override
public boolean equals(Object other) {
if (other.getClass() != Cache.Key.class) return false;
Key that = (Cache.Key) other;
return Objects.equals(this.key, that.key) && Objects.equals(this.file, that.file);
}
}
private class Value {
final V value;
final Instant created = Instant.now();
Value(V value) {
this.value = value;
}
}
}
@@ -0,0 +1,245 @@
/*
* 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.util.concurrent.ConcurrentHashMap
import java.util.function.Predicate
/**
* Maps classes with their package name segments.
*
* @author Akash Yadav
*/
open class ClassTrie(val root: Node = Node()) {
/**
* Appends the class name entry to this trie.
*
* @param name The fully qualified class name.
* @return The added node.
*/
open fun append(name: String): Node {
val segments = segments(name)
var node = root
for ((index, segment) in segments.withIndex()) {
if (segment.isEmpty() || !segment[0].isJavaIdentifierStart()) {
continue
}
node = createNode(node, segment, segments, index)
}
node.isClass = true
return node
}
/**
* Removes the class name node with the given fully qualified name.
*
* @param name The fully qualified name of the package segment or class name to remove.
*/
open fun remove(name: String) {
remove(name, null)
}
/**
* Removes the nodes (package segments and class names) with the given fully qualified name. A
* [Predicate] can be provided to check which nodes should be deleted.
*
* @param name The fully qualified name of the package segment or the class.
* @param predicate The predicate which can be used to test which entries should be deleted.
*/
open fun remove(name: String, predicate: Predicate<Node>?) {
val segments = segments(name)
val condition = predicate ?: Predicate { it.isClass }
var node: Node? = root
for (segment in segments) {
if (node == null || node.children.isEmpty()) {
break
}
val next = node.children[segment]
if (next != null && condition.test(next)) {
node.children.remove(next.name)
}
node = next
}
}
/** Removes all entries from this trie. */
fun clear() {
this.root.children.clear()
}
/**
* Checks if there is an entry with the given fully qualified name (package or class name).
*
* @param name The fully qualified package or class name.
* @return Whether there is a an entry for the given fully qualified name.
*/
open fun contains(name: String): Boolean {
val segments = segments(name)
var node: Node? = root
for (segment in segments) {
if (node == null) {
return false
}
node = node.children[segment]
}
return node != null
}
/**
* Finds all class nodes in the given package.
*
* @param packageName The package name to list class nodes from.
*/
open fun findInPackage(packageName: String): List<Node> {
val segments = segments(packageName)
val classes = mutableListOf<Node>()
var node: Node? = root
for (segement in segments) {
if (node == null || node.children.isEmpty() || !node.children.containsKey(segement)) {
break
}
if (node.isClass) {
classes.add(node)
}
node = node.children[segement]
}
if (node == null || node.children.isEmpty()) {
return classes
}
node.children.values.forEach { addRecursively(it, classes) }
return classes
}
/**
* Finds all class names in the given package.
*
* @param packageName The package name to list classes from.
*/
open fun findClassNames(packageName: String): List<String> {
return findInPackage(packageName).map { it.qualifiedName }
}
/**
* Finds node with the given qualified name. Or `null` if none was found.
*
* @param qualifiedName The fully qualified name of the node to find.
*/
open fun findNode(qualifiedName: String): Node? {
val segments = segments(qualifiedName)
var node: Node? = root
for (segment in segments) {
if (node == null) {
break
}
node = node.children[segment]
}
return node
}
/** Returns all class nodes available in this trie. */
fun allClassNodes(): Set<Node> {
return root.allClassNodes()
}
/** Returns all classes available in this trie. */
fun allClassNames(): Set<String> {
return root.allClassNames()
}
fun print() {
root.children.forEach { print(it.value, 0) }
}
fun print(node: Node?, indent: Int) {
if (node == null) {
return
}
val prefix = if (indent == 0) "" else "| ".repeat(indent / 2)
println(prefix + node.name)
if (node.children.isNotEmpty()) {
node.children.forEach { print(it.value, indent + 2) }
}
}
open fun segments(name: String): List<String> {
return if (name.contains('.')) {
name.split(".")
} else {
listOf(name)
}
}
protected open fun createNode(node: Node, segment: String, segments: List<String>, index: Int) =
node.createChild(segment, segments.subList(0, index + 1).joinToString(separator = "."))
private fun addRecursively(node: Node, classes: MutableList<Node>) {
if (node.isClass) {
classes.add(node)
}
if (node.children.isEmpty()) {
return
}
node.children.values.forEach { addRecursively(it, classes) }
}
/** A Node can be a package segment or a class name in the package trie. */
open class Node(val name: String, val qualifiedName: String, val parent: Node? = null) {
internal constructor() : this("", "")
val children: MutableMap<String, Node> = ConcurrentHashMap()
var isClass = false
open fun createChild(name: String, qualifiedName: String): Node {
return children.computeIfAbsent(name) { Node(it, qualifiedName, this) }
}
open fun removeChild(child: Node) {
this.children.remove(child.name)
}
open fun allClassNames(): Set<String> {
return this.allClassNodes().map { it.qualifiedName }.toSet()
}
open fun allClassNodes(): Set<Node> {
val all = mutableSetOf<Node>()
if (this.isClass) {
all.add(this)
}
if (children.isNotEmpty()) {
children.values.forEach { all.addAll(it.allClassNodes()) }
}
return all
}
}
}
@@ -0,0 +1,39 @@
/*
* 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.util.concurrent.atomic.AtomicBoolean
/**
* @author Akash Yadav
*/
open class DefaultRecyclable : RecyclableObjectPool.Recyclable {
private val _isRecycled = AtomicBoolean(false)
@set:Synchronized
@get:Synchronized
override var isRecycled: Boolean
get() = _isRecycled.get()
set(value) {
_isRecycled.set(value)
}
override fun recycle() {
}
}
@@ -0,0 +1,52 @@
/*
* 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.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import kotlin.io.path.PathsKt;
/**
* @author Akash Yadav
*/
public class DocumentUtils {
public static boolean isJavaFile(Path file) {
final var name = PathsKt.getName(file);
final var extension = PathsKt.getExtension(file);
return extension.equals("java")
&& Files.exists(file)
&& !Files.isDirectory(file)
&& !name.equals("module-info.java")
&& !name.equals("package-info.java");
}
public static boolean isXmlFile(Path file) {
return PathsKt.getExtension(file).equals("xml") && Files.exists(file) && !Files.isDirectory(file);
}
public static boolean isSameFile(Path first, Path second) {
try {
return Files.isSameFile(first, second);
} catch (IOException e) {
return false;
}
}
}
@@ -0,0 +1,112 @@
/*
* 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.nio.file.Path
import java.nio.file.Paths
import kotlin.io.path.ExperimentalPathApi
import kotlin.io.path.absolute
import kotlin.io.path.name
import kotlin.io.path.readText
import kotlin.io.path.walk
const val PROJECT_ROOT_FILE = ".androidide_root"
/**
* Provides paths to various directories and files.
*
* @author Akash Yadav
*/
class FileProvider {
companion object {
private val projectRoot by lazy {
currentDir().findProjectRoot()?.absolute()?.normalize()
}
@JvmField
val extension = run {
val file = File(".").canonicalFile
when (file.name) {
"xml" -> "xml" // Testing in ':lsp:xml' module
else -> "java" // Testing in ':lsp:java' or ':lsp:testing' module
}
}
@JvmStatic
fun currentDir(): Path = Paths.get(System.getProperty("user.dir")!!)
@JvmStatic
fun implModule(): Path = projectRoot().resolve("tooling/impl")
@JvmStatic
fun projectRoot(): Path =
checkNotNull(projectRoot) {
"Unable to file project root. Check if '${PROJECT_ROOT_FILE}' file exists in the project root directory."
}
@JvmStatic
private fun testingDir() = projectRoot().resolve("testing")
@JvmStatic
private fun testResourcesDir(): Path = testingDir().resolve("resources")
@JvmStatic
fun testHomeDir(): Path = testResourcesDir().resolve("test-home")
@JvmStatic
fun testProjectRoot(): Path = testResourcesDir().resolve("test-project")
@JvmStatic
fun sampleProjectRoot(): Path = testResourcesDir().resolve("sample-project")
/**
* Get the path to the 'resources' directory.
*
* @return The the resources directory.
*/
@JvmStatic
fun resources(): Path {
return testProjectRoot().resolve("app/src/main/resources")
}
/**
* Get the path to the file in resources.
*
* @param name The name of the file. Nested file paths can be separated using '/'.
* @return The path to the file.
*/
@JvmStatic
fun sourceFile(name: String): Path {
return resources().resolve("${name}_template.$extension").normalize()
}
@JvmStatic
fun contents(file: Path): StringBuilder = StringBuilder(file.readText())
}
}
@OptIn(ExperimentalPathApi::class)
private fun Path.findProjectRoot(): Path? {
if (walk().find { it.name == PROJECT_ROOT_FILE } != null) {
return this
}
return parent?.findProjectRoot()
}
@@ -0,0 +1,53 @@
/************************************************************************************
* 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.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
/** Reads an input stream line by line */
public class InputStreamLineReader implements Runnable {
private final InputStream in;
private final OnReadListener listener;
public InputStreamLineReader(InputStream in, OnReadListener listener) {
this.in = in;
this.listener = listener;
}
@Override
public void run() {
try {
final BufferedReader reader = new BufferedReader(new InputStreamReader(in));
String line;
while ((line = reader.readLine()) != null && listener != null) {
listener.onRead(line);
}
} catch (Throwable th) {
// ignored
}
}
public static interface OnReadListener {
void onRead(String line);
}
}
@@ -0,0 +1,72 @@
/*
* sora-editor - the awesome code editor for Android
* https://github.com/Rosemoe/sora-editor
* Copyright (C) 2020-2023 Rosemoe
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*
* Please contact Rosemoe by email 2073412493@qq.com if you need
* additional information or have any questions
*/
package com.itsaky.androidide.utils;
/**
* Pack two int into a long Also unpack it This is convenient while passing data
*
* @author Rose
*/
public class IntPair {
/**
* Convert an integer to a long whose binary bits are equal to the given integer
*/
private static long toUnsignedLong(int x) {
return ((long) x) & 0xffffffffL;
}
/**
* Pack two int into a long
*
* @param first First of pair
* @param second Second of pair
* @return Packed value
*/
public static long pack(int first, int second) {
return (toUnsignedLong(first) << 32L) | toUnsignedLong(second);
}
/**
* Get second of pair
*
* @param packedValue Packed value
* @return Second of pair
*/
public static int getSecond(long packedValue) {
return (int) (packedValue & 0xFFFFFFFFL);
}
/**
* Get first of pair
*
* @param packedValue Packed value
* @return First of pair
*/
public static int getFirst(long packedValue) {
return (int) (packedValue >> 32L);
}
}
@@ -0,0 +1,333 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.itsaky.androidide.utils;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.CoderResult;
import java.nio.charset.CodingErrorAction;
import java.nio.charset.UnsupportedCharsetException;
import java.util.Objects;
/**
* {@link InputStream} implementation that reads a character stream from a {@link Reader} and
* transforms it to a byte stream using a specified charset encoding. The stream is transformed
* using a {@link CharsetEncoder} object, guaranteeing that all charset encodings supported by the
* JRE are handled correctly. In particular for charsets such as UTF-16, the implementation ensures
* that one and only one byte order marker is produced.
*
* <p>Since in general it is not possible to predict the number of characters to be read from the
* {@link Reader} to satisfy a read request on the {@link ReaderInputStream}, all reads from the
* {@link Reader} are buffered. There is therefore no well defined correlation between the current
* position of the {@link Reader} and that of the {@link ReaderInputStream}. This also implies that
* in general there is no need to wrap the underlying {@link Reader} in a {@link
* java.io.BufferedReader}.
*
* <p>{@link ReaderInputStream} implements the inverse transformation of {@link
* java.io.InputStreamReader}; in the following example, reading from {@code in2} would return the
* same byte sequence as reading from {@code in} (provided that the initial byte sequence is legal
* with respect to the charset encoding):
*
* <pre>
* InputStream inputStream = ...
* Charset cs = ...
* InputStreamReader reader = new InputStreamReader(inputStream, cs);
* ReaderInputStream in2 = new ReaderInputStream(reader, cs);
* </pre>
*
* <p>{@link ReaderInputStream} implements the same transformation as {@link
* java.io.OutputStreamWriter}, except that the control flow is reversed: both classes transform a
* character stream into a byte stream, but {@link java.io.OutputStreamWriter} pushes data to the
* underlying stream, while {@link ReaderInputStream} pulls it from the underlying stream.
*
* <p>Note that while there are use cases where there is no alternative to using this class, very
* often the need to use this class is an indication of a flaw in the design of the code. This class
* is typically used in situations where an existing API only accepts an {@link InputStream}, but
* where the most natural way to produce the data is as a character stream, i.e. by providing a
* {@link Reader} instance. An example of a situation where this problem may appear is when
* implementing the {@code jdkx.activation.DataSource} interface from the Java Activation
* Framework.
*
* <p>Given the fact that the {@link Reader} class doesn't provide any way to predict whether the
* next read operation will block or not, it is not possible to provide a meaningful implementation
* of the {@link InputStream#available()} method. A call to this method will always return 0. Also,
* this class doesn't support {@link InputStream#mark(int)}.
*
* <p>Instances of {@link ReaderInputStream} are not thread safe.
*
* @since 2.0
*/
public class ReaderInputStream extends InputStream {
private static final int DEFAULT_BUFFER_SIZE = 1024;
private final Reader reader;
private final CharsetEncoder charsetEncoder;
/**
* CharBuffer used as input for the decoder. It should be reasonably large as we read data from
* the underlying Reader into this buffer.
*/
private final CharBuffer encoderIn;
/**
* ByteBuffer used as output for the decoder. This buffer can be small as it is only used to
* transfer data from the decoder to the buffer provided by the caller.
*/
private final ByteBuffer encoderOut;
private CoderResult lastCoderResult;
private boolean endOfInput;
/**
* Constructs a new {@link ReaderInputStream} that uses the default character encoding with a
* default input buffer size of {@value #DEFAULT_BUFFER_SIZE} characters.
*
* @param reader the target {@link Reader}
* @deprecated 2.5 use {@link #ReaderInputStream(Reader, Charset)} instead
*/
@Deprecated
public ReaderInputStream(final Reader reader) {
this(reader, Charset.defaultCharset());
}
/**
* Constructs a new {@link ReaderInputStream} with a default input buffer size of {@value
* #DEFAULT_BUFFER_SIZE} characters.
*
* @param reader the target {@link Reader}
* @param charset the charset encoding
*/
public ReaderInputStream(final Reader reader, final Charset charset) {
this(reader, charset, DEFAULT_BUFFER_SIZE);
}
/**
* Constructs a new {@link ReaderInputStream}.
*
* @param reader the target {@link Reader}.
* @param charset the charset encoding.
* @param bufferSize the size of the input buffer in number of characters.
*/
public ReaderInputStream(final Reader reader, final Charset charset, final int bufferSize) {
// @formatter:off
this(
reader,
toCharset(charset)
.newEncoder()
.onMalformedInput(CodingErrorAction.REPLACE)
.onUnmappableCharacter(CodingErrorAction.REPLACE),
bufferSize);
// @formatter:on
}
/**
* Constructs a new {@link ReaderInputStream}.
*
* @param reader the target {@link Reader}
* @param charsetEncoder the charset encoder, null defauls to the default Charset encoder.
* @param bufferSize the size of the input buffer in number of characters
* @since 2.1
*/
public ReaderInputStream(
final Reader reader, final CharsetEncoder charsetEncoder, final int bufferSize) {
this.reader = reader;
this.charsetEncoder = toCharsetEncoder(charsetEncoder);
this.encoderIn = CharBuffer.allocate(checkMinBufferSize(this.charsetEncoder, bufferSize));
this.encoderIn.flip();
this.encoderOut = ByteBuffer.allocate(128);
this.encoderOut.flip();
}
static int checkMinBufferSize(final CharsetEncoder charsetEncoder, final int bufferSize) {
final float minRequired = minBufferSize(charsetEncoder);
if (bufferSize < minRequired) {
throw new IllegalArgumentException(
String.format(
"Buffer size %,d must be at least %s for a CharsetEncoder %s.",
bufferSize, minRequired, charsetEncoder.charset().displayName()));
}
return bufferSize;
}
static float minBufferSize(final CharsetEncoder charsetEncoder) {
return charsetEncoder.maxBytesPerChar() * 2;
}
static CharsetEncoder toCharsetEncoder(CharsetEncoder charsetEncoder) {
return charsetEncoder != null ? charsetEncoder : Charset.defaultCharset().newEncoder();
}
static Charset toCharset(final Charset charset) {
return charset == null ? Charset.defaultCharset() : charset;
}
/**
* Constructs a new {@link ReaderInputStream}.
*
* @param reader the target {@link Reader}
* @param charsetEncoder the charset encoder
* @since 2.1
*/
public ReaderInputStream(final Reader reader, final CharsetEncoder charsetEncoder) {
this(reader, charsetEncoder, DEFAULT_BUFFER_SIZE);
}
/**
* Constructs a new {@link ReaderInputStream} with a default input buffer size of {@value
* #DEFAULT_BUFFER_SIZE} characters.
*
* @param reader the target {@link Reader}
* @param charsetName the name of the charset encoding
*/
public ReaderInputStream(final Reader reader, final String charsetName) {
this(reader, charsetName, DEFAULT_BUFFER_SIZE);
}
/**
* Constructs a new {@link ReaderInputStream}.
*
* @param reader the target {@link Reader}
* @param charsetName the name of the charset encoding, null maps to the default Charset.
* @param bufferSize the size of the input buffer in number of characters
*/
public ReaderInputStream(final Reader reader, final String charsetName, final int bufferSize) {
this(reader, toCharset(charsetName), bufferSize);
}
static Charset toCharset(final String charsetName) throws UnsupportedCharsetException {
return charsetName == null ? Charset.defaultCharset() : Charset.forName(charsetName);
}
/**
* Read a single byte.
*
* @return either the byte read or {@code -1} if the end of the stream has been reached
* @throws IOException if an I/O error occurs.
*/
@Override
public int read() throws IOException {
for (; ; ) {
if (encoderOut.hasRemaining()) {
return encoderOut.get() & 0xFF;
}
fillBuffer();
if (endOfInput && !encoderOut.hasRemaining()) {
return -1;
}
}
}
/**
* Fills the internal char buffer from the reader.
*
* @throws IOException If an I/O error occurs
*/
private void fillBuffer() throws IOException {
if (!endOfInput && (lastCoderResult == null || lastCoderResult.isUnderflow())) {
encoderIn.compact();
final int position = encoderIn.position();
// We don't use Reader#read(CharBuffer) here because it is more efficient
// to write directly to the underlying char array (the default implementation
// copies data to a temporary char array).
final int c = reader.read(encoderIn.array(), position, encoderIn.remaining());
if (c == -1) {
endOfInput = true;
} else {
encoderIn.position(position + c);
}
encoderIn.flip();
}
encoderOut.compact();
lastCoderResult = charsetEncoder.encode(encoderIn, encoderOut, endOfInput);
if (endOfInput) {
lastCoderResult = charsetEncoder.flush(encoderOut);
}
if (lastCoderResult.isError()) {
lastCoderResult.throwException();
}
encoderOut.flip();
}
/**
* Read the specified number of bytes into an array.
*
* @param b the byte array to read into
* @return the number of bytes read or {@code -1} if the end of the stream has been reached
* @throws IOException if an I/O error occurs.
*/
@Override
public int read(final byte[] b) throws IOException {
return read(b, 0, b.length);
}
/**
* Read the specified number of bytes into an array.
*
* @param array the byte array to read into
* @param off the offset to start reading bytes into
* @param len the number of bytes to read
* @return the number of bytes read or {@code -1} if the end of the stream has been reached
* @throws IOException if an I/O error occurs.
*/
@Override
public int read(final byte[] array, int off, int len) throws IOException {
Objects.requireNonNull(array, "array");
if (len < 0 || off < 0 || (off + len) > array.length) {
throw new IndexOutOfBoundsException(
"Array size=" + array.length + ", offset=" + off + ", length=" + len);
}
int read = 0;
if (len == 0) {
return 0; // Always return 0 if len == 0
}
while (len > 0) {
if (encoderOut.hasRemaining()) { // Data from the last read not fully copied
final int c = Math.min(encoderOut.remaining(), len);
encoderOut.get(array, off, c);
off += c;
len -= c;
read += c;
} else if (endOfInput) { // Already reach EOF in the last read
break;
} else { // Read again
fillBuffer();
}
}
return read == 0 && endOfInput ? -1 : read;
}
/**
* Close the stream. This method will cause the underlying {@link Reader} to be closed.
*
* @throws IOException if an I/O error occurs.
*/
@Override
public void close() throws IOException {
reader.close();
}
/**
* Gets the CharsetEncoder.
*
* @return the CharsetEncoder.
*/
CharsetEncoder getCharsetEncoder() {
return charsetEncoder;
}
}
@@ -0,0 +1,225 @@
/*
* 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 org.slf4j.LoggerFactory
import java.util.concurrent.ArrayBlockingQueue
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicLong
import kotlin.math.min
// TODO(itsaky): Automatically resize the pool based on the cache hit rate and the acess count
/**
* An object pool allows to recycle and reuse objects of a specific type. Implmentations of this are
* expected to be thread safe.
*
* @param capacity The maximum capacity of the pool.
* @param fillFirst The number of items that should be created and put in the cache.
* @param klass The class of the recyclable object. This is used for logging purposes.
* @param objFactory The factory which will be used to create new instances of the object.
* @author Akash Yadav
*/
class RecyclableObjectPool<RecyclableT : RecyclableObjectPool.Recyclable> @JvmOverloads constructor(
private val capacity: Int = CAPACITY_DEFAULT,
private val objFactory: Factory<RecyclableT>,
private val metricsEnabled: Boolean = true,
fillFirst: Int = 0,
klass: Class<RecyclableT>
) {
private val lastMetricsLog = AtomicLong(0)
companion object {
var DEBUG = false
var LOG_METRICS_ON_N_ACCESS = 400
private const val CACHE_HIT_WARNING_THRESHOLD = 50f
private val log by lazy {
LoggerFactory.getLogger(RecyclableObjectPool::class.java)
}
const val CAPACITY_DEFAULT: Int = 4096
const val CAPACITY_LARGE: Int = CAPACITY_DEFAULT * 2
const val CAPACITY_SMALL: Int = CAPACITY_DEFAULT / 2
const val CAPACITY_MINI: Int = CAPACITY_SMALL / 2
}
private val objName = klass.name
/**
* Access count and cache hit of the pool. MSB 32 bits of this long represent the access count
* while the LSB 32 bits represent the cache hit.
*/
private val accCount = AtomicLong(0)
/**
* The recycle count of this pool.
*/
private val recCount = AtomicInteger(0)
private val cache = ArrayBlockingQueue<RecyclableT>(capacity)
init {
this.fillFirst(fillFirst)
}
private fun fillFirst(n: Int) {
if (n < 1) {
return
}
val count = min(n, capacity)
for (i in 1..count) {
cache.offer(objFactory.create())
}
}
/**
* Recycle the given object.
*/
fun recycle(obj: RecyclableT): Boolean {
if (obj.isRecycled) {
log.warn("Trying to recyle already recycled object: {}", obj)
return false
}
return cache.offer(obj).also { inserted ->
if (inserted) {
obj.isRecycled = true
recCount.incrementAndGet()
}
}
}
/**
* Obtain an object from the pool or a new object if the pool is empty.
*/
fun obtain(): RecyclableT {
val acc = accCount.get()
var access = IntPair.getFirst(acc)
var cacheHit = IntPair.getSecond(acc)
// increment the access count
accCount.set(IntPair.pack(++access, cacheHit))
val result = this.cache.poll()?.also {
it.isRecycled = false
// increment the cache hit
accCount.set(IntPair.pack(access, ++cacheHit))
} ?: objFactory.create()
if (DEBUG && access % LOG_METRICS_ON_N_ACCESS == 0) {
logMetrics()
}
return result
}
fun cacheHitRate(): Float {
val acc = accCount.get();
val access = IntPair.getFirst(acc).toFloat()
val cacheHit = IntPair.getSecond(acc).toFloat()
return (cacheHit / access) * 100F
}
fun cacheMissRate(): Float {
return 100F - cacheHitRate()
}
fun recycleRate(): Float {
val rec = recCount.get().toFloat()
val access = IntPair.getFirst(accCount.get()).toFloat()
return (rec / access) * 100
}
fun cacheUtilization(): Float {
val access = IntPair.getFirst(accCount.get()).toFloat()
return (access / capacity) * 100
}
fun logMetrics() {
if (!(metricsEnabled && DEBUG) || System.currentTimeMillis() - lastMetricsLog.get() < 1000) {
return
}
val acc = accCount.get()
val rec = recCount.get()
val access = IntPair.getFirst(acc)
val cacheHit = IntPair.getSecond(acc)
val cacheHitRate = cacheHitRate()
val simpleName = objName.let { if (it.contains('.')) it.substringAfterLast('.') else it }
log.debug("{}: {}({})", javaClass.simpleName, simpleName, objName)
log.debug(" Recycle count : {}", rec)
log.debug(" Access count : {}", access)
log.debug(" Cache hit count : {}", cacheHit)
log.debug(" Cache hit rate : {}%", cacheHitRate)
log.debug(" Cache miss rate : {}%", cacheMissRate())
log.debug(" Recycle rate : {}%", recycleRate())
log.debug(" Cache utilization rate : {}%", cacheUtilization())
log.debug(" Objects in cache : {}", cache.size)
log.debug(" Total capacity : {}", capacity)
if (cacheHitRate < CACHE_HIT_WARNING_THRESHOLD) {
// Cache hit rate is less than threshold
// something might be wrong
// log an error
log.error("!!!!!!!!!!!!!!!!!!!!! CRITICAL ERROR !!!!!!!!!!!!!!!!!!!!!")
log.error("Cache-hit rate for '{}' is less than {}%!!", simpleName, CACHE_HIT_WARNING_THRESHOLD)
log.error("Make sure that instances of {} are obtained using the object pool", objName)
log.error("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
}
lastMetricsLog.set(System.currentTimeMillis())
}
/**
* Any object that can be recycled.
*
* A [Recyclable] object implementation should prefer defining a default no-args constructor which
* initializes the properties with the default values.
*/
interface Recyclable {
/**
* Whether the object has been recycled. This property is supposed to be modified only by the pool.
*/
var isRecycled: Boolean
/**
* Recycle the this object. Implementations should make sure that all the properties are resset
* to their default values (as if the object was newly created).
*/
fun recycle()
}
/**
* A [Factory] creates [Recyclable] objects.
*/
fun interface Factory<_RecyclableT : Recyclable> {
/**
* Create the [_RecyclableT] object.
*/
fun create(): _RecyclableT
}
}
@@ -0,0 +1,637 @@
/*
* 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/>.
*/
/*
* Copyright (C) 2014 The Android Open Source Project
* Copyright (c) 2005, 2013, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code 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
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package com.itsaky.androidide.utils;
import androidx.annotation.NonNull;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Optional;
import java.util.ServiceConfigurationError;
/**
* A simple service-provider loading facility. This class is an adaptation of the
* {@link java.util.ServiceLoader ServiceLoader} to avoid the usage of {@link Class#newInstance()}
* and use
* {@link java.lang.reflect.Constructor#newInstance(Object...) Constructor.newInstance(Object...)}
* for class instance creation.
*
* <p> A <i>service</i> is a well-known set of interfaces and (usually
* abstract) classes. A <i>service provider</i> is a specific implementation of a service. The
* classes in a provider typically implement the interfaces and subclass the classes defined in the
* service itself. Service providers can be installed in an implementation of the Java platform in
* the form of extensions, that is, jar files placed into any of the usual extension directories.
* Providers can also be made available by adding them to the application's class path or by some
* other platform-specific means.
*
* <p> For the purpose of loading, a service is represented by a single type,
* that is, a single interface or abstract class. (A concrete class can be used, but this is not
* recommended.) A provider of a given service contains one or more concrete classes that extend
* this <i>service type</i> with data and code specific to the provider. The <i>provider class</i>
* is typically not the entire provider itself but rather a proxy which contains enough information
* to decide whether the provider is able to satisfy a particular request together with code that
* can create the actual provider on demand. The details of provider classes tend to be highly
* service-specific; no single class or interface could possibly unify them, so no such type is
* defined here. The only requirement enforced by this facility is that provider classes must have
* a zero-argument constructor so that they can be instantiated during loading.
*
* <p><a name="format"> A service provider is identified by placing a
* <i>provider-configuration file</i> in the resource directory
* <tt>META-INF/services</tt>.</a> The file's name is the fully-qualified <a
* href="../lang/ClassLoader.html#name">binary name</a> of the service's type. The file contains a
* list of fully-qualified binary names of concrete provider classes, one per line. Space and tab
* characters surrounding each name, as well as blank lines, are ignored. The comment character is
* <tt>'#'</tt> (<tt>'&#92;u0023'</tt>,
* <font style="font-size:smaller;">NUMBER SIGN</font>); on
* each line all characters following the first comment character are ignored. The file must be
* encoded in UTF-8.
*
* <p> If a particular concrete provider class is named in more than one
* configuration file, or is named in the same configuration file more than once, then the
* duplicates are ignored. The configuration file naming a particular provider need not be in the
* same jar file or other distribution unit as the provider itself. The provider must be accessible
* from the same class loader that was initially queried to locate the configuration file; note that
* this is not necessarily the class loader from which the file was actually loaded.
*
* <p> Providers are located and instantiated lazily, that is, on demand. A
* service loader maintains a cache of the providers that have been loaded so far. Each invocation
* of the {@link #iterator iterator} method returns an iterator that first yields all of the
* elements of the cache, in instantiation order, and then lazily locates and instantiates any
* remaining providers, adding each one to the cache in turn. The cache can be cleared via the
* {@link #reload reload} method.
*
* <p> Service loaders always execute in the security context of the caller.
* Trusted system code should typically invoke the methods in this class, and the methods of the
* iterators which they return, from within a privileged security context.
*
* <p> Instances of this class are not safe for use by multiple concurrent
* threads.
*
* <p> Unless otherwise specified, passing a <tt>null</tt> argument to any
* method in this class will cause a {@link NullPointerException} to be thrown.
*
* <p><span style="font-weight: bold; padding-right: 1em">Example</span>
* Suppose we have a service type <tt>com.example.CodecSet</tt> which is intended to represent sets
* of encoder/decoder pairs for some protocol. In this case it is an abstract class with two
* abstract methods:
*
* <blockquote><pre>
* public abstract Encoder getEncoder(String encodingName);
* public abstract Decoder getDecoder(String encodingName);</pre></blockquote>
* <p>
* Each method returns an appropriate object or <tt>null</tt> if the provider does not support the
* given encoding. Typical providers support more than one encoding.
*
* <p> If <tt>com.example.impl.StandardCodecs</tt> is an implementation of the
* <tt>CodecSet</tt> service then its jar file also contains a file named
*
* <blockquote><pre>
* META-INF/services/com.example.CodecSet</pre></blockquote>
*
* <p> This file contains the single line:
*
* <blockquote><pre>
* com.example.impl.StandardCodecs # Standard codecs</pre></blockquote>
*
* <p> The <tt>CodecSet</tt> class creates and saves a single service instance
* at initialization:
*
* <blockquote><pre>
* private static ServiceLoader&lt;CodecSet&gt; codecSetLoader
* = ServiceLoader.load(CodecSet.class);</pre></blockquote>
*
* <p> To locate an encoder for a given encoding name it defines a static
* factory method which iterates through the known and available providers, returning only when it
* has located a suitable encoder or has run out of providers.
*
* <blockquote><pre>
* public static Encoder getEncoder(String encodingName) {
* for (CodecSet cp : codecSetLoader) {
* Encoder enc = cp.getEncoder(encodingName);
* if (enc != null)
* return enc;
* }
* return null;
* }</pre></blockquote>
*
* <p> A <tt>getDecoder</tt> method is defined similarly.
*
* <p><span style="font-weight: bold; padding-right: 1em">Usage Note</span> If
* the class path of a class loader that is used for provider loading includes remote network URLs
* then those URLs will be dereferenced in the process of searching for provider-configuration
* files.
*
* <p> This activity is normal, although it may cause puzzling entries to be
* created in web-server logs. If a web server is not configured correctly, however, then this
* activity may cause the provider-loading algorithm to fail spuriously.
*
* <p> A web server should return an HTTP 404 (Not Found) response when a
* requested resource does not exist. Sometimes, however, web servers are erroneously configured to
* return an HTTP 200 (OK) response along with a helpful HTML error page in such cases. This will
* cause a {@link ServiceConfigurationError} to be thrown when this class attempts to parse the HTML
* page as a provider-configuration file. The best solution to this problem is to fix the
* misconfigured web server to return the correct response code (HTTP 404) along with the HTML error
* page.
*
* @param <S> The type of the service to be loaded by this loader
* @author Mark Reinhold
* @since 1.6
*/
public final class ServiceLoader<S> implements Iterable<S> {
private static final String PREFIX = "META-INF/services/";
// The class or interface representing the service being loaded
private final Class<S> service;
// The class loader used to locate, load, and instantiate providers
private final ClassLoader loader;
// The access control context taken when the ServiceLoader is created
// Android-changed: do not use legacy security code.
// private final AccessControlContext acc;
// Cached providers, in instantiation order
private final LinkedHashMap<String, S> providers = new LinkedHashMap<>();
// The current lazy-lookup iterator
private LazyIterator lookupIterator;
/**
* Clear this loader's provider cache so that all providers will be reloaded.
*
* <p> After invoking this method, subsequent invocations of the {@link
* #iterator() iterator} method will lazily look up and instantiate providers from scratch, just
* as is done by a newly-created loader.
*
* <p> This method is intended for use in situations in which new providers
* can be installed into a running Java virtual machine.
*/
public void reload() {
providers.clear();
lookupIterator = new LazyIterator(service, loader);
}
private ServiceLoader(Class<S> svc, ClassLoader cl) {
service = Objects.requireNonNull(svc, "Service interface cannot be null");
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
// Android-changed: Do not use legacy security code.
// On Android, System.getSecurityManager() is always null.
// acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload();
}
private static void fail(Class<?> service, String msg, Throwable cause)
throws ServiceConfigurationError {
throw new ServiceConfigurationError(service.getName() + ": " + msg, cause);
}
private static void fail(Class<?> service, String msg) throws ServiceConfigurationError {
throw new ServiceConfigurationError(service.getName() + ": " + msg);
}
private static void fail(Class<?> service, URL u, int line, String msg)
throws ServiceConfigurationError {
fail(service, u + ":" + line + ": " + msg);
}
// Parse a single line from the given configuration file, adding the name
// on the line to the names list.
//
private int parseLine(Class<?> service, URL u, BufferedReader r, int lc, List<String> names)
throws IOException, ServiceConfigurationError {
String ln = r.readLine();
if (ln == null) {
return -1;
}
int ci = ln.indexOf('#');
if (ci >= 0) {
ln = ln.substring(0, ci);
}
ln = ln.trim();
int n = ln.length();
if (n != 0) {
if ((ln.indexOf(' ') >= 0) || (ln.indexOf('\t') >= 0)) {
fail(service, u, lc, "Illegal configuration-file syntax");
}
int cp = ln.codePointAt(0);
if (!Character.isJavaIdentifierStart(cp)) {
fail(service, u, lc, "Illegal provider-class name: " + ln);
}
for (int i = Character.charCount(cp); i < n; i += Character.charCount(cp)) {
cp = ln.codePointAt(i);
if (!Character.isJavaIdentifierPart(cp) && (cp != '.')) {
fail(service, u, lc, "Illegal provider-class name: " + ln);
}
}
if (!providers.containsKey(ln) && !names.contains(ln)) {
names.add(ln);
}
}
return lc + 1;
}
// Parse the content of the given URL as a provider-configuration file.
//
// @param service
// The service type for which providers are being sought;
// used to construct error detail strings
//
// @param u
// The URL naming the configuration file to be parsed
//
// @return A (possibly empty) iterator that will yield the provider-class
// names in the given configuration file that are not yet members
// of the returned set
//
// @throws ServiceConfigurationError
// If an I/O error occurs while reading from the given URL, or
// if a configuration-file format error is detected
//
private Iterator<String> parse(Class<?> service, URL u) throws ServiceConfigurationError {
InputStream in = null;
BufferedReader r = null;
ArrayList<String> names = new ArrayList<>();
try {
in = u.openStream();
r = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
int lc = 1;
//noinspection StatementWithEmptyBody
while ((lc = parseLine(service, u, r, lc, names)) >= 0);
} catch (IOException x) {
fail(service, "Error reading configuration file", x);
} finally {
try {
if (r != null) {
r.close();
}
if (in != null) {
in.close();
}
} catch (IOException y) {
fail(service, "Error closing configuration file", y);
}
}
return names.iterator();
}
// Private inner class implementing fully-lazy provider lookup
//
private class LazyIterator implements Iterator<S> {
Class<S> service;
ClassLoader loader;
Enumeration<URL> configs = null;
Iterator<String> pending = null;
String nextName = null;
private LazyIterator(Class<S> service, ClassLoader loader) {
this.service = service;
this.loader = loader;
}
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
String fullName = PREFIX + service.getName();
if (loader == null) {
configs = ClassLoader.getSystemResources(fullName);
} else {
configs = loader.getResources(fullName);
}
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
pending = parse(service, configs.nextElement());
}
nextName = pending.next();
return true;
}
private S nextService() {
if (!hasNextService()) {
throw new NoSuchElementException();
}
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,
// Android-changed: Let the ServiceConfigurationError have a cause.
"Provider " + cn + " not found", x);
// "Provider " + cn + " not found");
}
if (!service.isAssignableFrom(c)) {
// Android-changed: Let the ServiceConfigurationError have a cause.
ClassCastException cce = new ClassCastException(
service.getCanonicalName() + " is not assignable from " + c.getCanonicalName());
fail(service, "Provider " + cn + " not a subtype", cce);
}
try {
S p = service.cast(c.getConstructor().newInstance());
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service, "Provider " + cn + " could not be instantiated", x);
}
throw new Error(); // This cannot happen
}
public boolean hasNext() {
// Android-changed: do not use legacy security code
/* if (acc == null) { */
return hasNextService();
/*
} else {
PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
public Boolean run() { return hasNextService(); }
};
return AccessController.doPrivileged(action, acc);
}
*/
}
public S next() {
// Android-changed: do not use legacy security code
/* if (acc == null) { */
return nextService();
/*
} else {
PrivilegedAction<S> action = new PrivilegedAction<S>() {
public S run() { return nextService(); }
};
return AccessController.doPrivileged(action, acc);
}
*/
}
public void remove() {
throw new UnsupportedOperationException();
}
}
/**
* Lazily loads the available providers of this loader's service.
*
* <p> The iterator returned by this method first yields all of the
* elements of the provider cache, in instantiation order. It then lazily loads and instantiates
* any remaining providers, adding each one to the cache in turn.
*
* <p> To achieve laziness the actual work of parsing the available
* provider-configuration files and instantiating providers must be done by the iterator itself.
* Its {@link java.util.Iterator#hasNext hasNext} and {@link java.util.Iterator#next next} methods
* can therefore throw a {@link ServiceConfigurationError} if a provider-configuration file
* violates the specified format, or if it names a provider class that cannot be found and
* instantiated, or if the result of instantiating the class is not assignable to the service
* type, or if any other kind of exception or error is thrown as the next provider is located and
* instantiated. To write robust code it is only necessary to catch
* {@link ServiceConfigurationError} when using a service iterator.
*
* <p> If such an error is thrown then subsequent invocations of the
* iterator will make a best effort to locate and instantiate the next available provider, but in
* general such recovery cannot be guaranteed.
* <blockquote style="font-size: smaller; line-height: 1.2"><span
* style="padding-right: 1em; font-weight: bold">Design Note</span> Throwing an error in these
* cases may seem extreme. The rationale for this behavior is that a malformed
* provider-configuration file, like a malformed class file, indicates a serious problem with the
* way the Java virtual machine is configured or is being used. As such it is preferable to throw
* an error rather than try to recover or, even worse, fail silently.</blockquote>
*
* <p> The iterator returned by this method does not support removal.
* Invoking its {@link java.util.Iterator#remove() remove} method will cause an
* {@link UnsupportedOperationException} to be thrown.
*
* @return An iterator that lazily loads providers for this loader's service
* @implNote When adding providers to the cache, the {@link #iterator Iterator} processes
* resources in the order that the
* {@link java.lang.ClassLoader#getResources(java.lang.String) ClassLoader.getResources(String)}
* method finds the service configuration files.
*/
@NonNull
public Iterator<S> iterator() {
return new Iterator<S>() {
final Iterator<Map.Entry<String, S>> knownProviders = providers.entrySet().iterator();
public boolean hasNext() {
if (knownProviders.hasNext()) {
return true;
}
return lookupIterator.hasNext();
}
public S next() {
if (knownProviders.hasNext()) {
return knownProviders.next().getValue();
}
return lookupIterator.next();
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
/**
* Creates a new service loader for the given service type and class loader.
*
* @param <S> the class of the service type
* @param service The interface or abstract class representing the service
* @param loader The class loader to be used to load provider-configuration files and provider
* classes, or <tt>null</tt> if the system class loader (or, failing that, the
* bootstrap class loader) is to be used
* @return A new service loader
*/
public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader) {
return new ServiceLoader<>(service, loader);
}
/**
* Creates a new service loader for the given service type, using the current thread's
* {@linkplain java.lang.Thread#getContextClassLoader context class loader}.
*
* <p> An invocation of this convenience method of the form
*
* <blockquote><pre>
* ServiceLoader.load(<i>service</i>)</pre></blockquote>
* <p>
* is equivalent to
*
* <blockquote><pre>
* ServiceLoader.load(<i>service</i>,
* Thread.currentThread().getContextClassLoader())</pre></blockquote>
*
* @param <S> the class of the service type
* @param service The interface or abstract class representing the service
* @return A new service loader
*/
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
/**
* Creates a new service loader for the given service type, using the extension class loader.
*
* <p> This convenience method simply locates the extension class loader,
* call it <tt><i>extClassLoader</i></tt>, and then returns
*
* <blockquote><pre>
* ServiceLoader.load(<i>service</i>, <i>extClassLoader</i>)</pre></blockquote>
*
* <p> If the extension class loader cannot be found then the system class
* loader is used; if there is no system class loader then the bootstrap class loader is used.
*
* <p> This method is intended for use when only installed providers are
* desired. The resulting service will only find and load providers that have been installed into
* the current Java virtual machine; providers on the application's class path will be ignored.
*
* @param <S> the class of the service type
* @param service The interface or abstract class representing the service
* @return A new service loader
*/
public static <S> ServiceLoader<S> loadInstalled(Class<S> service) {
ClassLoader cl = ClassLoader.getSystemClassLoader();
ClassLoader prev = null;
while (cl != null) {
prev = cl;
cl = cl.getParent();
}
return ServiceLoader.load(service, prev);
}
// BEGIN Android-added: loadFromSystemProperty(), for internal use.
// Instantiates a class from a system property (used elsewhere in libcore).
/**
* Internal API to support built-in SPIs that check a system property first. Returns an instance
* specified by a property with the class' binary name, or null if no such property is set.
*
* @hide
*/
public static <S> S loadFromSystemProperty(final Class<S> service) {
try {
final String className = System.getProperty(service.getName());
if (className != null) {
Class<?> c = ClassLoader.getSystemClassLoader().loadClass(className);
return (S) c.getConstructor().newInstance();
}
return null;
} catch (Exception e) {
throw new Error(e);
}
}
// END Android-added: loadFromSystemProperty(), for internal use.
/**
* Load the first available service provider of this loader's service. This convenience method is
* equivalent to invoking the {@link #iterator() iterator()} method and obtaining the first
* element. It therefore returns the first element from the provider cache if possible, it
* otherwise attempts to load and instantiate the first provider.
*
* <p> The following example loads the first available service provider. If
* no service providers are located then it uses a default implementation.
* <pre>{@code
* CodecFactory factory = ServiceLoader.load(CodecFactory.class)
* .findFirst()
* .orElse(DEFAULT_CODECSET_FACTORY);
* }</pre>
*
* @return The first service provider or empty {@code Optional} if no service providers are
* located
* @throws ServiceConfigurationError If a provider class cannot be loaded for any of the reasons
* specified in the <a href="#errors">Errors</a> section above.
* @since 9
*/
public Optional<S> findFirst() {
Iterator<S> iterator = iterator();
if (iterator.hasNext()) {
return Optional.of(iterator.next());
} else {
return Optional.empty();
}
}
/**
* Loads the first available service provider of this loader's service. Throws
* {@link ServiceNotFoundException} if the service provider cannot be found.
*
* @return The first service provider.
* @throws ServiceNotFoundException If no service provider is found for the service.
* @see #findFirst()
*/
public S findFirstOrThrow() throws ServiceNotFoundException {
final var first = findFirst();
if (first.isPresent()) {
return first.get();
}
throw new ServiceNotFoundException(service);
}
/**
* Returns a string describing this service.
*
* @return A descriptive string
*/
public String toString() {
return "java.util.ServiceLoader[" + service.getName() + "]";
}
}
@@ -0,0 +1,30 @@
/*
* 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;
/**
* Exception thrown when a service cannot be loaded by {@link ServiceLoader}.
*
* @author Akash Yadav
*/
class ServiceNotFoundException extends IllegalStateException {
public ServiceNotFoundException(Class<?> service) {
super("Cannot find service " + service);
}
}
@@ -0,0 +1,180 @@
/*
* 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.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.time.Instant
import kotlin.io.path.nameWithoutExtension
/**
* A [ClassTrie] implementation to store information about java source files.
*
* @author Akash Yadav
*/
open class SourceClassTrie(root: SourcePackageNode = SourcePackageNode()) : ClassTrie(root) {
companion object {
@JvmStatic
private val pkgNameMethod by lazy {
Class.forName("com.itsaky.androidide.projects.util.StringSearch")
.getDeclaredMethod("packageName", Path::class.java)
}
@JvmStatic
private fun packageName(file: Path): String {
return try {
pkgNameMethod.invoke(null, file) as String
} catch (error: Throwable) {
throw RuntimeException(error)
}
}
}
override fun append(name: String): Node {
throw UnsupportedOperationException("Method not supported!")
}
open fun append(path: Path, sourceDir: Path): SourceNode {
val modified = Files.getLastModifiedTime(path).toInstant()
val packageName = packageName(path)
val segments = segments(packageName)
var node: SourcePackageNode = root as SourcePackageNode
var currentPath = sourceDir
for ((index, segment) in segments.withIndex()) {
if (segment.isEmpty() || !segment[0].isJavaIdentifierStart()) {
continue
}
currentPath = currentPath.resolve(segment)
node = createNode(currentPath, node, segment, segments, index)
}
node.isClass = false
val name = path.nameWithoutExtension
val klass =
SourceNode(
file = path,
packageName = packageName,
modified = modified,
parent = node,
name = name
)
klass.isClass = true
node.children[name] = klass
return klass
}
open fun createNode(
dir: Path,
node: SourcePackageNode,
segment: String,
segments: List<String>,
index: Int
) = node.createChild(dir, segment, segments.subList(0, index + 1).joinToString(separator = "."))
override fun createNode(node: Node, segment: String, segments: List<String>, index: Int): Node {
throw UnsupportedOperationException("Method not supported")
}
open fun allSources(): List<SourceNode> {
return allClassNodes().filterIsInstance(SourceNode::class.java)
}
fun findSource(file: Path): SourceNode? {
return findSourceInNode(file, root)
}
fun findDirNode(dir: Path, node: Node): SourcePackageNode? {
var dirNode: SourcePackageNode? = null
for (child in node.children.values) {
if (
child !is SourceNode &&
child is SourcePackageNode &&
DocumentUtils.isSameFile(child.dir, dir)
) {
dirNode = child
}
dirNode = dirNode ?: findDirNode(dir, child)
if (dirNode != null) {
break
}
}
return dirNode
}
fun getSourceFilesInDir(dir: Path): List<SourceNode> {
val dirNode = findDirNode(dir, root) ?: return emptyList()
return dirNode.children.values.filterIsInstance(SourceNode::class.java)
}
private fun findSourceInNode(file: Path, node: Node): SourceNode? {
if (node is SourceNode && DocumentUtils.isSameFile(file, node.file)) {
return node
}
for (child in node.children.values) {
if (child is SourceNode && DocumentUtils.isSameFile(file, child.file)) {
return child
}
val childResult = findSourceInNode(file, child)
if (childResult != null) {
return childResult
}
}
return null
}
open class SourceNode(
val file: Path,
val packageName: String,
val modified: Instant,
parent: SourcePackageNode? = null,
name: String
) : Node(name = name, qualifiedName = "$packageName.$name", parent = parent) {
override fun createChild(name: String, qualifiedName: String): Node {
throw UnsupportedOperationException("Method not supported!")
}
}
open class SourcePackageNode(
val dir: Path,
parent: SourcePackageNode? = null,
name: String,
qualifiedName: String
) : Node(name, qualifiedName, parent) {
internal constructor() : this(Paths.get(""), null, "", "")
override fun createChild(name: String, qualifiedName: String): Node {
throw UnsupportedOperationException("Method not suported!")
}
open fun createChild(dir: Path, name: String, qualifiedName: String): SourcePackageNode {
return this.children.computeIfAbsent(name) {
SourcePackageNode(dir, this, name, qualifiedName)
} as SourcePackageNode
}
}
}
@@ -0,0 +1,80 @@
/*
* 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 org.slf4j.LoggerFactory
import java.io.PrintStream
/**
* A stop watch helps to log duration between the time when the instance of the stopwatch instance
* was created and the time when [StopWatch.log] or [StopWatch.lap] method is called.
*
* @param label The label for the log message.
* @author Akash Yadav
*/
class StopWatch
@JvmOverloads
constructor(
val label: String,
val start: Long = System.currentTimeMillis(),
var lastLap: Long = start
) {
companion object {
private val log = LoggerFactory.getLogger(StopWatch::class.java)
}
fun log() {
log.debug("{} completed in {}ms", label, System.currentTimeMillis() - start)
}
fun lap(message: String) {
log.debug("{} in {}ms", message, System.currentTimeMillis() - start)
lastLap = System.currentTimeMillis()
}
fun lapFromLast(message: String) {
log.debug("{} in {}ms", message, System.currentTimeMillis() - lastLap)
lastLap = System.currentTimeMillis()
}
fun writeTo(stream: PrintStream) {
stream.println("$label completed in ${System.currentTimeMillis() - start}ms")
}
}
/**
* Run the given action with a stopwatch to log the time the action took to execute.
*
* @see StopWatch
*/
inline fun <R> withStopWatch(
label: String,
start: Long = System.currentTimeMillis(),
lastLap: Long = start,
action: (StopWatch) -> R
): R {
return StopWatch(label, start, lastLap).run {
try {
action(this)
} finally {
log()
}
}
}
@@ -0,0 +1,33 @@
package com.itsaky.androidide.utils;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
public class StringUtil {
@Contract(pure = true)
public static boolean isJavaIdentifier(@NotNull String text) {
int len = text.length();
if (len == 0) return false;
if (!isJavaIdentifierStart(text.charAt(0))) return false;
for (int i = 1; i < len; i++) {
if (!isJavaIdentifierPart(text.charAt(i))) return false;
}
return true;
}
@Contract(pure = true)
public static boolean isJavaIdentifierStart(char c) {
return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || Character.isJavaIdentifierStart(c);
}
@Contract(pure = true)
public static boolean isJavaIdentifierPart(char c) {
return c >= '0' && c <= '9'
|| c >= 'a' && c <= 'z'
|| c >= 'A' && c <= 'Z'
|| Character.isJavaIdentifierPart(c);
}
}
@@ -0,0 +1,199 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.itsaky.androidide.utils;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.NoSuchElementException;
/**
* @param <T> The type of path.
* @see <a
* href="https://github.com/JetBrains/intellij-community/blob/master/platform/util/base/src/com/intellij/filename/UniqueNameBuilder.java">UniqueNameBuilder.java</a>
*/
public final class UniqueNameBuilder<T> {
private static final String VFS_SEPARATOR = "/";
private final Map<T, String> myPaths = new HashMap<>();
private final String mySeparator;
private final String myRoot;
public UniqueNameBuilder(String root, String separator) {
myRoot = root;
mySeparator = separator;
}
public boolean contains(T file) {
return myPaths.containsKey(file);
}
public int size() {
return myPaths.size();
}
private static final class Node {
final String myText;
final HashMap<String, Node> myChildren;
final Node myParentNode;
int myNestedChildrenCount;
Node(String text, Node parentNode) {
myText = text;
myParentNode = parentNode;
myChildren = new HashMap<>();
}
Node findOrAddChild(String word) {
Node node = myChildren.get(word);
if (node == null) {
myChildren.put(word, node = new Node(word, this));
}
return node;
}
}
private final Node myRootNode = new Node("", null);
// Build a trie from path components starting from end
// E.g. following try will be build from example
// |<-------[/fabrique] <- [/idea]
// /idea/pycharm/download/index.html |
// /idea/fabrique/download/index.html [RootNode] <- [/index.html] <- [/download] <- [/pycharm] <- [/idea]
// /idea/pycharm/documentation/index.html |
// |<------[/documentation] <- [/pycharm] <- [/idea]
public void addPath(T key, String path) {
path = trimStart(path, myRoot);
myPaths.put(key, path);
Node current = myRootNode;
Iterator<String> pathComponentsIterator = new PathComponentsIterator(path);
while (pathComponentsIterator.hasNext()) {
String word = pathComponentsIterator.next();
current = current.findOrAddChild(word);
}
for (Node c = current; c != null; c = c.myParentNode) {
++c.myNestedChildrenCount;
}
}
private static String trimStart(String s, String prefix) {
if (s.startsWith(prefix)) {
return s.substring(prefix.length());
}
return s;
}
public String getShortPath(T key) {
String path = myPaths.get(key);
if (path == null) {
return key.toString();
}
Node current = myRootNode;
Node firstNodeWithBranches = null;
Node firstNodeBeforeNodeWithBranches = null;
Node fileNameNode = null;
Iterator<String> pathComponentsIterator = new PathComponentsIterator(path);
while (pathComponentsIterator.hasNext()) {
String pathComponent = pathComponentsIterator.next();
current = current.findOrAddChild(pathComponent);
if (fileNameNode == null) {
fileNameNode = current;
}
if (firstNodeBeforeNodeWithBranches == null &&
firstNodeWithBranches != null &&
current.myChildren.size() <= 1) {
if (current.myParentNode.myNestedChildrenCount - current.myParentNode.myChildren.size()
< 1) {
firstNodeBeforeNodeWithBranches = current;
}
}
if (current.myChildren.size() != 1 && firstNodeWithBranches == null) {
firstNodeWithBranches = current;
}
}
StringBuilder b = new StringBuilder();
if (firstNodeBeforeNodeWithBranches == null) {
firstNodeBeforeNodeWithBranches = current;
}
boolean skipFirstSeparator = true;
for (Node c = firstNodeBeforeNodeWithBranches; c != myRootNode; c = c.myParentNode) {
if (c != fileNameNode && c != firstNodeBeforeNodeWithBranches
&& c.myParentNode.myChildren.size() == 1) {
b.append(mySeparator);
b.append("\u2026");
while (c.myParentNode != fileNameNode && c.myParentNode.myChildren.size() == 1) {
c = c.myParentNode;
}
} else {
if (c.myText.startsWith(VFS_SEPARATOR)) {
if (!skipFirstSeparator) {
b.append(mySeparator);
}
skipFirstSeparator = false;
b.append(c.myText, VFS_SEPARATOR.length(), c.myText.length());
} else {
b.append(c.myText);
}
}
}
return b.toString();
}
public String getSeparator() {
return mySeparator;
}
private static final class PathComponentsIterator implements Iterator<String> {
private final String myPath;
private int myLastPos;
private int mySeparatorPos;
PathComponentsIterator(String path) {
myPath = path;
myLastPos = path.length();
mySeparatorPos = path.lastIndexOf(VFS_SEPARATOR);
}
@Override
public boolean hasNext() {
return myLastPos != 0;
}
@Override
public String next() {
if (myLastPos == 0) {
throw new NoSuchElementException();
}
String pathComponent;
if (mySeparatorPos != -1) {
pathComponent = myPath.substring(mySeparatorPos, myLastPos);
myLastPos = mySeparatorPos;
mySeparatorPos = myPath.lastIndexOf(VFS_SEPARATOR, myLastPos - 1);
} else {
pathComponent = myPath.substring(0, myLastPos);
if (!pathComponent.startsWith(VFS_SEPARATOR)) {
pathComponent = VFS_SEPARATOR + pathComponent;
}
myLastPos = 0;
}
return pathComponent;
}
@Override
public void remove() {
Iterator.super.remove();
}
}
}
@@ -0,0 +1,53 @@
/*
* 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;
/**
* Utilities related to VM.
*
* @author Akash Yadav
*/
public class VMUtils {
private static Boolean isJvm = null;
/**
* @return <code>true</code> if the current platform is JVM, <code>false</code> otherwise.
*/
public static boolean isJvm() {
if (isJvm != null) {
return isJvm;
}
try {
// If we're in a testing environment
Class.forName("org.junit.runners.JUnit4");
return isJvm = true;
} catch (ClassNotFoundException e) {
// ignored
}
try {
Class.forName("android.content.Context");
return isJvm = false;
} catch (ClassNotFoundException e) {
return isJvm = true;
}
}
}
@@ -0,0 +1,27 @@
/*
* 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 androidx.collection.mutableIntObjectMapOf
/**
* Create an IntIntMap from a vararg of pairs.
*/
fun intByteMapOf(vararg pairs: Pair<Int, Byte>) = mutableIntObjectMapOf<Byte>().apply {
pairs.forEach { pair -> put(pair.first, pair.second) }
}
@@ -0,0 +1,23 @@
/*
* 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
@Suppress("UNCHECKED_CAST")
fun <T> uncheckedCast(obj: Any): T {
return obj as T
}
@@ -0,0 +1,50 @@
/*
* 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
/**
* Create a new [RecyclableObjectPool] with the given [capacity] and the object [factory].
*/
inline fun <reified T : RecyclableObjectPool.Recyclable> newRecyclableObjectPool(
capacity: Int = RecyclableObjectPool.CAPACITY_DEFAULT,
fillFirst: Int = 0,
metricsEnabled: Boolean = true,
factory: RecyclableObjectPool.Factory<T>
): RecyclableObjectPool<T> {
return RecyclableObjectPool(
capacity = capacity,
fillFirst = fillFirst,
metricsEnabled = metricsEnabled,
klass = T::class.java,
objFactory = factory
)
}
/**
* Recycle all the recyclable objects in this iterable.
*/
fun <T : RecyclableObjectPool.Recyclable> Iterable<T>.recycleAll() {
forEach { it.recycle() }
}
/**
* Recycle all the recyclable objects in this array.
*/
fun <T : RecyclableObjectPool.Recyclable> Array<T>.recycleAll() {
forEach { it.recycle() }
}
@@ -0,0 +1,30 @@
/*
* 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.util.Locale
/**
* Capitalizes the string.
*/
@JvmOverloads
fun String.capitalizeString(locale: Locale = Locale.getDefault()): String {
return replaceFirstChar { char ->
if (char.isLowerCase()) char.titlecase(locale) else char.toString()
}
}
@@ -0,0 +1,54 @@
/*
* 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 com.google.common.truth.Truth.assertThat
import org.junit.Test
import java.io.File
import java.nio.file.Files
class FileProviderTest {
@Test
fun testPath() {
val path = FileProvider.sourceFile("SourceFileTest")
assertThat(Files.exists(path)).isTrue()
assertThat(path.fileName.toString()).isEqualTo("SourceFileTest_template.java")
}
@Test
fun testNested() {
val path = FileProvider.sourceFile("package/SourceFileTest")
assertThat(Files.exists(path)).isTrue()
assertThat(path.fileName.toString()).isEqualTo("SourceFileTest_template.java")
}
@Test
fun testExtension() {
val folder = File(".").canonicalFile
assertThat(FileProvider.extension).isNotEmpty()
assertThat(FileProvider.extension)
.isEqualTo(
when (folder.name) {
"xml" -> "xml"
else -> "java"
}
)
}
}

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