mirror of
https://gitee.com/zhangsken/AndroidIDE.git
synced 2026-10-16 07:49:44 +08:00
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:
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -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@@";
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -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.
|
||||
```
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
+312
@@ -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)
|
||||
}
|
||||
}
|
||||
+200
@@ -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)
|
||||
}
|
||||
}
|
||||
+52
@@ -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)
|
||||
}
|
||||
+140
@@ -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
|
||||
}
|
||||
}
|
||||
+90
@@ -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>
|
||||
@@ -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"))
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+25
@@ -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 {}
|
||||
+110
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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/
|
||||
@@ -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
@@ -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>
|
||||
+36
@@ -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()
|
||||
}
|
||||
}
|
||||
+63
@@ -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
|
||||
}
|
||||
}
|
||||
+75
@@ -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) {}
|
||||
}
|
||||
+50
@@ -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) {
|
||||
}
|
||||
}
|
||||
+46
@@ -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) {}
|
||||
}
|
||||
+44
@@ -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
|
||||
}
|
||||
+52
@@ -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)
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.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
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.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)
|
||||
}
|
||||
}
|
||||
+50
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+70
@@ -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() ?: ""
|
||||
}
|
||||
}
|
||||
+83
@@ -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 }
|
||||
+44
@@ -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
|
||||
}
|
||||
+42
@@ -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
|
||||
}
|
||||
}
|
||||
+32
@@ -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)
|
||||
}
|
||||
}
|
||||
+79
@@ -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
|
||||
) {
|
||||
}
|
||||
}
|
||||
+50
@@ -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
|
||||
}
|
||||
}
|
||||
+113
@@ -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)
|
||||
}
|
||||
}
|
||||
+42
@@ -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)
|
||||
}
|
||||
}
|
||||
+177
@@ -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)
|
||||
}
|
||||
+98
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
+42
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
+47
@@ -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)
|
||||
}
|
||||
}
|
||||
+131
@@ -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)
|
||||
}
|
||||
}
|
||||
+27
@@ -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
|
||||
+46
@@ -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>
|
||||
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -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)
|
||||
}
|
||||
Vendored
+21
@@ -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()
|
||||
}
|
||||
+53
@@ -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>'\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<CodecSet> 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() + "]";
|
||||
}
|
||||
}
|
||||
+30
@@ -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
|
||||
}
|
||||
+50
@@ -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
Reference in New Issue
Block a user