diff --git a/.github/labeler.yml b/.github/labeler.yml
new file mode 100644
index 000000000..fcfdae9f4
--- /dev/null
+++ b/.github/labeler.yml
@@ -0,0 +1,36 @@
+# Add 'pluto-core' label to any changes within 'pluto' or 'plugin' module
+core:
+- pluto/**
+- pluto-plugins/base/**
+- pluto-plugins/bundle/**
+- .github/**
+
+# Add 'script' label to any changes within 'scripts' folder
+script:
+- script/**
+
+# Add 'sample' label to any changes within 'sample' module
+sample:
+- sample/**
+
+# labels for plugin changes
+datastore:
+- pluto-plugins/plugins/datastore/**
+
+exceptions:
+- pluto-plugins/plugins/exceptions/**
+
+layout-inspector:
+- pluto-plugins/plugins/layout-inspector/**
+
+logger:
+- pluto-plugins/plugins/logger/**
+
+network:
+- pluto-plugins/plugins/network/**
+
+rooms-database:
+- pluto-plugins/plugins/rooms-database/**
+
+shared-preferences:
+- pluto-plugins/plugins/shared-preferences/**
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index ef3af2b29..9be5c848d 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -14,12 +14,19 @@ jobs:
- name: Build
run: ./gradlew --no-daemon --max-workers 8 --build-cache assembleDebug
+ build-no-op:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - name: Build No Op
+ run: ./gradlew --no-daemon --max-workers 8 --build-cache assembleDebugNoOp
+
ktlint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Validating Ktlint
- run: ./gradlew --no-daemon --max-workers 8 --build-cache ktlintDebugCheck
+ run: ./gradlew --no-daemon --max-workers 8 --build-cache ktlintCheck
detekt:
runs-on: ubuntu-latest
diff --git a/.github/workflows/daily_builds.yml b/.github/workflows/daily_builds.yml
index c76fedd5d..47365034c 100644
--- a/.github/workflows/daily_builds.yml
+++ b/.github/workflows/daily_builds.yml
@@ -2,7 +2,8 @@ name: Daily Builds
on:
schedule:
- - cron: '0 20 * * *'
+ - cron: '0 20 * * *' # Runs daily at 20:00 UTC
+ workflow_dispatch: # Enables manual trigger
jobs:
build:
diff --git a/.github/workflows/pr_label.yml b/.github/workflows/pr_label.yml
new file mode 100644
index 000000000..461356907
--- /dev/null
+++ b/.github/workflows/pr_label.yml
@@ -0,0 +1,22 @@
+# This workflow will triage pull requests and apply a label based on the
+# paths that are modified in the pull request.
+#
+# To use this workflow, you will need to set up a .github/labeler.yml
+# file with configuration. For more information, see:
+# https://github.com/actions/labeler
+
+name: Labeler
+on: [pull_request_target]
+
+jobs:
+ label:
+
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ pull-requests: write
+
+ steps:
+ - uses: actions/labeler@v4
+ with:
+ repo-token: "${{ secrets.GITHUB_TOKEN }}"
diff --git a/.github/workflows/process_release.yml b/.github/workflows/process_release.yml
new file mode 100644
index 000000000..009109261
--- /dev/null
+++ b/.github/workflows/process_release.yml
@@ -0,0 +1,80 @@
+name: Release Workflow
+on:
+ release:
+ types:
+ - created
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v2
+
+ - name: Run on New Release
+ run: |
+ echo "A new release was created."
+ echo "Release name: ${{ github.event.release.name }}"
+ echo "Release tag: ${{ github.event.release.tag_name }}"
+
+ - name: Validating Release
+ run: |
+ pattern="^v[0-9]+(\.[0-9]+)+$"
+ if [[ ${{ github.event.release.name }} =~ $pattern ]]; then
+ echo "${{ github.event.release.name }} is valid"
+ else
+ echo "${{ github.event.release.name }} is not valid"
+ exit 1
+ fi
+
+ - name: Extract Version details
+ run: |
+ releaseName="${{ github.event.release.name }}"
+ echo "release name: $releaseName"
+ versionName=${releaseName:1}
+ echo "version name: $versionName"
+ IFS='.' read -ra versionStubs <<< "$versionName"
+ echo "versionMajor : ${versionStubs[0]}"
+ echo "versionMinor : ${versionStubs[1]}"
+ echo "versionPatch : ${versionStubs[2]}"
+
+ echo "versionMajor=${versionStubs[0]}" >> $GITHUB_ENV
+ echo "versionMinor=${versionStubs[1]}" >> $GITHUB_ENV
+ echo "versionPatch=${versionStubs[2]}" >> $GITHUB_ENV
+
+ - name: Commit version.properties changes
+ run: |
+ git fetch --all
+ git checkout develop
+ echo "major=${{ env.versionMajor }}" > version.properties
+ echo "minor=${{ env.versionMinor }}" >> version.properties
+ echo "patch=${{ env.versionPatch }}" >> version.properties
+ echo "channel=release" >> version.properties
+ echo "build=0" >> version.properties
+
+ git config --local user.email "${GITHUB_ACTOR}@users.noreply.github.com"
+ git config --local user.name "${GITHUB_ACTOR}"
+ git add version.properties
+
+ commitTitle="ci(release): version bump ${{ env.versionMajor }}.${{ env.versionMinor }}.${{ env.versionPatch }}"
+ commitBody="see release details https://github.com/androidPluto/pluto/releases/tag/${{ github.event.release.tag_name }}"
+ git commit -m "$commitTitle" -m "$commitBody"
+
+ - name: Publish artifacts to MavenCentral
+# run: ./gradlew publish
+ run: ./gradlew publishReleasePublicationToSonatypeRepository --max-workers 1 closeAndReleaseSonatypeStagingRepository
+ env:
+ OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }}
+ OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }}
+ SIGNING_KEY_ID: ${{ secrets.SIGNING_KEY_ID }}
+ SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }}
+ SIGNING_SECRET_KEY: ${{ secrets.SIGNING_SECRET_KEY }}
+ SONATYPE_STAGING_PROFILE_ID: ${{ secrets.SONATYPE_STAGING_PROFILE_ID }}
+
+ - name: Pushing changes to version.properties post deployment
+ run: |
+ git fetch --all
+ git checkout develop
+ git push
+
+
diff --git a/.gitignore b/.gitignore
index 8f8e39112..4b1eec440 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,24 +1,14 @@
*.iml
.gradle
-/local.properties
-/.idea/caches
-/.idea/libraries
-/.idea/modules.xml
-/.idea/workspace.xml
-/.idea/navEditor.xml
-/.idea/assetWizardSettings.xml
+*local.properties
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
-/.idea/.name
-/.idea/codeStyles/codeStyleConfig.xml
-/.idea/gradle.xml
-/.idea/misc.xml
-/.idea/codeStyles/Project.xml
-/.idea/runConfigurations.xml
-/.idea/vcs.xml
-/.idea/jarRepositories.xml
-/.idea/.gitignore
-/.idea/compiler.xml
+/.idea/*
+/scripts/publish/_credentials.properties
+/scripts/publish/_newCreds.properties
+*.gpg
+/buildSrc/build
+mavenCredentials.properties
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index b4a65bb61..0d37cc849 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,4 +1,4 @@
-Contributing
+
+
+
+
+**We're looking for contributors, help us improve Pluto.** π
+
+
+
+Hers's how you can help
+ - Look for issues marked as [`help wanted`](https://github.com/androidPluto/pluto/labels/help%20wanted).
+ - Submit a new Plugin, see the [guidelines](/SUBMIT_GUIDELINES.md).
+ - While submitting a new PR,
+ - make sure tests are all successful
+ - if you think we need any new test, feel free to add new tests
+ - please attach a reference video or image for the change
+
+### Prerequisite
+
+In order to start contributing to Pluto, you need to fork the project and open it in Android Studio/IntelliJ IDEA.
+
+Before committing we suggest you install the pre-commit hooks with the following command:
+```
+./gradlew installGitHook
+```
+
+This will make sure your code is validated against `ktLint` and `detekt` before every commit.
+The command will run automatically before the `clean` task, so you should have the pre-commit hook installed by then.
+
+Before submitting a PR please run:
+```
+./gradlew build
+```
+This will build the library and make sure your CI checks will pass.
+
+>
+> Before your code can be accepted into the project you must also sign the [Individual Contributor License Agreement (CLA)][1]. You will be prompted to sign the agreement once you raise the pull request.
+>
+
+[1]: https://cla-assistant.io/androidPluto/pluto
+
diff --git a/README.md b/README.md
index 271d5a2ce..b943aa90a 100644
--- a/README.md
+++ b/README.md
@@ -1,167 +1,131 @@
-# Pluto
-[](https://maven-badges.herokuapp.com/maven-central/com.mocklets/pluto)
-[](https://cla-assistant.io/mocklets/pluto)
-[](https://github.com/mocklets/pluto/actions/workflows/daily_builds.yml)
+# Android Pluto
-Pluto is a on-device debugger for Android applications, which helps in inspection of HTTP requests/responses, capture Crashes and ANRs and manipulating application data on-the-go.
+
+[](https://cla-assistant.io/androidPluto/pluto)
+[](https://github.com/androidPluto/pluto/actions/workflows/daily_builds.yml)
-It comes with a UI to monitor and share the information, as well as APIs to access and use that information in your application.
-
-
-
-
-
-
------
-
-### Pluto Dependency graph
-
-
-
-
+Pluto is an on-device debugging framework for Android applications, which helps in the inspection of HTTP requests/responses, captures Crashes, and ANRs, and manipulates application data on the go.
-***
+It comes with a UI to monitor and share the information, as well as APIs to access and use that information in your application.
+
-## Integrate Pluto in your application
+## π Integrate Pluto in your application
### Add Gradle Dependencies
-Pluto is distributed through [***mavenCentral***](https://search.maven.org/artifact/com.mocklets/pluto). To use it, you need to add the following Gradle dependency to your build.gradle file of you android app module.
-
-
+Pluto is distributed through [***mavenCentral***](https://central.sonatype.com/artifact/com.androidpluto/pluto). To use it, you need to add the following Gradle dependency to your build.gradle file of your app module.
-> Note: add both the pluto and the pluto-no-op variant to isolate Pluto from release builds.
+> Note: add both the `pluto` and the `pluto-no-op` variant to isolate Pluto from release builds.
```groovy
+def plutoVersion = "3.0.0"
+
dependencies {
- debugImplementation 'com.mocklets:pluto:LATEST_VERSION'
- releaseImplementation'com.mocklets:pluto-no-op:LATEST_VERSION'
+ ....
+ debugImplementation "com.androidpluto:pluto:$plutoVersion"
+ releaseImplementation "com.androidpluto:pluto-no-op:$plutoVersion"
+ ....
}
```
-### Intialize Pluto
+### Initialize Pluto
-Now to start using Pluto, intialize Pluto SDK from you application class by passing context to it.
+Now to start using Pluto, initialize Pluto SDK from you application class by passing context to it.
```kotlin
-Pluto.initialize(context)
+Pluto.Installer(this)
+ .addPlugin(....)
+ .install()
```
+### Install plugins
-### Add Pluto interceptor
-
-To debug HTTP requests/responses, plug the PlutoInterceptor in your OkHttp Client Builder
-```kotlin
-val client = OkHttpClient.Builder()
- .addInterceptor(PlutoInterceptor())
- .build()
-```
-
-### Set Global Exception Handler
+Unlike [version 1.x.x](https://github.com/androidPluto/pluto/wiki/Integrating-Pluto-1.x.x), Pluto now allows developers to add debuggers as plugin bundle or individual plugins based on their need.
-To intercept uncaught exceptions in your app, attach `UncaughtExceptionHandler` to Pluto
-```kotlin
-Pluto.setExceptionHandler { thread, throwable ->
- Log.d("Exception", "uncaught exception handled on thread: " + thread.name, throwable)
+Plugin bundle comes with all the basic plugins bundled together as single dependency.
+```groovy
+dependencies {
+ ....
+ debugImplementation "com.androidpluto.plugins:bundle-core:$plutoVersion"
+ releaseImplementation "com.androidpluto.plugins:bundle-core-no-op:$plutoVersion"
+ ....
}
```
+But, if you want to use individual plugins, here is the list of some plugins provided by us
-### Listen to ANRs
+- **[Network Plugin](pluto-plugins/plugins/network)**
+- **[Exceptions & Crashes Plugin](pluto-plugins/plugins/exceptions)**
+- **[Logger Plugin](pluto-plugins/plugins/logger)**
+- **[Shared Preferences Plugin](pluto-plugins/plugins/shared-preferences)**
+- **[Rooms Database Plugin](pluto-plugins/plugins/rooms-database)**
+- **[Datastore Preferences Plugin](pluto-plugins/plugins/datastore)**
+- **[Layout Inspector Plugin](pluto-plugins/plugins/layout-inspector)**
-Pluto can capture and store potential ANRs occurring in the app. You can also listen to these ANRs and report these to any Crash reporting tools like Firebase Crashlytics, Bugsnag, etc.
-```kotlin
-Pluto.setANRListener(object: ANRListener {
- override fun onAppNotResponding(exception: ANRException) {
- exception.printStackTrace()
- PlutoLog.e("ANR", exception.threadStateMap)
- }
-})
-```
+We will be adding more to the [list](https://central.sonatype.com/search?q=com.androidpluto.plugins). So please stay tuned.
+Please refer to their respective README for integration steps.
+
+> You can also help us expand the Pluto ecosystem now.
Pluto now allows to develop custom debuggers as plugin. Read [Develop Custom Plugins](https://github.com/androidPluto/pluto/wiki/Develop-Custom-Pluto-Plugins-(Beta)).
+
+π You are all set!
-### Add Pluto Logs
+Now re-build and run your app, you will receive a notification from Pluto, use it to access Pluto UI.
-Pluto allows you to log and persist the user journey through the app, and help debug them without any need to connect to Logcat.
-```kotlin
-PlutoLog.event("analytics", eventName, HashMap(attributes))
-PlutoLog.d("debug_log", "button clicked")
-PlutoLog.e("error_log", "api call falied with http_status 400")
-PlutoLog.w("warning_log", "warning log")
-PlutoLog.i("info_log", "api call completed")
-```
+
-But if you are connected to Logcat, PlutoLogs behave similar to Log class, with an improvement to tag the method and file name also. In Logcat, PlutoLogs will look like the following.
-```
-D/onClick(MainActivity.kt:40) | debug_log: button clicked
-E/onFailure(NetworkManager.kt:17) | error_log: api call falied with http_status 400
-```
+## Grouping Plugins *(Optional)*
+Pluto now allows to group similar plugins together to have better readability & categorization.
+
+To create a group, we need to override PluginGroup & attach Plugins to it. *(We have taken the example of grouping datasource plugins together)*
+```kotlin
+class DataSourcePluginGroup : PluginGroup("datasource-group") {
-### Set App Properties
+ override fun getConfig() = PluginGroupConfiguration(
+ name = "DataSource Group"
+ )
-Pluto allows storing information like App status(like app configurations), User properties(like email, profile) and Device fingerprint(like IMEI).
+ override fun getPlugins() = listOf(
+ PlutoSharePreferencesPlugin(),
+ PlutoDatastorePreferencesPlugin(),
+ PlutoRoomsDatabasePlugin()
+ )
+}
+```
-This data can later be accessed via Pluto debug UI. This method can be called multiple times and it will keep on appending the data.
+Then add the group to Plugin installer.
```kotlin
-Pluto.setAppProperties(hashMapOf(
- "User id" to "2whdue-dn4f-3hr-dfhrhs",
- "User email" to "john.smith@gmail.com"
-))
+Pluto.Installer(this)
+ .addPluginGroup(DataSourcePluginGroup())
+ .install()
```
-π You are all done!
+
-Now re-build and run your app, you will receive a notification from Pluto, use it to access Pluto UI.
-
-***
-
-
-## Contribution
+## π Contribution
**We're looking for contributors, help us improve Pluto.** π
-Hers's how you can help
- - Look for issues marked as [`help wanted`](https://github.com/mocklets/pluto/labels/help%20wanted)
- - While submitting a new PR, make sure tests are all successful. If you think we need any new test, feel free to add new tests.
-
-### Prerequisite
-
-In order to start working on Pluto, you need to fork the project and open it in Android Studio/IntelliJ IDEA.
-
-Before committing we suggest you install the pre-commit hooks with the following command:
-```
-./gradlew installGitHook
-```
-
-This will make sure your code is validated against `ktLint` and `detekt` before every commit.
-The command will run automatically before the `clean` task, so you should have the pre-commit hook installed by then.
-
-Before submitting a PR please run:
-```
-./gradlew build
-```
-This will build the library and make sure your CI checks will pass.
-
-***
-
-
-## Acknowledgments
+Please refer to your [`Contribution guidelines`](/CONTRIBUTING.md) to get started.
-Big thanks π to [ChuckerTeam/chucker](https://github.com/ChuckerTeam/chucker) for inspiration behind network interceptor code.
+
+Have an idea to improve Pluto? Let's connect on
+- [Twitter](https://twitter.com/srtv_prateek)
+- [Github](https://github.com/srtvprateek)
-***
+
-## License
+## π License
```
-Copyright 2021 Graylattice Communications Private Limited.
+Copyright 2021 Android Pluto.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
diff --git a/SUBMIT_GUIDELINES.md b/SUBMIT_GUIDELINES.md
new file mode 100644
index 000000000..d075a7cae
--- /dev/null
+++ b/SUBMIT_GUIDELINES.md
@@ -0,0 +1,36 @@
+## Submit a plugin to Pluto
+
+We are looking for contributors to add new & exciting Plugins for the Android developers out there. But at the same time we need to made the uniformity and standard through out the plugins.
+
+To start with a new plugins, start a discussion for [Plugin Suggestions](https://github.com/androidPluto/pluto/discussions/categories/plugin-suggestions). This will help you gather valuable feedback and insights even before putting any dev effort.
+
+Once you have enough information, lets jump into development.
+
+
+
+### π Develop
+
+- We have a uniform package name for all our plugins `com.pluto.plugins.`. Please make sure you follow this.
+- All the plugins need to accept `identifier` in `Plugin.kt` constructor. This helps developers to open the Plugin directly using the `identifier`.
+- In your plugin's *build.gradle*, mention `resourcePrefix 'pluto_PLUGIN-NAME___'`. This will make sure your resources are not being overridden by the parent apps.
+- Make sure you add both `lib` & `lib-no-op` variants.
+
+
+### π Publish
+
+- Make sure you use the same version logic as the whole project.
+- Setup your Maven configuration, like given below
+``` groovy
+ext {
+ PUBLISH_GROUP_ID = "com.androidpluto.plugins" // do not change this
+ PUBLISH_VERSION = verPublish // do not change this
+ PUBLISH_ARTIFACT_ID = 'PLUGIN_NAME'
+}
+```
+
+
+### π Get your plugin merged
+
+Once the development is done, its time to get your code merged. Open a Pull Request with label `submit plugin`.
+
+We will review it & if everything is good, it ll be merged to **develop** π.
diff --git a/build-utils.gradle b/build-utils.gradle
deleted file mode 100644
index 2526e743c..000000000
--- a/build-utils.gradle
+++ /dev/null
@@ -1,24 +0,0 @@
-ext.genVersion = {
- Properties versionProps = new Properties()
- versionProps.load(new FileInputStream(file("$project.rootDir/pluto-version.properties")))
-
- def versionMajor = versionProps['major'].toInteger()
- def versionMinor = versionProps['minor'].toInteger()
- def versionPatch = versionProps['patch'].toInteger()
- def versionBuild = versionProps['build'].toInteger()
- def versionChannel = versionProps['channel']
-
- def verCode = (versionMajor * 1000000) + (versionMinor * 10000) + (versionPatch * 100) + versionBuild
- def verName = "${versionMajor}.${versionMinor}.${versionPatch}.${versionBuild}"
- def verNameShort = "${versionMajor}.${versionMinor}.${versionPatch}"
- def verPublish = "${versionMajor}.${versionMinor}.${versionPatch}"
- if (versionChannel != "release") {
- verPublish = "${verPublish}-${versionChannel}"
- }
-
- return [verCode, verName, versionBuild, verNameShort, verPublish]
-}
-
-ext.gitSha ={
- return 'git rev-parse --short=10 HEAD'.execute().text.trim()
-}
diff --git a/build.gradle b/build.gradle
deleted file mode 100644
index 04d5de07e..000000000
--- a/build.gradle
+++ /dev/null
@@ -1,76 +0,0 @@
-// Top-level build file where you can add configuration options common to all sub-projects/modules.
-apply plugin: 'org.jetbrains.dokka'
-apply plugin: 'io.github.gradle-nexus.publish-plugin'
-
-buildscript {
- ext.kotlin_version = '1.5.20'
- ext.detekt_version = '1.13.1'
- repositories {
- maven { url "https://plugins.gradle.org/m2/" }
- google()
- jcenter()
- mavenCentral()
- }
- dependencies {
- classpath 'com.android.tools.build:gradle:4.2.2'
- classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
- classpath "com.github.dcendents:android-maven-gradle-plugin:2.1"
-
- // maven central dependencies
- classpath 'io.github.gradle-nexus:publish-plugin:1.1.0'
- classpath 'org.jetbrains.dokka:dokka-gradle-plugin:1.4.30'
-
- // ktlint & detekt dependencies
- classpath "org.jlleitschuh.gradle:ktlint-gradle:9.4.0"
- classpath "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:$detekt_version"
- }
-}
-
-allprojects {
- repositories {
- google()
- jcenter()
- }
-}
-
-subprojects {
- if (project.name != "pluto-no-op") {
- pluginManager.withPlugin('kotlin-android') {
- apply from: "$rootDir/static-analysis/code-analysis.gradle"
- }
- }
-}
-
-ext {
- minSdkVersion = 19
- targetSdkVersion = 29
- compileSdkVersion = 29
- buildToolsVersion = '29.0.3'
-
- androidXCoreVersion = '1.5.0'
- okhttpVersion = '3.14.4'
- roomsVersion = '2.3.0'
-}
-
-task installGitHook(type: Copy) {
- from new File(rootProject.rootDir, 'pre-commit')
- into { new File(rootProject.rootDir, '.git/hooks') }
- fileMode 0777
-}
-
-task clean(type: Delete) {
- dependsOn(installGitHook)
- delete rootProject.buildDir
-}
-
-tasks.withType(io.gitlab.arturbosch.detekt.Detekt).configureEach {
- exclude(".*/resources/.*,.*/build/.*")
-}
-
-task prCheck {
- dependsOn ':sample:assembleDebug'
- dependsOn ':pluto:validateChanges'
-}
-
-apply from: "${rootDir}/scripts/publish-root.gradle"
-apply from: "${rootDir}/scripts/project-dependancy-graph.gradle"
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
new file mode 100644
index 000000000..65c024c88
--- /dev/null
+++ b/build.gradle.kts
@@ -0,0 +1,56 @@
+import org.gradle.api.tasks.Copy
+import org.gradle.api.tasks.Delete
+import io.gitlab.arturbosch.detekt.Detekt
+
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+plugins {
+ alias(libs.plugins.dokka)
+ alias(libs.plugins.android.library) apply false
+ alias(libs.plugins.android.application) apply false
+ alias(libs.plugins.kotlin.android) apply false
+ alias(libs.plugins.kotlin.parcelize) apply false
+ alias(libs.plugins.ksp) apply false
+ alias(libs.plugins.detekt) apply false
+ alias(libs.plugins.ktlint) apply false
+}
+
+allprojects {
+ repositories {
+ google()
+ mavenCentral()
+ // for testing Pluto staged repository
+ maven("https://s01.oss.sonatype.org/content/groups/staging/")
+ }
+}
+
+subprojects {
+// if (project.name != "pluto-no-op") {
+ pluginManager.withPlugin("kotlin-android") {
+ apply (from = "$rootDir/scripts/static-analysis/code-analysis.gradle")
+ }
+// }
+}
+
+val installGitHook by tasks.registering(Copy::class) {
+ from(File(rootProject.rootDir, "pre-commit"))
+ into(File(rootProject.rootDir, ".git/hooks"))
+ fileMode = "0777".toInt(8)
+}
+
+val clean by tasks.registering(Delete::class) {
+ dependsOn(installGitHook)
+ delete(rootProject.buildDir)
+}
+
+tasks.withType().configureEach {
+ exclude(".*/resources/.*,.*/build/.*")
+}
+
+val prCheck by tasks.registering {
+ dependsOn(":sample:assembleDebug")
+ dependsOn(":pluto:validateChanges")
+}
+
+apply (from = "$rootDir/scripts/project-dependancy-graph.gradle")
+apply (from = "$rootDir/maven-versions.gradle.kts")
+apply(from = "$rootDir/publishTasks.gradle.kts")
\ No newline at end of file
diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts
new file mode 100644
index 000000000..b22ed732f
--- /dev/null
+++ b/buildSrc/build.gradle.kts
@@ -0,0 +1,7 @@
+plugins {
+ `kotlin-dsl`
+}
+
+repositories {
+ mavenCentral()
+}
\ No newline at end of file
diff --git a/buildSrc/src/main/kotlin/Versioning.kt b/buildSrc/src/main/kotlin/Versioning.kt
new file mode 100644
index 000000000..b62113ba3
--- /dev/null
+++ b/buildSrc/src/main/kotlin/Versioning.kt
@@ -0,0 +1,40 @@
+import java.io.File
+import java.util.Properties
+
+object Versioning {
+ fun loadVersioningData(): Map {
+ val version = mutableMapOf()
+
+ val versionProps = Properties().apply {
+ load(File("${System.getProperty("user.dir")}/version.properties").inputStream())
+ }
+
+ val versionMajor = versionProps["major"]?.toString()?.toInt() ?: 0
+ val versionMinor = versionProps["minor"]?.toString()?.toInt() ?: 0
+ val versionPatch = versionProps["patch"]?.toString()?.toInt() ?: 0
+ val versionBuild = versionProps["build"]?.toString()?.toInt() ?: 0
+ val versionChannel = versionProps["channel"]?.toString() ?: "release"
+
+ version["code"] = (versionMajor * 1_000_000) + (versionMinor * 10_000) + (versionPatch * 100) + versionBuild
+ version["name"] = "$versionMajor.$versionMinor.$versionPatch-rc$versionBuild"
+
+ var publishVersion = "$versionMajor.$versionMinor.$versionPatch"
+ if (versionChannel != "release") {
+ publishVersion = "$publishVersion-$versionChannel$versionBuild"
+ }
+ version["publish"] = publishVersion
+
+ val gitSha = "git rev-parse --short=10 HEAD".runCommand()?.trim() ?: ""
+ version["gitSha"] = gitSha
+
+ return version
+ }
+
+ private fun String.runCommand(): String? {
+ return try {
+ ProcessBuilder("/bin/sh", "-c", this).start().inputStream.bufferedReader().readText()
+ } catch (e: Exception) {
+ null
+ }
+ }
+}
diff --git a/gradle.properties b/gradle.properties
index 83df5e3a0..9b9c7086d 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,19 +1,36 @@
-# Project-wide Gradle settings.
-# IDE (e.g. Android Studio) users:
-# Gradle settings configured through the IDE *will override*
-# any settings specified in this file.
-# For more details on how to configure your build environment visit
+## For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
+#
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
-org.gradle.jvmargs=-Xmx1536m
+# Default value: -Xmx1024m -XX:MaxPermSize=256m
+# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
+#
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
-# AndroidX package structure to make it clearer which packages are bundled with the
-# Android operating system, and which are packaged with your app's APK
-# https://developer.android.com/topic/libraries/support-library/androidx-rn
+#Thu Sep 14 17:27:07 IST 2023
android.useAndroidX=true
-# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
+org.gradle.jvmargs=-Xmx2048M -Dkotlin.daemon.jvm.options\="-Xmx2048M"
+
+# Maven Central publish credentials
+mavenCentralUsername=XXXX
+mavenCentralPassword=XXXX
+signing.keyId=XXXX
+signing.password=XXXX
+signing.secretKeyRingFile=XXXX
+
+# Maven Central publish pom details
+pom.inceptionYear=2024
+pom.url=https://androidpluto.com
+pom.license.name=The Apache Software License, Version 2.0
+pom.license.url=http://www.apache.org/licenses/LICENSE-2.0.txt
+pom.developer.id=srtvprateek
+pom.developer.name=Prateek Srivastava
+pom.developer.email=srtv.prateek@gmail.com
+pom.scm.connection=https://github.com/androidPluto/pluto
+pom.scm.developerConnection=https://github.com/srtvprateek
+pom.scm.url=https://github.com/androidPluto/pluto
+
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
new file mode 100644
index 000000000..0f6a311e6
--- /dev/null
+++ b/gradle/libs.versions.toml
@@ -0,0 +1,94 @@
+[versions]
+
+testCore = "1.6.1"
+coreTesting = "2.2.0"
+java = "1.8"
+
+minSdk = "21"
+mockitoCore = "5.10.0"
+robolectric = "4.11.1"
+runner = "1.6.2"
+targetSdk = "32"
+compileSdk = "34"
+buildTools = "34.0.0"
+
+agp = "8.6.0"
+androidXCore = "1.6.0"
+androidXLifecycle = "2.8.7"
+detekt = "1.19.0"
+kotlin = "1.9.22"
+ktlint-plugin = "11.1.0"
+ktor = "2.3.2"
+ksp = "1.9.22-1.0.16"
+moshi = "1.15.1"
+navigation = "2.8.6"
+okhttp = "4.12.0"
+retrofit = "2.9.0"
+room = "2.5.1"
+mavenPublish = "0.28.0"
+
+[plugins]
+
+android-application = { id = "com.android.application", version.ref = "agp" }
+android-library = { id = "com.android.library", version.ref = "agp" }
+detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" }
+dokka = { id = "org.jetbrains.dokka", version = "1.5.0" }
+kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
+kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" }
+kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
+ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
+ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint-plugin" }
+maven-publish = { id = "com.vanniktech.maven.publish", version.ref = "mavenPublish"}
+
+[libraries]
+
+androidx-annotation = { module = "androidx.annotation:annotation", version = "1.4.0" }
+androidx-appcompat = { module = "androidx.appcompat:appcompat", version = "1.4.0" }
+androidx-browser = { module = "androidx.browser:browser", version = "1.4.0" }
+androidx-cardview = { module = "androidx.cardview:cardview", version = "1.0.0" }
+androidx-constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version = "2.1.2" }
+androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidXCore" }
+androidx-core-testing = { module = "androidx.arch.core:core-testing", version.ref = "coreTesting" }
+androidx-test-core = { module = "androidx.test:core", version.ref = "testCore" }
+androidx-lifecycle-common = { module = "androidx.lifecycle:lifecycle-common-java8", version.ref = "androidXLifecycle" }
+androidx-lifecycle-livedata = { module = "androidx.lifecycle:lifecycle-livedata-ktx", version.ref = "androidXLifecycle" }
+androidx-lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "androidXLifecycle" }
+androidx-lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "androidXLifecycle" }
+androidx-navigation-fragment = { module = "androidx.navigation:navigation-fragment-ktx", version.ref = "navigation" }
+androidx-navigation-ui = { module = "androidx.navigation:navigation-ui", version.ref = "navigation" }
+androidx-preference = { module = "androidx.preference:preference-ktx", version = "1.2.0" }
+androidx-recyclerview = { module = "androidx.recyclerview:recyclerview", version = "1.2.1" }
+androidx-runner = { module = "androidx.test:runner", version.ref = "runner" }
+androidx-swiperefreshlayout = { module = "androidx.swiperefreshlayout:swiperefreshlayout", version = "1.1.0" }
+
+datastore-preferences = { module = "androidx.datastore:datastore-preferences", version = "1.0.0" }
+
+google-material = { module = "com.google.android.material:material", version = "1.6.1" }
+
+junit = { module = "junit:junit", version = "4.13.2" }
+
+kotlin-stdlib-jdk8 = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin" }
+kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version = "1.6.0" }
+kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version = "1.5.1" }
+ktor-client-android = { module = "io.ktor:ktor-client-android", version.ref = "ktor" }
+ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" }
+ktor-client-core-jvm = { module = "io.ktor:ktor-client-core-jvm", version.ref = "ktor" }
+ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }
+
+leakcanary-android = { module = "com.squareup.leakcanary:leakcanary-android", version = "2.7" }
+
+mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockitoCore" }
+moshi = { module = "com.squareup.moshi:moshi", version.ref = "moshi" }
+moshi-codegen = { module = "com.squareup.moshi:moshi-kotlin-codegen", version.ref = "moshi" }
+
+okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
+okhttp-logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" }
+okio = { module = "com.squareup.okio:okio", version = "2.10.0" }
+
+retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
+retrofit-converter-moshi = { module = "com.squareup.retrofit2:converter-moshi", version.ref = "retrofit" }
+robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" }
+room = { module = "androidx.room:room-ktx", version.ref = "room" }
+room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
+
+timber = { module = "com.jakewharton.timber:timber", version = "5.0.1" }
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index c99d73470..2ec4e51cd 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
-#Fri Dec 25 12:26:30 IST 2020
+#Tue Feb 08 21:06:21 IST 2022
distributionBase=GRADLE_USER_HOME
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip
distributionPath=wrapper/dists
-zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip
+zipStoreBase=GRADLE_USER_HOME
diff --git a/maven-versions.gradle.kts b/maven-versions.gradle.kts
new file mode 100644
index 000000000..e4317b9b7
--- /dev/null
+++ b/maven-versions.gradle.kts
@@ -0,0 +1 @@
+extra["plutoVersion"] = "2.0.5"
\ No newline at end of file
diff --git a/pluto-no-op/build.gradle b/pluto-no-op/build.gradle
deleted file mode 100644
index 4682a1c0b..000000000
--- a/pluto-no-op/build.gradle
+++ /dev/null
@@ -1,51 +0,0 @@
-plugins {
- id 'com.android.library'
- id 'kotlin-android'
-}
-
-apply from: "$project.rootDir/build-utils.gradle"
-apply from: "${rootDir}/scripts/publish-module.gradle"
-
-def verCode, verName, verBuild, verNameShort, verPublish
-(verCode, verName, verBuild, verNameShort, verPublish) = genVersion()
-
-ext {
- PUBLISH_GROUP_ID = "com.mocklets"
- PUBLISH_VERSION = verPublish
- PUBLISH_ARTIFACT_ID = 'pluto-no-op'
-}
-
-android {
- compileSdkVersion rootProject.compileSdkVersion
- buildToolsVersion rootProject.buildToolsVersion
-
- defaultConfig {
- minSdkVersion rootProject.minSdkVersion
- targetSdkVersion rootProject.targetSdkVersion
- versionCode verCode
- versionName verName
- }
-
- buildTypes {
- release {
- minifyEnabled false
- }
- }
- compileOptions {
- sourceCompatibility JavaVersion.VERSION_1_8
- targetCompatibility JavaVersion.VERSION_1_8
- }
- kotlinOptions {
- jvmTarget = '1.6'
- }
-}
-
-dependencies {
- implementation "androidx.core:core-ktx:$androidXCoreVersion"
- implementation "com.squareup.okhttp3:okhttp:$okhttpVersion"
-}
-
-task buildAndUpload {
- dependsOn 'assemble'
- dependsOn 'bintrayUpload'
-}
\ No newline at end of file
diff --git a/pluto-no-op/src/main/AndroidManifest.xml b/pluto-no-op/src/main/AndroidManifest.xml
deleted file mode 100644
index 324c9d04c..000000000
--- a/pluto-no-op/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/pluto-no-op/src/main/java/com/mocklets/pluto/Pluto.kt b/pluto-no-op/src/main/java/com/mocklets/pluto/Pluto.kt
deleted file mode 100644
index 2649f165c..000000000
--- a/pluto-no-op/src/main/java/com/mocklets/pluto/Pluto.kt
+++ /dev/null
@@ -1,20 +0,0 @@
-package com.mocklets.pluto
-
-import android.content.Context
-import androidx.annotation.Keep
-import com.mocklets.pluto.modules.exceptions.ANRListener
-
-@Keep
-object Pluto {
-
- @JvmOverloads
- fun initialize(context: Context, shouldShowIntroToast: Boolean = true) {}
-
- fun setAppProperties(properties: HashMap) {}
-
- fun setExceptionHandler(uncaughtExceptionHandler: Thread.UncaughtExceptionHandler) {}
-
- fun setANRListener(listener: ANRListener) {}
-
- fun showUi() {}
-}
diff --git a/pluto-no-op/src/main/java/com/mocklets/pluto/PlutoInterceptor.kt b/pluto-no-op/src/main/java/com/mocklets/pluto/PlutoInterceptor.kt
deleted file mode 100644
index e136065bc..000000000
--- a/pluto-no-op/src/main/java/com/mocklets/pluto/PlutoInterceptor.kt
+++ /dev/null
@@ -1,14 +0,0 @@
-package com.mocklets.pluto
-
-import androidx.annotation.Keep
-import okhttp3.Interceptor
-import okhttp3.Response
-
-@Keep
-class PlutoInterceptor : Interceptor {
-
- override fun intercept(chain: Interceptor.Chain): Response {
- val request = chain.request()
- return chain.proceed(request)
- }
-}
diff --git a/pluto-no-op/src/main/java/com/mocklets/pluto/PlutoLog.kt b/pluto-no-op/src/main/java/com/mocklets/pluto/PlutoLog.kt
deleted file mode 100644
index 3caf7d037..000000000
--- a/pluto-no-op/src/main/java/com/mocklets/pluto/PlutoLog.kt
+++ /dev/null
@@ -1,27 +0,0 @@
-package com.mocklets.pluto
-
-import androidx.annotation.Keep
-
-@Keep
-class PlutoLog private constructor() {
-
- companion object {
- @JvmStatic
- fun v(tag: String, message: String?, tr: Throwable? = null) {}
-
- @JvmStatic
- fun d(tag: String, message: String?, tr: Throwable? = null) {}
-
- @JvmStatic
- fun i(tag: String, message: String?, tr: Throwable? = null) {}
-
- @JvmStatic
- fun w(tag: String, message: String?, tr: Throwable? = null) {}
-
- @JvmStatic
- fun e(tag: String, message: String?, tr: Throwable? = null) {}
-
- @JvmStatic
- fun event(tag: String, event: String, attributes: HashMap?) {}
- }
-}
diff --git a/pluto-no-op/src/main/java/com/mocklets/pluto/modules/exceptions/ANRException.kt b/pluto-no-op/src/main/java/com/mocklets/pluto/modules/exceptions/ANRException.kt
deleted file mode 100644
index ec6f92279..000000000
--- a/pluto-no-op/src/main/java/com/mocklets/pluto/modules/exceptions/ANRException.kt
+++ /dev/null
@@ -1,14 +0,0 @@
-package com.mocklets.pluto.modules.exceptions
-
-import androidx.annotation.Keep
-
-@Keep
-class ANRException(thread: Thread) : Exception("ANR detected in Pluto-No-Op") {
-
- val threadStateMap: String
-
- init {
- stackTrace = thread.stackTrace
- threadStateMap = ""
- }
-}
diff --git a/pluto-no-op/src/main/java/com/mocklets/pluto/modules/exceptions/ANRListener.kt b/pluto-no-op/src/main/java/com/mocklets/pluto/modules/exceptions/ANRListener.kt
deleted file mode 100644
index 30c4d19dd..000000000
--- a/pluto-no-op/src/main/java/com/mocklets/pluto/modules/exceptions/ANRListener.kt
+++ /dev/null
@@ -1,8 +0,0 @@
-package com.mocklets.pluto.modules.exceptions
-
-import androidx.annotation.Keep
-
-@Keep
-interface ANRListener {
- fun onAppNotResponding(exception: ANRException)
-}
diff --git a/pluto-no-op/.gitignore b/pluto-plugins/base/lib/.gitignore
similarity index 100%
rename from pluto-no-op/.gitignore
rename to pluto-plugins/base/lib/.gitignore
diff --git a/pluto-plugins/base/lib/build.gradle.kts b/pluto-plugins/base/lib/build.gradle.kts
new file mode 100644
index 000000000..f60cd8506
--- /dev/null
+++ b/pluto-plugins/base/lib/build.gradle.kts
@@ -0,0 +1,109 @@
+import com.vanniktech.maven.publish.SonatypeHost
+
+plugins {
+ alias(libs.plugins.android.library)
+ alias(libs.plugins.kotlin.android)
+ alias(libs.plugins.maven.publish)
+}
+
+val version = Versioning.loadVersioningData()
+val verCode = version["code"] as Int
+val verPublish = version["publish"] as String
+val verGitSHA = version["gitSha"] as String
+
+android {
+ namespace = "com.pluto.plugin"
+ resourcePrefix = "pluto___"
+ compileSdk = libs.versions.compileSdk.get().toInt()
+ buildToolsVersion = libs.versions.buildTools.get()
+
+ buildFeatures {
+ buildConfig = true
+ viewBinding = true
+ }
+
+ defaultConfig {
+ minSdk = libs.versions.minSdk.get().toInt()
+
+ buildConfigField("String", "VERSION_NAME", "\"$verPublish\"")
+ buildConfigField("long", "VERSION_CODE", "$verCode")
+ buildConfigField("String", "GIT_SHA", "\"$verGitSHA\"")
+ }
+
+ buildTypes {
+ getByName("release") {
+// isDebuggable = true
+ isMinifyEnabled = false
+ isShrinkResources = false
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.toVersion(libs.versions.java.get())
+ targetCompatibility = JavaVersion.toVersion(libs.versions.java.get())
+ }
+
+ kotlinOptions {
+ jvmTarget = libs.versions.java.get()
+ }
+
+ lint {
+ abortOnError = false
+ targetSdk = libs.versions.targetSdk.get().toInt()
+ }
+}
+
+extra["PUBLISH_GROUP_ID"] = "com.androidpluto"
+extra["PUBLISH_ARTIFACT_ID"] = "plugin"
+extra["PUBLISH_ARTIFACT_NAME"] = "Android Pluto Base Plugin Module"
+extra["PUBLISH_ARTIFACT_DESCRIPTION"] = "Open Sourced, on-device debugger for Android apps"
+
+mavenPublishing {
+ coordinates(
+ groupId = extra["PUBLISH_GROUP_ID"] as String,
+ artifactId = extra["PUBLISH_ARTIFACT_ID"] as String,
+ version = verPublish
+ )
+ pom {
+ name.set(extra["PUBLISH_ARTIFACT_NAME"] as String)
+ description.set(extra["PUBLISH_ARTIFACT_DESCRIPTION"] as String)
+ inceptionYear.set(project.findProperty("pom.inceptionYear") as? String)
+ url.set(project.findProperty("pom.url") as? String)
+ licenses {
+ license {
+ name.set(project.findProperty("pom.license.name") as? String)
+ url.set(project.findProperty("pom.license.url") as? String)
+ }
+ }
+ developers {
+ developer {
+ id.set(project.findProperty("pom.developer.id") as? String)
+ name.set(project.findProperty("pom.developer.name") as? String)
+ email.set(project.findProperty("pom.developer.email") as? String)
+ }
+ }
+ scm {
+ connection.set(project.findProperty("pom.scm.connection") as? String)
+ developerConnection.set(project.findProperty("pom.scm.developerConnection") as? String)
+ url.set(project.findProperty("pom.scm.url") as? String)
+ }
+ }
+ publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL)
+ signAllPublications()
+}
+
+dependencies {
+ implementation(libs.androidx.core)
+ implementation(libs.androidx.appcompat)
+
+ api(libs.androidx.constraintlayout)
+ api(libs.androidx.navigation.fragment)
+ api(libs.androidx.recyclerview)
+
+ api(libs.google.material)
+
+ api(libs.androidx.cardview)
+
+ api(libs.androidx.lifecycle.common)
+ api(libs.androidx.lifecycle.runtime)
+}
diff --git a/pluto-plugins/base/lib/src/main/AndroidManifest.xml b/pluto-plugins/base/lib/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..44008a433
--- /dev/null
+++ b/pluto-plugins/base/lib/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/pluto-plugins/base/lib/src/main/java/com/pluto/plugin/DataModel.kt b/pluto-plugins/base/lib/src/main/java/com/pluto/plugin/DataModel.kt
new file mode 100644
index 000000000..454625f16
--- /dev/null
+++ b/pluto-plugins/base/lib/src/main/java/com/pluto/plugin/DataModel.kt
@@ -0,0 +1,49 @@
+package com.pluto.plugin
+
+import androidx.annotation.DrawableRes
+
+/**
+ * Data class containing details about a plugin's developer.
+ *
+ * This class is used to provide information about the developer of a plugin,
+ * which can be displayed in the plugin's details screen.
+ *
+ * @property vcsLink Optional link to the version control system repository
+ * @property website Optional link to the developer's website
+ * @property twitter Optional link to the developer's Twitter profile
+ */
+data class DeveloperDetails(
+ val vcsLink: String? = null,
+ val website: String? = null,
+ val twitter: String? = null
+)
+
+/**
+ * Data class containing configuration for a plugin.
+ *
+ * This class defines the visual and metadata properties of a plugin,
+ * such as its name, icon, and version.
+ *
+ * @property name The display name of the plugin
+ * @property icon The resource ID of the plugin's icon
+ * @property version The version string of the plugin
+ */
+data class PluginConfiguration(
+ val name: String,
+ @DrawableRes val icon: Int = R.drawable.pluto___ic_plugin_placeholder_icon,
+ val version: String
+)
+
+/**
+ * Data class containing configuration for a plugin group.
+ *
+ * This class defines the visual properties of a plugin group,
+ * such as its name and icon.
+ *
+ * @property name The display name of the plugin group
+ * @property icon The resource ID of the plugin group's icon
+ */
+data class PluginGroupConfiguration(
+ val name: String,
+ @DrawableRes val icon: Int = R.drawable.pluto___ic_plugin_group_placeholder_icon,
+)
diff --git a/pluto-plugins/base/lib/src/main/java/com/pluto/plugin/Plugin.kt b/pluto-plugins/base/lib/src/main/java/com/pluto/plugin/Plugin.kt
new file mode 100644
index 000000000..cd3e6d801
--- /dev/null
+++ b/pluto-plugins/base/lib/src/main/java/com/pluto/plugin/Plugin.kt
@@ -0,0 +1,170 @@
+package com.pluto.plugin
+
+import android.app.Application
+import android.content.Context
+import android.os.Bundle
+import android.widget.Toast
+import android.widget.Toast.LENGTH_SHORT
+import androidx.annotation.Keep
+import androidx.fragment.app.Fragment
+
+/**
+ * Base class for all Pluto plugins.
+ *
+ * This abstract class provides the foundation for creating Pluto debugging plugins.
+ * It handles plugin lifecycle, configuration, and UI presentation.
+ *
+ * To create a new plugin, extend this class and implement the required abstract methods.
+ *
+ * Example:
+ * ```
+ * class NetworkPlugin : Plugin("network") {
+ * override fun getConfig() = PluginConfiguration(
+ * name = "Network",
+ * icon = R.drawable.ic_network,
+ * version = "1.0.0"
+ * )
+ *
+ * override fun getView() = NetworkFragment()
+ *
+ * override fun onPluginInstalled() {
+ * // Initialize plugin resources
+ * }
+ *
+ * override fun onPluginDataCleared() {
+ * // Clear plugin data
+ * }
+ * }
+ * ```
+ *
+ * @property identifier A unique string identifier for the plugin
+ */
+@Keep
+abstract class Plugin(val identifier: String) : PluginEntity(identifier) {
+
+ /**
+ * The application context.
+ *
+ * This property provides access to the application context for the plugin.
+ * It throws an IllegalStateException if accessed before the plugin is installed.
+ */
+ val context: Context
+ get() = returnContext()
+
+ /**
+ * The application instance.
+ *
+ * This property provides access to the application instance for the plugin.
+ * It throws an IllegalStateException if accessed before the plugin is installed.
+ */
+ val application: Application
+ get() = returnApplication()
+
+ /** The internal application instance, set during installation */
+ private var _application: Application? = null
+
+ /**
+ * Returns the application context.
+ *
+ * @throws IllegalStateException if the plugin is not installed
+ * @return The application context
+ */
+ private fun returnContext(): Context {
+ _application?.let {
+ return it.applicationContext
+ }
+ throw IllegalStateException("${this.javaClass.name} plugin is not installed yet.")
+ }
+
+ /**
+ * Returns the application instance.
+ *
+ * @throws IllegalStateException if the plugin is not installed
+ * @return The application instance
+ */
+ private fun returnApplication(): Application {
+ _application?.let {
+ return it
+ }
+ throw IllegalStateException("${this.javaClass.name} plugin is not installed yet.")
+ }
+
+ /**
+ * Bundle for saving instance state.
+ *
+ * This bundle can be used to save and restore the plugin's state.
+ */
+ var savedInstance: Bundle = Bundle()
+ private set
+
+ /**
+ * Installs the plugin with the provided application instance.
+ *
+ * This method is final and cannot be overridden. It sets the application
+ * instance and calls onPluginInstalled().
+ *
+ * @param application The application instance to use for installation
+ */
+ final override fun install(application: Application) {
+ this._application = application
+ onPluginInstalled()
+ }
+
+ /**
+ * Returns the plugin configuration.
+ *
+ * This method should provide a PluginConfiguration object that defines
+ * the plugin's name, icon, and version.
+ *
+ * @return The plugin configuration
+ */
+ abstract fun getConfig(): PluginConfiguration
+
+ /**
+ * Returns the plugin's UI view.
+ *
+ * This method should provide a Fragment that implements the plugin's UI.
+ *
+ * @return The plugin's UI fragment
+ */
+ abstract fun getView(): Fragment
+
+ /**
+ * Returns the plugin developer's details.
+ *
+ * This method can be overridden to provide information about the plugin's
+ * developer, such as VCS link, website, and Twitter handle.
+ *
+ * @return The developer details, or null if not provided
+ */
+ open fun getDeveloperDetails(): DeveloperDetails? = null
+
+ /**
+ * Called when the plugin is installed.
+ *
+ * This method is called during the plugin installation process.
+ * It should be used to initialize any resources needed by the plugin.
+ */
+ abstract fun onPluginInstalled()
+
+ /**
+ * Called when the plugin's data should be cleared.
+ *
+ * This method is called when the user requests to clear the plugin's data.
+ * It should be used to clear any cached data or logs.
+ */
+ abstract fun onPluginDataCleared()
+
+ /**
+ * Called when the plugin's view is created.
+ *
+ * This method is called when the plugin's UI is created.
+ * It shows a toast message indicating that the view has switched to this plugin.
+ *
+ * @param savedInstanceState The saved instance state bundle
+ */
+ @SuppressWarnings("UnusedPrivateMember")
+ fun onPluginViewCreated(savedInstanceState: Bundle?) {
+ Toast.makeText(context, "View switched to ${getConfig().name}", LENGTH_SHORT).show()
+ }
+}
diff --git a/pluto-plugins/base/lib/src/main/java/com/pluto/plugin/PluginEntity.kt b/pluto-plugins/base/lib/src/main/java/com/pluto/plugin/PluginEntity.kt
new file mode 100644
index 000000000..5f517cead
--- /dev/null
+++ b/pluto-plugins/base/lib/src/main/java/com/pluto/plugin/PluginEntity.kt
@@ -0,0 +1,42 @@
+package com.pluto.plugin
+
+import android.app.Application
+
+/**
+ * Base class for all Pluto plugin entities.
+ *
+ * This abstract class serves as the foundation for both individual plugins and plugin groups.
+ * It provides a common interface for installation and identity management.
+ *
+ * @property identifier A unique string identifier for the plugin entity
+ */
+abstract class PluginEntity(private val identifier: String) {
+
+ /**
+ * Installs the plugin entity with the provided application instance.
+ *
+ * This method is called during Pluto initialization to set up the plugin.
+ *
+ * @param application The application instance to use for installation
+ */
+ abstract fun install(application: Application)
+
+ /**
+ * Compares this plugin entity with another object for equality.
+ *
+ * Plugin entities are considered equal if they have the same identifier.
+ *
+ * @param other The object to compare with
+ * @return True if the objects are equal, false otherwise
+ */
+ override fun equals(other: Any?): Boolean = other is PluginEntity && identifier == other.identifier
+
+ /**
+ * Returns a hash code value for this plugin entity.
+ *
+ * The hash code is based on the identifier to ensure consistency with equals.
+ *
+ * @return The hash code value
+ */
+ override fun hashCode(): Int = identifier.hashCode()
+}
diff --git a/pluto-plugins/base/lib/src/main/java/com/pluto/plugin/PluginGroup.kt b/pluto-plugins/base/lib/src/main/java/com/pluto/plugin/PluginGroup.kt
new file mode 100644
index 000000000..b721cb289
--- /dev/null
+++ b/pluto-plugins/base/lib/src/main/java/com/pluto/plugin/PluginGroup.kt
@@ -0,0 +1,82 @@
+package com.pluto.plugin
+
+import android.app.Application
+
+/**
+ * Base class for grouping related Pluto plugins.
+ *
+ * This abstract class allows multiple plugins to be grouped together under a
+ * common identifier. Plugin groups are useful for organizing related plugins,
+ * such as database inspection tools or network monitoring utilities.
+ *
+ * To create a new plugin group, extend this class and implement the required
+ * abstract methods.
+ *
+ * Example:
+ * ```
+ * class DatabasePluginGroup : PluginGroup("database") {
+ * override fun getConfig() = PluginGroupConfiguration(
+ * name = "Database Tools"
+ * )
+ *
+ * override fun getPlugins() = listOf(
+ * RoomDatabasePlugin(),
+ * SharedPreferencesPlugin()
+ * )
+ * }
+ * ```
+ *
+ * @param identifier A unique string identifier for the plugin group
+ */
+abstract class PluginGroup(identifier: String) : PluginEntity(identifier) {
+
+ /** Set of installed plugins in this group */
+ private var plugins: LinkedHashSet = linkedSetOf()
+
+ /**
+ * List of all installed plugins in this group.
+ *
+ * This property returns a copy of the internal plugins set as a list,
+ * ensuring that the original set cannot be modified externally.
+ */
+ val installedPlugins: List
+ get() {
+ val list = arrayListOf()
+ list.addAll(plugins)
+ return list
+ }
+
+ /**
+ * Returns the plugin group configuration.
+ *
+ * This method should provide a PluginGroupConfiguration object that defines
+ * the group's name and icon.
+ *
+ * @return The plugin group configuration
+ */
+ abstract fun getConfig(): PluginGroupConfiguration
+
+ /**
+ * Returns the list of plugins in this group.
+ *
+ * This method should provide a list of all plugins that belong to this group.
+ *
+ * @return The list of plugins in this group
+ */
+ protected abstract fun getPlugins(): List
+
+ /**
+ * Installs all plugins in this group with the provided application instance.
+ *
+ * This method is final and cannot be overridden. It installs each plugin
+ * in the group and adds it to the internal registry of plugins.
+ *
+ * @param application The application instance to use for installation
+ */
+ final override fun install(application: Application) {
+ getPlugins().forEach {
+ it.install(application)
+ plugins.add(it)
+ }
+ }
+}
diff --git a/pluto-plugins/base/lib/src/main/java/com/pluto/plugin/libinterface/FilesInterface.kt b/pluto-plugins/base/lib/src/main/java/com/pluto/plugin/libinterface/FilesInterface.kt
new file mode 100644
index 000000000..c13e2e180
--- /dev/null
+++ b/pluto-plugins/base/lib/src/main/java/com/pluto/plugin/libinterface/FilesInterface.kt
@@ -0,0 +1,26 @@
+package com.pluto.plugin.libinterface
+
+import android.app.Application
+import java.io.File
+import java.io.IOException
+import java.util.concurrent.atomic.AtomicLong
+
+class FilesInterface(application: Application) {
+ private val uniqueIdGenerator = AtomicLong()
+ private val directory: File = application.applicationContext.filesDir
+
+ fun createFile(filename: String = "pluto-${uniqueIdGenerator.getAndIncrement()}"): File? = try {
+ File(directory, filename).apply {
+ if (exists() && !delete()) {
+ throw IOException("Failed to delete file $this")
+ }
+ parentFile?.mkdirs()
+ if (!createNewFile()) {
+ throw IOException("File $this already exists")
+ }
+ }
+ } catch (e: IOException) {
+ IOException("An error occurred while creating a Pluto file", e).printStackTrace()
+ null
+ }
+}
diff --git a/pluto-plugins/base/lib/src/main/java/com/pluto/plugin/libinterface/LibraryInfoInterface.kt b/pluto-plugins/base/lib/src/main/java/com/pluto/plugin/libinterface/LibraryInfoInterface.kt
new file mode 100644
index 000000000..f8e29dce8
--- /dev/null
+++ b/pluto-plugins/base/lib/src/main/java/com/pluto/plugin/libinterface/LibraryInfoInterface.kt
@@ -0,0 +1,5 @@
+package com.pluto.plugin.libinterface
+
+import androidx.fragment.app.FragmentActivity
+
+data class LibraryInfoInterface(val pluginActivityClass: Class, val selectorActivityClass: Class)
diff --git a/pluto-plugins/base/lib/src/main/java/com/pluto/plugin/libinterface/NotificationInterface.kt b/pluto-plugins/base/lib/src/main/java/com/pluto/plugin/libinterface/NotificationInterface.kt
new file mode 100644
index 000000000..3bfc0b7fe
--- /dev/null
+++ b/pluto-plugins/base/lib/src/main/java/com/pluto/plugin/libinterface/NotificationInterface.kt
@@ -0,0 +1,28 @@
+package com.pluto.plugin.libinterface
+
+import android.app.Application
+import android.app.PendingIntent
+import android.content.Intent
+import android.os.Build
+import android.os.Bundle
+import androidx.fragment.app.FragmentActivity
+
+class NotificationInterface(private val application: Application, private val pluginActivityClass: Class) {
+
+ fun getPendingIntent(identifier: String, bundle: Bundle? = null): PendingIntent {
+ val notificationIntent = Intent(application.applicationContext, pluginActivityClass)
+ .putExtra(ID_LABEL, identifier)
+ bundle?.let { notificationIntent.putExtra(BUNDLE_LABEL, it) }
+ notificationIntent.action = System.currentTimeMillis().toString()
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ PendingIntent.getActivity(application.applicationContext, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT)
+ } else {
+ PendingIntent.getActivity(application.applicationContext, 0, notificationIntent, 0)
+ }
+ }
+
+ companion object {
+ const val ID_LABEL = "pluginIdentifier"
+ const val BUNDLE_LABEL = "pluginBundle"
+ }
+}
diff --git a/pluto-plugins/base/lib/src/main/java/com/pluto/plugin/libinterface/PlutoInterface.kt b/pluto-plugins/base/lib/src/main/java/com/pluto/plugin/libinterface/PlutoInterface.kt
new file mode 100644
index 000000000..467cd3f05
--- /dev/null
+++ b/pluto-plugins/base/lib/src/main/java/com/pluto/plugin/libinterface/PlutoInterface.kt
@@ -0,0 +1,47 @@
+package com.pluto.plugin.libinterface
+
+import android.app.Application
+import androidx.fragment.app.FragmentActivity
+import java.lang.IllegalStateException
+
+class PlutoInterface private constructor(
+ val application: Application,
+ val pluginActivityClass: Class,
+ val selectorActivityClass: Class
+) {
+
+ companion object {
+ private var instance: PlutoInterface? = null
+
+ private val get: PlutoInterface
+ get() {
+ if (instance != null) {
+ return instance!!
+ }
+ throw IllegalStateException("PluginUiBridge not initialised yet")
+ }
+
+ val notification: NotificationInterface
+ get() = NotificationInterface(get.application, get.pluginActivityClass)
+
+ val libInfo: LibraryInfoInterface
+ get() = LibraryInfoInterface(get.pluginActivityClass, get.selectorActivityClass)
+
+ val files: FilesInterface by lazy { FilesInterface(get.application) } // singleton
+
+ fun create(
+ application: Application,
+ pluginActivityClass: Class,
+ selectorActivityClass: Class
+ ): PlutoInterface? {
+ if (instance == null) {
+ synchronized(PlutoInterface::class.java) {
+ if (instance == null) {
+ instance = PlutoInterface(application, pluginActivityClass, selectorActivityClass)
+ }
+ }
+ }
+ return instance
+ }
+ }
+}
diff --git a/pluto-plugins/base/lib/src/main/java/com/pluto/plugin/settings/SettingsPreferences.kt b/pluto-plugins/base/lib/src/main/java/com/pluto/plugin/settings/SettingsPreferences.kt
new file mode 100644
index 000000000..0e34b0da4
--- /dev/null
+++ b/pluto-plugins/base/lib/src/main/java/com/pluto/plugin/settings/SettingsPreferences.kt
@@ -0,0 +1,37 @@
+package com.pluto.plugin.settings
+
+import android.content.Context
+import android.content.SharedPreferences
+
+object SettingsPreferences {
+
+ private val settingsPrefs: SharedPreferences
+ get() = returnSettings()
+ private var _settingsPrefs: SharedPreferences? = null
+ private fun returnSettings(): SharedPreferences {
+ _settingsPrefs?.let { return it }
+ throw IllegalStateException("Settings preferences is not initialised yet.")
+ }
+
+ fun init(context: Context) {
+ _settingsPrefs = context.preferences("_pluto_pref_lib_settings")
+ }
+
+ var isDarkThemeEnabled: Boolean
+ get() = settingsPrefs.getBoolean(IS_DARK_THEME_ENABLED, true)
+ set(value) = settingsPrefs.edit().putBoolean(IS_DARK_THEME_ENABLED, value).apply()
+
+ var isRightHandedAccessPopup: Boolean
+ get() = settingsPrefs.getBoolean(IS_RIGHT_HANDED_ACCESS_POPUP, true)
+ set(value) = settingsPrefs.edit().putBoolean(IS_RIGHT_HANDED_ACCESS_POPUP, value).apply()
+
+ var gridSize: Int
+ get() = settingsPrefs.getInt(GRID_SIZE, 5)
+ set(value) = settingsPrefs.edit().putInt(GRID_SIZE, value).apply()
+
+ private const val IS_DARK_THEME_ENABLED = "is_dark_theme_enabled"
+ private const val IS_RIGHT_HANDED_ACCESS_POPUP = "is_right_handed_access_popup"
+ private const val GRID_SIZE = "grid_size"
+}
+
+private fun Context.preferences(name: String, mode: Int = Context.MODE_PRIVATE) = getSharedPreferences(name, mode)
diff --git a/pluto-plugins/base/lib/src/main/java/com/pluto/plugin/share/ContentShare.kt b/pluto-plugins/base/lib/src/main/java/com/pluto/plugin/share/ContentShare.kt
new file mode 100644
index 000000000..6e1f67a3f
--- /dev/null
+++ b/pluto-plugins/base/lib/src/main/java/com/pluto/plugin/share/ContentShare.kt
@@ -0,0 +1,27 @@
+package com.pluto.plugin.share
+
+import androidx.activity.ComponentActivity
+import androidx.activity.viewModels
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+
+fun Fragment.lazyContentSharer(): Lazy = activityViewModels()
+
+fun ComponentActivity.lazyContentSharer(): Lazy = viewModels()
+
+data class Shareable(
+ val title: String,
+ val content: String,
+ val fileName: String = "File shared from Pluto"
+) {
+ val processedContent: String = StringBuilder().apply {
+ append(content)
+ append("\n\n-------------------------------------\nreport generated by Pluto (https://androidpluto.com)")
+ }.toString()
+}
+
+sealed class ShareAction {
+ data class ShareAsText(val shareable: Shareable) : ShareAction()
+ data class ShareAsFile(val shareable: Shareable, val type: String = "text/*") : ShareAction()
+ data class ShareAsCopy(val shareable: Shareable) : ShareAction()
+}
diff --git a/pluto-plugins/base/lib/src/main/java/com/pluto/plugin/share/ContentShareViewModel.kt b/pluto-plugins/base/lib/src/main/java/com/pluto/plugin/share/ContentShareViewModel.kt
new file mode 100644
index 000000000..367c559bf
--- /dev/null
+++ b/pluto-plugins/base/lib/src/main/java/com/pluto/plugin/share/ContentShareViewModel.kt
@@ -0,0 +1,24 @@
+package com.pluto.plugin.share
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.ViewModel
+import com.pluto.utilities.SingleLiveEvent
+
+class ContentShareViewModel : ViewModel() {
+
+ val data: LiveData
+ get() = _data
+ private val _data = SingleLiveEvent()
+
+ val action: LiveData
+ get() = _action
+ private val _action = SingleLiveEvent()
+
+ fun performAction(action: ShareAction) {
+ _action.postValue(action)
+ }
+
+ fun share(shareable: Shareable) {
+ _data.postValue(shareable)
+ }
+}
diff --git a/pluto-plugins/base/lib/src/main/java/com/pluto/plugin/share/ShareFragment.kt b/pluto-plugins/base/lib/src/main/java/com/pluto/plugin/share/ShareFragment.kt
new file mode 100644
index 000000000..b256e71d5
--- /dev/null
+++ b/pluto-plugins/base/lib/src/main/java/com/pluto/plugin/share/ShareFragment.kt
@@ -0,0 +1,70 @@
+package com.pluto.plugin.share
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.activityViewModels
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment
+import com.pluto.plugin.R
+import com.pluto.plugin.databinding.PlutoFragmentShareBinding
+import com.pluto.utilities.setOnDebounceClickListener
+import com.pluto.utilities.viewBinding
+
+class ShareFragment : BottomSheetDialogFragment() {
+
+ private val binding by viewBinding(PlutoFragmentShareBinding::bind)
+ private val shareViewModel: ContentShareViewModel by activityViewModels()
+ private var shareContent: Shareable? = null
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
+ inflater.inflate(R.layout.pluto___fragment_share, container, false)
+
+ override fun getTheme(): Int = R.style.PlutoBottomSheetDialogTheme
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ shareContent = arguments?.toShareable()
+
+ binding.title.text = shareContent?.title ?: getString(R.string.pluto___share_as)
+
+ binding.shareCopy.setOnDebounceClickListener {
+ shareContent?.let {
+ shareViewModel.performAction(ShareAction.ShareAsCopy(it))
+ }
+ dismiss()
+ }
+
+ binding.shareText.setOnDebounceClickListener {
+ shareContent?.let {
+ shareViewModel.performAction(ShareAction.ShareAsText(it))
+ }
+ dismiss()
+ }
+
+ binding.shareFile.setOnDebounceClickListener {
+ shareContent?.let {
+ shareViewModel.performAction(ShareAction.ShareAsFile(it))
+ }
+ dismiss()
+ }
+ }
+}
+
+private fun Bundle?.toShareable(): Shareable? {
+ this?.let {
+ return if (it.getString("title") != null &&
+ it.getString("content") != null &&
+ it.getString("fileName") != null
+ ) {
+ Shareable(
+ title = it.getString("title") ?: "",
+ content = it.getString("content") ?: "",
+ fileName = it.getString("fileName") ?: "",
+ )
+ } else {
+ null
+ }
+ }
+ return null
+}
diff --git a/pluto-plugins/base/lib/src/main/java/com/pluto/plugin/share/ShareKtx.kt b/pluto-plugins/base/lib/src/main/java/com/pluto/plugin/share/ShareKtx.kt
new file mode 100644
index 000000000..59a2eb61d
--- /dev/null
+++ b/pluto-plugins/base/lib/src/main/java/com/pluto/plugin/share/ShareKtx.kt
@@ -0,0 +1,75 @@
+package com.pluto.plugin.share
+
+import android.content.ClipData
+import android.content.ClipboardManager
+import android.content.Context
+import android.content.Intent
+import android.os.Environment
+import androidx.core.content.FileProvider
+import com.pluto.utilities.DebugLog
+import java.io.File
+import java.io.FileOutputStream
+
+fun Context.share(message: String, title: String? = null, subject: String? = null) {
+ val intent = Intent().apply {
+ type = "text/plain"
+ putExtra(Intent.EXTRA_SUBJECT, subject ?: "")
+ putExtra(Intent.EXTRA_TEXT, message)
+ action = Intent.ACTION_SEND
+ }
+ startActivity(Intent.createChooser(intent, title ?: "Share via..."))
+}
+
+fun Context.shareFile(message: String, title: String? = null, fileName: String, contentType: String) {
+ val dir = getDirectoryName()
+ val file = generateFile(message, dir, contentType)
+ val uri = FileProvider.getUriForFile(applicationContext, "pluto___${applicationContext.packageName}.provider", file)
+ val intent = Intent().apply {
+ type = contentType
+
+ addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ putExtra(Intent.EXTRA_TEXT, fileName)
+ putExtra(Intent.EXTRA_STREAM, uri)
+ action = Intent.ACTION_SEND
+ }
+ startActivity(Intent.createChooser(intent, title ?: "Share via..."))
+}
+
+fun Context.copyToClipboard(data: String, label: String) {
+ val clipboard: ClipboardManager? = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager?
+ val clip = ClipData.newPlainText(label, data)
+ clipboard?.setPrimaryClip(clip)
+}
+
+private fun Context.getDirectoryName(): File {
+ val dir = File(getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS), "PlutoShares")
+ if (!dir.exists()) {
+ if (dir.mkdir()) {
+ DebugLog.d("Create Directory", "Directory Created : $dir")
+ }
+ }
+ return dir
+}
+
+@Suppress("TooGenericExceptionCaught")
+private fun generateFile(content: String, saveFilePath: File, contentType: String): File {
+ val dir = File(saveFilePath.absolutePath)
+ if (!dir.exists()) {
+ dir.mkdirs()
+ }
+ val file = File(saveFilePath.absolutePath, "pluto_share.${getFileExtension(contentType)}")
+ try {
+ val fOut = FileOutputStream(file)
+ fOut.write(content.toByteArray())
+ fOut.flush()
+ fOut.close()
+ } catch (e: Exception) {
+ DebugLog.e("share", "error while generating file", e)
+ }
+ return file
+}
+
+private fun getFileExtension(contentType: String): String = when (contentType) {
+ "text/csv" -> "csv"
+ else -> "txt"
+}
diff --git a/pluto-plugins/base/lib/src/main/java/com/pluto/plugin/share/csv/CSVFormatter.kt b/pluto-plugins/base/lib/src/main/java/com/pluto/plugin/share/csv/CSVFormatter.kt
new file mode 100644
index 000000000..0a9b32758
--- /dev/null
+++ b/pluto-plugins/base/lib/src/main/java/com/pluto/plugin/share/csv/CSVFormatter.kt
@@ -0,0 +1,31 @@
+package com.pluto.plugin.share.csv
+
+class CSVFormatter private constructor() {
+
+ companion object {
+ fun write(nextLine: Array): String {
+ val sb = StringBuffer()
+ for (i in nextLine.indices) {
+ if (i != 0) {
+ sb.append(SEPARATOR)
+ }
+ val nextElement = nextLine[i] ?: continue
+ sb.append(QUOTE_CHARACTER)
+ for (element in nextElement) {
+ when (element) {
+ ESCAPE_OR_QUOTE_CHARACTER -> sb.append(ESCAPE_OR_QUOTE_CHARACTER).append(element)
+ else -> sb.append(element)
+ }
+ }
+ sb.append(QUOTE_CHARACTER)
+ }
+ sb.append(NEW_LINE)
+ return sb.toString()
+ }
+
+ private const val ESCAPE_OR_QUOTE_CHARACTER = '"'
+ private const val SEPARATOR = ','
+ private const val QUOTE_CHARACTER = '"'
+ private const val NEW_LINE = "\n"
+ }
+}
diff --git a/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/AutoClearedValue.kt b/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/AutoClearedValue.kt
new file mode 100644
index 000000000..5334c88f3
--- /dev/null
+++ b/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/AutoClearedValue.kt
@@ -0,0 +1,92 @@
+/**
+ * Source: https://gist.github.com/Zhuinden/ea3189198938bd16c03db628e084a4fa#file-fragmentviewbindingdelegate-kt
+ */
+
+// https://github.com/Zhuinden/fragmentviewbindingdelegate-kt
+package com.pluto.utilities
+
+import android.view.View
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.Observer
+import androidx.viewbinding.ViewBinding
+import kotlin.properties.ReadOnlyProperty
+import kotlin.properties.ReadWriteProperty
+import kotlin.reflect.KProperty
+
+private open class AutoClearedValue(protected val fragment: Fragment) :
+ ReadWriteProperty {
+ protected var value: T? = null
+
+ init {
+ fragment.lifecycle.addObserver(object : DefaultLifecycleObserver {
+ val viewLifecycleOwnerLiveDataObserver = Observer {
+ it?.lifecycle?.addObserver(object : DefaultLifecycleObserver {
+ override fun onDestroy(owner: LifecycleOwner) {
+ value = null
+ }
+ })
+ }
+
+ override fun onCreate(owner: LifecycleOwner) {
+ fragment.viewLifecycleOwnerLiveData.observeForever(
+ viewLifecycleOwnerLiveDataObserver
+ )
+ }
+
+ override fun onDestroy(owner: LifecycleOwner) {
+ fragment.viewLifecycleOwnerLiveData.removeObserver(
+ viewLifecycleOwnerLiveDataObserver
+ )
+ }
+ })
+ }
+
+ override fun getValue(thisRef: Fragment, property: KProperty<*>): T {
+ return value
+ ?: throw IllegalStateException("should never call auto-cleared-value get when it might not be available")
+ }
+
+ override fun setValue(thisRef: Fragment, property: KProperty<*>, value: T) {
+ this.value = value
+ }
+}
+
+private abstract class BaseAutoClearedInitializedValue(fragment: Fragment) :
+ AutoClearedValue(fragment) {
+ override fun getValue(thisRef: Fragment, property: KProperty<*>): T {
+ val lifecycle = fragment.viewLifecycleOwner.lifecycle
+ check(lifecycle.currentState.isAtLeast(Lifecycle.State.INITIALIZED)) {
+ "Should not attempt to get bindings when Fragment views are destroyed."
+ }
+ return value ?: initialize().also { this.value = it }
+ }
+
+ protected abstract fun initialize(): T
+}
+
+private class FragmentViewBindingDelegate(
+ fragment: Fragment,
+ private val viewBindingInitializerFactory: (View) -> T
+) : BaseAutoClearedInitializedValue(fragment) {
+ override fun initialize(): T {
+ return viewBindingInitializerFactory(fragment.requireView())
+ }
+}
+
+private class AutoClearedInitializedValue(
+ fragment: Fragment,
+ private val initializerFactory: () -> T
+) : BaseAutoClearedInitializedValue(fragment) {
+ override fun initialize(): T {
+ return initializerFactory.invoke()
+ }
+}
+
+fun Fragment.autoClearInitializer(initializerFactory: () -> T) =
+ AutoClearedInitializedValue(this, initializerFactory) as ReadOnlyProperty
+
+fun Fragment.viewBinding(viewBindingFactory: (View) -> T) =
+ FragmentViewBindingDelegate(this, viewBindingFactory) as ReadOnlyProperty
diff --git a/pluto/src/main/java/com/mocklets/pluto/core/ui/DeBounceClickListener.kt b/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/DeBounceClickListener.kt
similarity index 75%
rename from pluto/src/main/java/com/mocklets/pluto/core/ui/DeBounceClickListener.kt
rename to pluto-plugins/base/lib/src/main/java/com/pluto/utilities/DeBounceClickListener.kt
index aa2d569a5..b8f6f504a 100644
--- a/pluto/src/main/java/com/mocklets/pluto/core/ui/DeBounceClickListener.kt
+++ b/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/DeBounceClickListener.kt
@@ -1,22 +1,22 @@
-package com.mocklets.pluto.core.ui
+package com.pluto.utilities
import android.view.HapticFeedbackConstants
import android.view.SoundEffectConstants
import android.view.View
-import com.mocklets.pluto.core.ui.ClickUtils.Companion.DEBOUNCE_DELAY
-import com.mocklets.pluto.core.ui.ClickUtils.Companion.enabled
+import com.pluto.utilities.ClickUtils.Companion.DEBOUNCE_DELAY
+import com.pluto.utilities.ClickUtils.Companion.enabled
-internal fun View.setDebounceClickListener(
+fun View.setOnDebounceClickListener(
delay: Long = DEBOUNCE_DELAY,
haptic: Boolean = false,
action: ((View) -> Unit)?
) {
- action?.let { action ->
+ action?.let {
setOnClickListener { view ->
view.onDebounce(delay) {
view.hapticFeedback(haptic)
view.soundFeedback()
- action.invoke(view)
+ it.invoke(view)
}
}
return
@@ -32,7 +32,7 @@ private inline fun View.onDebounce(delay: Long, next: () -> Unit?) {
}
}
-internal fun View.hapticFeedback(isGlobal: Boolean) {
+fun View.hapticFeedback(isGlobal: Boolean) {
isHapticFeedbackEnabled = true
performHapticFeedback(
HapticFeedbackConstants.VIRTUAL_KEY,
@@ -40,7 +40,7 @@ internal fun View.hapticFeedback(isGlobal: Boolean) {
)
}
-internal fun View.soundFeedback() {
+fun View.soundFeedback() {
isSoundEffectsEnabled = true
playSoundEffect(SoundEffectConstants.CLICK)
}
diff --git a/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/DebugLog.kt b/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/DebugLog.kt
new file mode 100644
index 000000000..0c6214939
--- /dev/null
+++ b/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/DebugLog.kt
@@ -0,0 +1,40 @@
+package com.pluto.utilities
+
+import android.util.Log
+
+class DebugLog private constructor() {
+
+ companion object {
+ var enabled: Boolean = true
+
+ fun v(tag: String, message: String?, tr: Throwable? = null) {
+ if (enabled) {
+ Log.v(tag, message.toString(), tr)
+ }
+ }
+
+ fun d(tag: String, message: String?, tr: Throwable? = null) {
+ if (enabled) {
+ Log.d(tag, message.toString(), tr)
+ }
+ }
+
+ fun i(tag: String, message: String?, tr: Throwable? = null) {
+ if (enabled) {
+ Log.i(tag, message.toString(), tr)
+ }
+ }
+
+ fun w(tag: String, message: String?, tr: Throwable? = null) {
+ if (enabled) {
+ Log.w(tag, message.toString(), tr)
+ }
+ }
+
+ fun e(tag: String, message: String?, tr: Throwable? = null) {
+ if (enabled) {
+ Log.e(tag, message, tr)
+ }
+ }
+ }
+}
diff --git a/pluto/src/main/java/com/mocklets/pluto/core/SingleLiveEvent.kt b/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/SingleLiveEvent.kt
similarity index 89%
rename from pluto/src/main/java/com/mocklets/pluto/core/SingleLiveEvent.kt
rename to pluto-plugins/base/lib/src/main/java/com/pluto/utilities/SingleLiveEvent.kt
index b9bdf061a..4b908a633 100644
--- a/pluto/src/main/java/com/mocklets/pluto/core/SingleLiveEvent.kt
+++ b/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/SingleLiveEvent.kt
@@ -1,4 +1,4 @@
-package com.mocklets.pluto.core
+package com.pluto.utilities
/*
* Copyright 2017 Google Inc.
@@ -44,14 +44,11 @@ class SingleLiveEvent : MutableLiveData() {
Log.w(TAG, "Multiple observers registered but only one will be notified of changes.")
}
// Observe the internal MutableLiveData
- super.observe(
- owner,
- Observer { t ->
- if (pending.compareAndSet(true, false)) {
- observer.onChanged(t)
- }
+ super.observe(owner) {
+ if (pending.compareAndSet(true, false)) {
+ observer.onChanged(it)
}
- )
+ }
}
@MainThread
diff --git a/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/device/Device.kt b/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/device/Device.kt
new file mode 100644
index 000000000..308ca8efd
--- /dev/null
+++ b/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/device/Device.kt
@@ -0,0 +1,115 @@
+package com.pluto.utilities.device
+
+import android.content.Context
+import android.content.pm.PackageInfo
+import android.content.pm.PackageManager
+import android.os.Build
+import android.provider.Settings
+import androidx.annotation.Keep
+import kotlin.math.ceil
+import kotlin.math.pow
+import kotlin.math.sqrt
+
+data class Device(val context: Context) {
+ val build: BuildData = BuildData()
+ val screen: ScreenData = context.getScreen()
+ val software: SoftwareData = context.getSoftwareData()
+ val app: AppData = context.getAppData()
+}
+
+private fun Context.getSoftwareData(): SoftwareData {
+ return SoftwareData(
+ androidId = Settings.Secure.getString(contentResolver, Settings.Secure.ANDROID_ID),
+ androidAPILevel = Build.VERSION.SDK_INT.toString(),
+ androidOs = Build.VERSION.RELEASE,
+ isRooted = RootUtil.isDeviceRooted
+ )
+}
+
+private const val DENSITY_MULTIPLIER = 160f
+private fun Context.getScreen(): ScreenData {
+ val dm = resources.displayMetrics
+ val height = dm.heightPixels
+ val width = dm.widthPixels
+ val x = (width / dm.xdpi.toDouble()).pow(2.0)
+ val y = (height / dm.ydpi.toDouble()).pow(2.0)
+ val screenInches = sqrt(x + y)
+ val rounded = ceil(screenInches)
+ val densityDpi = (dm.density * DENSITY_MULTIPLIER).toInt()
+
+ return ScreenData(
+ widthPx = width,
+ heightPx = height,
+ sizeInches = rounded,
+ density = densityDpi,
+ orientation = getOrientation()
+ )
+}
+
+private fun Context.getOrientation(): String {
+ return when (resources.configuration.orientation) {
+ 1 -> "portrait"
+ 2 -> "landscape"
+ else -> "undefined"
+ }
+}
+
+private fun Context.getAppData(): AppData {
+ packageManager.getApplicationLabel(
+ packageManager.getApplicationInfo(packageName, PackageManager.GET_META_DATA)
+ ) as String
+ val info: PackageInfo = packageManager.getPackageInfo(packageName, 0)
+ return AppData(
+ name = packageManager.getApplicationLabel(packageManager.getApplicationInfo(packageName, PackageManager.GET_META_DATA)) as String,
+ packageName = info.packageName,
+ version = VersionData(
+ name = info.versionName,
+ code = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) info.longVersionCode else info.versionCode.toLong()
+ )
+ )
+}
+
+@Keep
+data class VersionData(
+ val name: String,
+ val code: Long
+)
+
+@Keep
+data class AppData(
+ val name: String,
+ val packageName: String,
+ val version: VersionData
+)
+
+@Keep
+data class ScreenData(
+ val widthPx: Int,
+ val heightPx: Int,
+ val sizeInches: Double,
+ val density: Int,
+ val orientation: String
+)
+
+@Keep
+data class SoftwareData(
+ val androidId: String?,
+ val androidAPILevel: String?,
+ val androidOs: String?,
+ val isRooted: Boolean
+)
+
+@Keep
+data class BuildData(
+ val user: String? = Build.USER,
+ val host: String? = Build.HOST,
+ val id: String? = Build.ID,
+ val fingerprint: String? = Build.FINGERPRINT,
+ val manufacturer: String? = Build.MANUFACTURER,
+ val hardware: String? = Build.HARDWARE,
+ val board: String = Build.BOARD,
+ val brand: String? = Build.BRAND,
+ val bootloader: String? = Build.BOOTLOADER,
+ val model: String? = Build.MODEL,
+ val gaId: String? = null
+)
diff --git a/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/device/RootUtil.kt b/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/device/RootUtil.kt
new file mode 100644
index 000000000..efcc937e3
--- /dev/null
+++ b/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/device/RootUtil.kt
@@ -0,0 +1,56 @@
+package com.pluto.utilities.device
+
+import android.os.Build
+import java.io.BufferedReader
+import java.io.File
+import java.io.InputStreamReader
+
+/** Credits: https://stackoverflow.com/questions/1101380/determine-if-running-on-a-rooted-device/8097801
+ *
+ * @author Kevin Kowalewski
+ */
+internal class RootUtil private constructor() {
+ companion object {
+ val isDeviceRooted: Boolean
+ get() = checkRootMethod1() || checkRootMethod2() || checkRootMethod3()
+
+ private fun checkRootMethod1(): Boolean {
+ val buildTags = Build.TAGS
+ return buildTags != null && buildTags.contains("test-keys")
+ }
+
+ private fun checkRootMethod2(): Boolean {
+ val paths = arrayOf(
+ "/system/app/Superuser.apk",
+ "/sbin/su",
+ "/system/bin/su",
+ "/system/xbin/su",
+ "/data/local/xbin/su",
+ "/data/local/bin/su",
+ "/system/sd/xbin/su",
+ "/system/bin/failsafe/su",
+ "/data/local/su",
+ "/su/bin/su"
+ )
+ for (path in paths) {
+ if (File(path).exists()) return true
+ }
+ return false
+ }
+
+ @Suppress("TooGenericExceptionCaught", "SwallowedException")
+ private fun checkRootMethod3(): Boolean {
+ var process: Process? = null
+ return try {
+ process = Runtime.getRuntime().exec(arrayOf("/system/xbin/which", "su"))
+ val stream = BufferedReader(InputStreamReader(process.inputStream))
+ stream.readLine() != null
+ } catch (t: Throwable) {
+// DebugLog.e("root-utils", "exception occurred", t)
+ false
+ } finally {
+ process?.destroy()
+ }
+ }
+ }
+}
diff --git a/pluto/src/main/java/com/mocklets/pluto/core/extensions/AnimationKtx.kt b/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/extensions/AnimationKtx.kt
similarity index 96%
rename from pluto/src/main/java/com/mocklets/pluto/core/extensions/AnimationKtx.kt
rename to pluto-plugins/base/lib/src/main/java/com/pluto/utilities/extensions/AnimationKtx.kt
index cade819e8..a8c4aa3a8 100644
--- a/pluto/src/main/java/com/mocklets/pluto/core/extensions/AnimationKtx.kt
+++ b/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/extensions/AnimationKtx.kt
@@ -1,4 +1,4 @@
-package com.mocklets.pluto.core.extensions
+package com.pluto.utilities.extensions
import android.view.animation.Animation
diff --git a/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/extensions/ContextKtx.kt b/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/extensions/ContextKtx.kt
new file mode 100644
index 000000000..e241314dd
--- /dev/null
+++ b/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/extensions/ContextKtx.kt
@@ -0,0 +1,53 @@
+package com.pluto.utilities.extensions
+
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.widget.Toast
+import android.widget.Toast.LENGTH_LONG
+import android.widget.Toast.LENGTH_SHORT
+import androidx.activity.OnBackPressedCallback
+import androidx.annotation.ColorInt
+import androidx.annotation.ColorRes
+import androidx.annotation.DrawableRes
+import androidx.annotation.FontRes
+import androidx.core.content.ContextCompat
+import androidx.core.content.res.ResourcesCompat
+import androidx.fragment.app.Fragment
+
+@ColorInt
+fun Context.color(@ColorRes id: Int) = ContextCompat.getColor(this, id)
+
+fun Context.font(@FontRes id: Int) = ResourcesCompat.getFont(this, id)
+
+fun Context.drawable(@DrawableRes id: Int) = ContextCompat.getDrawable(this, id)
+
+fun Context.toast(message: String, isLong: Boolean = false) {
+ Toast.makeText(this, message, if (isLong) LENGTH_LONG else LENGTH_SHORT).show()
+}
+
+fun Fragment.toast(message: String, isLong: Boolean = false) {
+ context?.let {
+ Toast.makeText(it, message, if (isLong) LENGTH_LONG else LENGTH_SHORT).show()
+ }
+}
+
+fun Context.checkAndOpenSupportedApp(uri: Uri?) {
+ val intent = Intent(Intent.ACTION_VIEW, uri)
+ if (intent.resolveActivity(packageManager) != null) {
+ startActivity(intent)
+ } else {
+ toast("No app to perform this action")
+ }
+}
+
+fun Fragment.onBackPressed(action: () -> Unit) {
+ requireActivity().onBackPressedDispatcher.addCallback(
+ viewLifecycleOwner,
+ object : OnBackPressedCallback(true) {
+ override fun handleOnBackPressed() {
+ action.invoke()
+ }
+ }
+ )
+}
diff --git a/pluto/src/main/java/com/mocklets/pluto/core/extensions/DateKtx.kt b/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/extensions/DateKtx.kt
similarity index 96%
rename from pluto/src/main/java/com/mocklets/pluto/core/extensions/DateKtx.kt
rename to pluto-plugins/base/lib/src/main/java/com/pluto/utilities/extensions/DateKtx.kt
index 51cdc2ea3..11501b125 100644
--- a/pluto/src/main/java/com/mocklets/pluto/core/extensions/DateKtx.kt
+++ b/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/extensions/DateKtx.kt
@@ -1,4 +1,4 @@
-package com.mocklets.pluto.core.extensions
+package com.pluto.utilities.extensions
import java.text.SimpleDateFormat
import java.util.Calendar
diff --git a/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/extensions/DimensKtx.kt b/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/extensions/DimensKtx.kt
new file mode 100644
index 000000000..89c761273
--- /dev/null
+++ b/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/extensions/DimensKtx.kt
@@ -0,0 +1,46 @@
+package com.pluto.utilities.extensions
+
+import android.content.res.Resources
+import android.util.TypedValue
+import java.text.DecimalFormat
+import java.util.Locale
+
+val Float.dp: Float
+ get() {
+ val displayMetrics = Resources.getSystem().displayMetrics
+ return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, this, displayMetrics)
+ }
+
+val Float.sp: Float
+ get() {
+ val displayMetrics = Resources.getSystem().displayMetrics
+ return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, this, displayMetrics)
+ }
+
+val Float.dp2px: Float
+ get() {
+ val scale = Resources.getSystem().displayMetrics.density
+ return this * scale + 0.5f
+ }
+
+val Float.px2dp: Float
+ get() {
+ val scale = Resources.getSystem().displayMetrics.density
+ return this / scale + 0.5f
+ }
+
+val Float.px2sp: Float
+ get() {
+ val scale = Resources.getSystem().displayMetrics.scaledDensity
+ return this / scale
+ }
+
+val Int.twoDigit: String
+ get() {
+ return String.format(Locale.ENGLISH, "%02d", this)
+ }
+
+val Float.twoDecimal: String
+ get() {
+ return DecimalFormat("#.##").format(this)
+ }
diff --git a/pluto/src/main/java/com/mocklets/pluto/core/extensions/KeyboardKtx.kt b/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/extensions/KeyboardKtx.kt
similarity index 97%
rename from pluto/src/main/java/com/mocklets/pluto/core/extensions/KeyboardKtx.kt
rename to pluto-plugins/base/lib/src/main/java/com/pluto/utilities/extensions/KeyboardKtx.kt
index f1670e267..43d3ab56f 100644
--- a/pluto/src/main/java/com/mocklets/pluto/core/extensions/KeyboardKtx.kt
+++ b/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/extensions/KeyboardKtx.kt
@@ -1,4 +1,4 @@
-package com.mocklets.pluto.core.extensions
+package com.pluto.utilities.extensions
import android.content.Context
import android.view.View
diff --git a/pluto/src/main/java/com/mocklets/pluto/core/extensions/LayoutKtx.kt b/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/extensions/LayoutKtx.kt
similarity index 85%
rename from pluto/src/main/java/com/mocklets/pluto/core/extensions/LayoutKtx.kt
rename to pluto-plugins/base/lib/src/main/java/com/pluto/utilities/extensions/LayoutKtx.kt
index c088e6d45..92cf51c4e 100644
--- a/pluto/src/main/java/com/mocklets/pluto/core/extensions/LayoutKtx.kt
+++ b/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/extensions/LayoutKtx.kt
@@ -1,4 +1,4 @@
-package com.mocklets.pluto.core.extensions
+package com.pluto.utilities.extensions
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
@@ -21,18 +21,18 @@ fun View.fadeInAndOut(lifecycleScope: LifecycleCoroutineScope) {
fadeOut.duration = SMOOTH_TRANSITION_DELAY
fadeIn.addListener(object : AnimatorListenerAdapter() {
- override fun onAnimationStart(animation: Animator?) {
+ override fun onAnimationStart(animation: Animator) {
visibility = View.VISIBLE
}
- override fun onAnimationEnd(animation: Animator?) {
+ override fun onAnimationEnd(animation: Animator) {
lifecycleScope.delayedLaunchWhenResumed(FADE_IN_OUT_ANIMATION_DURATION) {
fadeOut.start()
}
}
})
fadeOut.addListener(object : AnimatorListenerAdapter() {
- override fun onAnimationEnd(animation: Animator?) {
+ override fun onAnimationEnd(animation: Animator) {
visibility = View.GONE
}
})
diff --git a/pluto/src/main/java/com/mocklets/pluto/core/extensions/LifecycleKtx.kt b/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/extensions/LifecycleKtx.kt
similarity index 89%
rename from pluto/src/main/java/com/mocklets/pluto/core/extensions/LifecycleKtx.kt
rename to pluto-plugins/base/lib/src/main/java/com/pluto/utilities/extensions/LifecycleKtx.kt
index fda597eb1..7e4045d30 100644
--- a/pluto/src/main/java/com/mocklets/pluto/core/extensions/LifecycleKtx.kt
+++ b/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/extensions/LifecycleKtx.kt
@@ -1,4 +1,4 @@
-package com.mocklets.pluto.core.extensions
+package com.pluto.utilities.extensions
import androidx.lifecycle.LifecycleCoroutineScope
import kotlinx.coroutines.CoroutineScope
diff --git a/pluto/src/main/java/com/mocklets/pluto/core/extensions/LiveDataKtx.kt b/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/extensions/LiveDataKtx.kt
similarity index 85%
rename from pluto/src/main/java/com/mocklets/pluto/core/extensions/LiveDataKtx.kt
rename to pluto-plugins/base/lib/src/main/java/com/pluto/utilities/extensions/LiveDataKtx.kt
index b8bb111f9..d3a8644b8 100644
--- a/pluto/src/main/java/com/mocklets/pluto/core/extensions/LiveDataKtx.kt
+++ b/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/extensions/LiveDataKtx.kt
@@ -1,4 +1,4 @@
-package com.mocklets.pluto.core.extensions
+package com.pluto.utilities.extensions
import androidx.lifecycle.MutableLiveData
diff --git a/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/extensions/PairKtx.kt b/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/extensions/PairKtx.kt
new file mode 100644
index 000000000..3c1b24730
--- /dev/null
+++ b/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/extensions/PairKtx.kt
@@ -0,0 +1,15 @@
+package com.pluto.utilities.extensions
+
+fun Pair, Iterable>.forEachIndexed(action: (Int, A, B?) -> Unit) {
+ val ia = first.iterator().withIndex()
+ val ib = second.iterator().withIndex()
+
+ while (ia.hasNext() && ib.hasNext()) {
+ val next = ia.next()
+ val index = next.index
+ val va = next.value
+ val vb = ib.next().value
+
+ action(index, va, vb)
+ }
+}
diff --git a/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/extensions/PopupKtx.kt b/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/extensions/PopupKtx.kt
new file mode 100644
index 000000000..8d9b171eb
--- /dev/null
+++ b/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/extensions/PopupKtx.kt
@@ -0,0 +1,58 @@
+package com.pluto.utilities.extensions
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.view.Gravity
+import android.view.Menu
+import android.view.MenuItem
+import android.view.View
+import androidx.annotation.MenuRes
+import androidx.appcompat.view.menu.MenuBuilder
+import androidx.appcompat.view.menu.MenuPopupHelper
+import androidx.appcompat.widget.PopupMenu
+import androidx.core.view.forEach
+import com.pluto.plugin.R
+import com.pluto.utilities.spannable.createSpan
+
+@SuppressLint("RestrictedApi")
+fun Context.showMoreOptions(view: View, @MenuRes menu: Int, listener: (MenuItem) -> Unit) {
+ val popup = PopupMenu(this, view, Gravity.END).apply {
+ menuInflater.inflate(menu, this.menu)
+ applyFontToMenu(this.menu)
+ setOnMenuItemClickListener { item ->
+ listener.invoke(item)
+ true
+ }
+ }
+
+ val menuHelper = MenuPopupHelper(this, popup.menu as MenuBuilder, view)
+ menuHelper.setForceShowIcon(true)
+ menuHelper.gravity = Gravity.END
+ menuHelper.show()
+}
+
+private fun Context.applyFontToMenu(m: Menu) {
+ for (i in 0 until m.size()) {
+ applyFontToMenuItem(m.getItem(i))
+ }
+}
+
+private fun Context.applyFontToMenuItem(mi: MenuItem) {
+
+ mi.subMenu?.forEach {
+ applyFontToMenuItem(it)
+ }
+
+ mi.title?.let { title ->
+ mi.title = createSpan {
+ append(
+ fontColor(
+ fontSize(semiBold(title), MENU_FONT_SIZE),
+ color(R.color.pluto___text_dark_80)
+ )
+ )
+ }
+ }
+}
+
+private const val MENU_FONT_SIZE = 15
diff --git a/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/extensions/RecyclerViewKtx.kt b/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/extensions/RecyclerViewKtx.kt
new file mode 100644
index 000000000..c6babf508
--- /dev/null
+++ b/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/extensions/RecyclerViewKtx.kt
@@ -0,0 +1,26 @@
+package com.pluto.utilities.extensions
+
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.pluto.utilities.list.BaseAdapter
+import com.pluto.utilities.list.ListItem
+import java.lang.IllegalStateException
+
+fun RecyclerView.linearLayoutManager(): LinearLayoutManager? {
+ if (layoutManager is LinearLayoutManager) {
+ return layoutManager as LinearLayoutManager
+ }
+ return null
+}
+
+fun RecyclerView.setList(list: List) {
+ adapter?.let {
+ if (it is BaseAdapter) {
+ it.list = list
+ } else {
+ throw IllegalStateException("adapter is not BaseAdapter")
+ }
+ } ?: run {
+ throw IllegalStateException("adapter not set")
+ }
+}
diff --git a/pluto/src/main/java/com/mocklets/pluto/core/extensions/SettingsKtx.kt b/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/extensions/SettingsKtx.kt
similarity index 78%
rename from pluto/src/main/java/com/mocklets/pluto/core/extensions/SettingsKtx.kt
rename to pluto-plugins/base/lib/src/main/java/com/pluto/utilities/extensions/SettingsKtx.kt
index f2d2615c5..8927dc3cf 100644
--- a/pluto/src/main/java/com/mocklets/pluto/core/extensions/SettingsKtx.kt
+++ b/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/extensions/SettingsKtx.kt
@@ -1,4 +1,4 @@
-package com.mocklets.pluto.core.extensions
+package com.pluto.utilities.extensions
import android.content.Context
import android.content.Intent
@@ -6,7 +6,7 @@ import android.net.Uri
import android.os.Build
import android.provider.Settings
-internal fun Context.canDrawOverlays(): Boolean {
+fun Context.canDrawOverlays(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
Settings.canDrawOverlays(this)
} else {
@@ -14,7 +14,7 @@ internal fun Context.canDrawOverlays(): Boolean {
}
}
-internal fun Context.openOverlaySettings() {
+fun Context.openOverlaySettings() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val intent = Intent(
Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
diff --git a/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/extensions/StringsKtx.kt b/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/extensions/StringsKtx.kt
new file mode 100644
index 000000000..5516c075e
--- /dev/null
+++ b/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/extensions/StringsKtx.kt
@@ -0,0 +1,14 @@
+package com.pluto.utilities.extensions
+
+import java.util.Locale
+
+fun String.capitalizeText(default: Locale = Locale.getDefault()): String =
+ replaceFirstChar { if (it.isLowerCase()) it.titlecase(default) else it.toString() }
+
+fun String.truncateExcess(offset: Int): String {
+ return if (length > offset) {
+ "${take(offset)}..."
+ } else {
+ this
+ }
+}
diff --git a/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/extensions/ViewKtx.kt b/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/extensions/ViewKtx.kt
new file mode 100644
index 000000000..02e86e512
--- /dev/null
+++ b/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/extensions/ViewKtx.kt
@@ -0,0 +1,55 @@
+package com.pluto.utilities.extensions
+
+import android.content.Context
+import android.content.res.Resources
+import android.view.View
+import android.view.WindowManager
+import com.pluto.utilities.DebugLog
+
+@SuppressWarnings("TooGenericExceptionCaught")
+fun Context.removeViewFromWindow(v: View) {
+ try {
+ val windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager
+ windowManager.removeView(v)
+ } catch (t: Throwable) {
+ DebugLog.e("pluto_sdk", "error while removing view", t)
+ }
+}
+
+@SuppressWarnings("TooGenericExceptionCaught")
+fun Context.addViewToWindow(v: View, params: WindowManager.LayoutParams): Boolean {
+ return try {
+ if (canDrawOverlays()) {
+ val windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager
+ windowManager.addView(v, params)
+ true
+ } else {
+ false
+ }
+ } catch (t: Throwable) {
+ DebugLog.e("pluto_sdk", "error while adding view", t)
+ removeViewFromWindow(v)
+ false
+ }
+}
+
+@SuppressWarnings("MagicNumber")
+@Throws(Resources.NotFoundException::class)
+fun View.getIdInfo(): ViewIdInfo? {
+ val isViewIdGenerated: Boolean = id and -0x1000000 == 0 && id and 0x00FFFFFF != 0
+ return if (id != View.NO_ID && !isViewIdGenerated) {
+ ViewIdInfo(
+ packageName = when (id and -0x1000000) {
+ 0x7f000000 -> "app"
+ 0x01000000 -> "android"
+ else -> resources.getResourcePackageName(id)
+ },
+ typeName = resources.getResourceTypeName(id),
+ entryName = resources.getResourceEntryName(id)
+ )
+ } else {
+ null
+ }
+}
+
+data class ViewIdInfo(val packageName: String, val typeName: String, val entryName: String)
diff --git a/pluto/src/main/java/com/mocklets/pluto/core/ui/list/BaseAdapter.kt b/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/list/BaseAdapter.kt
similarity index 89%
rename from pluto/src/main/java/com/mocklets/pluto/core/ui/list/BaseAdapter.kt
rename to pluto-plugins/base/lib/src/main/java/com/pluto/utilities/list/BaseAdapter.kt
index cdb9725fd..8372ddda4 100644
--- a/pluto/src/main/java/com/mocklets/pluto/core/ui/list/BaseAdapter.kt
+++ b/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/list/BaseAdapter.kt
@@ -1,11 +1,11 @@
-package com.mocklets.pluto.core.ui.list
+package com.pluto.utilities.list
import android.content.Context
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
-internal abstract class BaseAdapter : DiffAwareAdapter() {
+abstract class BaseAdapter : DiffAwareAdapter() {
abstract fun onViewHolderCreated(parent: ViewGroup, viewType: Int): DiffAwareHolder?
abstract fun getItemViewType(item: ListItem): Int?
@@ -24,7 +24,7 @@ internal abstract class BaseAdapter : DiffAwareAdapter() {
}
}
-internal abstract class DiffAwareHolder(
+abstract class DiffAwareHolder(
view: View,
private val listener: DiffAwareAdapter.OnActionListener?
) : RecyclerView.ViewHolder(view) {
diff --git a/pluto/src/main/java/com/mocklets/pluto/core/ui/list/CustomItemDecorator.kt b/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/list/CustomItemDecorator.kt
similarity index 80%
rename from pluto/src/main/java/com/mocklets/pluto/core/ui/list/CustomItemDecorator.kt
rename to pluto-plugins/base/lib/src/main/java/com/pluto/utilities/list/CustomItemDecorator.kt
index c35370c8e..214cf8480 100644
--- a/pluto/src/main/java/com/mocklets/pluto/core/ui/list/CustomItemDecorator.kt
+++ b/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/list/CustomItemDecorator.kt
@@ -1,14 +1,14 @@
-package com.mocklets.pluto.core.ui.list
+package com.pluto.utilities.list
import android.content.Context
import android.graphics.Canvas
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.ItemDecoration
-import com.mocklets.pluto.R
-import com.mocklets.pluto.core.extensions.drawable
+import com.pluto.plugin.R
+import com.pluto.utilities.extensions.drawable
-internal class CustomItemDecorator(context: Context, private val edge: Int = 0) : ItemDecoration() {
+class CustomItemDecorator(context: Context, private val edge: Int = 0) : ItemDecoration() {
private val divider = context.drawable(R.drawable.pluto___line_divider)
diff --git a/pluto/src/main/java/com/mocklets/pluto/core/ui/list/DiffAwareAdapter.kt b/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/list/DiffAwareAdapter.kt
similarity index 92%
rename from pluto/src/main/java/com/mocklets/pluto/core/ui/list/DiffAwareAdapter.kt
rename to pluto-plugins/base/lib/src/main/java/com/pluto/utilities/list/DiffAwareAdapter.kt
index 34b05b01a..0bed66c3d 100644
--- a/pluto/src/main/java/com/mocklets/pluto/core/ui/list/DiffAwareAdapter.kt
+++ b/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/list/DiffAwareAdapter.kt
@@ -1,11 +1,11 @@
-package com.mocklets.pluto.core.ui.list
+package com.pluto.utilities.list
import android.view.ViewGroup
import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
-internal abstract class DiffAwareAdapter : RecyclerView.Adapter() {
+abstract class DiffAwareAdapter : RecyclerView.Adapter() {
private val asyncListDiffer by lazy {
AsyncListDiffer(
@@ -58,7 +58,7 @@ internal abstract class DiffAwareAdapter : RecyclerView.Ad
abstract override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH
interface OnActionListener {
- fun onAction(action: String, data: ListItem, holder: DiffAwareHolder? = null)
+ fun onAction(action: String, data: ListItem, holder: DiffAwareHolder)
}
}
diff --git a/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/selector/DataSelector.kt b/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/selector/DataSelector.kt
new file mode 100644
index 000000000..0e69fcbaa
--- /dev/null
+++ b/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/selector/DataSelector.kt
@@ -0,0 +1,53 @@
+package com.pluto.utilities.selector
+
+import androidx.activity.ComponentActivity
+import androidx.activity.viewModels
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MediatorLiveData
+import androidx.lifecycle.ViewModel
+import com.pluto.utilities.SingleLiveEvent
+import com.pluto.utilities.list.ListItem
+
+class Selector : ViewModel() {
+
+ internal val singleChoiceData: LiveData>
+ get() = _singleChoiceData
+ private val _singleChoiceData = SingleLiveEvent>()
+ internal val singleChoiceResult = SingleLiveEvent()
+
+ internal val multiChoiceData: LiveData>>
+ get() = _multiChoiceData
+ private val _multiChoiceData = SingleLiveEvent>>()
+ internal val multiChoiceResult = SingleLiveEvent>()
+
+ val data = MediatorLiveData()
+
+ init {
+ data.addSource(singleChoiceData) { data.postValue(it) }
+ data.addSource(multiChoiceData) { data.postValue(it) }
+ }
+
+ fun selectSingle(title: String, list: List, preSelected: SelectorOption? = null): SingleLiveEvent {
+ _singleChoiceData.postValue(SelectorData(title, list, preSelected))
+ return singleChoiceResult
+ }
+
+ fun selectMultiple(title: String, list: List, preSelected: List? = null): SingleLiveEvent> {
+ _multiChoiceData.postValue(SelectorData(title, list, preSelected))
+ return multiChoiceResult
+ }
+}
+
+abstract class SelectorOption : ListItem() {
+ abstract fun displayText(): CharSequence
+
+ fun displayTextString(): String = displayText().toString()
+}
+
+internal data class SelectorData(val title: String, val list: List, val preSelected: T?)
+
+fun Fragment.lazyDataSelector(): Lazy = activityViewModels()
+
+fun ComponentActivity.lazyDataSelector(): Lazy = viewModels()
diff --git a/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/selector/ui/DataSelectorDialog.kt b/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/selector/ui/DataSelectorDialog.kt
new file mode 100644
index 000000000..3d3a80d46
--- /dev/null
+++ b/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/selector/ui/DataSelectorDialog.kt
@@ -0,0 +1,112 @@
+package com.pluto.utilities.selector.ui
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.View.GONE
+import android.view.ViewGroup
+import com.google.android.material.bottomsheet.BottomSheetBehavior
+import com.google.android.material.bottomsheet.BottomSheetDialog
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment
+import com.pluto.plugin.R
+import com.pluto.plugin.databinding.PlutoSelectorDialogBinding
+import com.pluto.utilities.SingleLiveEvent
+import com.pluto.utilities.device.Device
+import com.pluto.utilities.extensions.dp
+import com.pluto.utilities.list.CustomItemDecorator
+import com.pluto.utilities.list.DiffAwareAdapter
+import com.pluto.utilities.list.DiffAwareHolder
+import com.pluto.utilities.list.ListItem
+import com.pluto.utilities.selector.SelectorData
+import com.pluto.utilities.selector.SelectorOption
+import com.pluto.utilities.selector.lazyDataSelector
+import com.pluto.utilities.setOnDebounceClickListener
+import com.pluto.utilities.viewBinding
+
+class DataSelectorDialog : BottomSheetDialogFragment() {
+
+ private val binding by viewBinding(PlutoSelectorDialogBinding::bind)
+ private val selector by lazyDataSelector()
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
+ inflater.inflate(R.layout.pluto___selector_dialog, container, false)
+
+ override fun getTheme(): Int = R.style.PlutoBottomSheetDialogTheme
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ dialog?.setOnShowListener {
+ val dialog = it as BottomSheetDialog
+ val bottomSheet = dialog.findViewById(com.google.android.material.R.id.design_bottom_sheet)
+ bottomSheet?.let {
+ dialog.behavior.peekHeight = Device(requireContext()).screen.heightPx
+ dialog.behavior.state = BottomSheetBehavior.STATE_EXPANDED
+ }
+ }
+ selector.singleChoiceData.observe(viewLifecycleOwner) {
+ binding.title.text = it.title
+ setupSingleChoiceUI(it)
+ }
+ selector.multiChoiceData.observe(viewLifecycleOwner) {
+ binding.title.text = it.title
+ setupMultiChoiceUI(it)
+ }
+ }
+
+ private fun setupSingleChoiceUI(data: SelectorData) {
+ binding.doneCta.visibility = GONE
+ binding.clear.visibility = GONE
+ binding.list.apply {
+ adapter = SingleSelectorAdapter(
+ object : DiffAwareAdapter.OnActionListener {
+ override fun onAction(action: String, data: ListItem, holder: DiffAwareHolder) {
+ if (data is SelectorOption) {
+ selector.singleChoiceResult.postValue(data)
+ dismiss()
+ }
+ }
+ },
+ data.preSelected
+ ).apply { this.list = data.list }
+ addItemDecoration(CustomItemDecorator(requireContext(), DECORATOR_DIVIDER_PADDING))
+ }
+ }
+
+ private fun setupMultiChoiceUI(data: SelectorData>) {
+ val tempSelectedOptionLiveData = SingleLiveEvent>()
+ data.preSelected.let { tempSelectedOptionLiveData.postValue(it) }
+
+ binding.list.apply {
+ adapter = MultiSelectorAdapter(
+ object : DiffAwareAdapter.OnActionListener {
+ override fun onAction(action: String, data: ListItem, holder: DiffAwareHolder) {
+ if (data is SelectorOption) {
+ var tempSet = tempSelectedOptionLiveData.value?.toHashSet() ?: hashSetOf()
+ val isOptionAlreadyPresent = tempSet.any { it.displayTextString() == data.displayTextString() }
+ if (isOptionAlreadyPresent) {
+ tempSet = tempSet.filter { it.displayTextString() != data.displayTextString() }.toHashSet()
+ } else {
+ tempSet.add(data)
+ }
+ tempSelectedOptionLiveData.postValue(tempSet.toList())
+ }
+ }
+ },
+ tempSelectedOptionLiveData
+ ).apply { this.list = data.list }
+ addItemDecoration(CustomItemDecorator(requireContext(), DECORATOR_DIVIDER_PADDING))
+ }
+ binding.doneCta.setOnDebounceClickListener {
+ selector.multiChoiceResult.postValue(tempSelectedOptionLiveData.value?.toList() ?: emptyList())
+ dismiss()
+ }
+
+ binding.clear.setOnDebounceClickListener {
+ selector.multiChoiceResult.postValue(emptyList())
+ dismiss()
+ }
+ }
+
+ private companion object {
+ val DECORATOR_DIVIDER_PADDING = 16f.dp.toInt()
+ }
+}
diff --git a/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/selector/ui/MultiSelectorAdapter.kt b/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/selector/ui/MultiSelectorAdapter.kt
new file mode 100644
index 000000000..85738f114
--- /dev/null
+++ b/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/selector/ui/MultiSelectorAdapter.kt
@@ -0,0 +1,61 @@
+package com.pluto.utilities.selector.ui
+
+import android.view.ViewGroup
+import androidx.lifecycle.Observer
+import com.pluto.plugin.R
+import com.pluto.plugin.databinding.PlutoMultiChoiceSelectorItemBinding
+import com.pluto.utilities.SingleLiveEvent
+import com.pluto.utilities.extensions.inflate
+import com.pluto.utilities.list.BaseAdapter
+import com.pluto.utilities.list.DiffAwareAdapter
+import com.pluto.utilities.list.DiffAwareHolder
+import com.pluto.utilities.list.ListItem
+import com.pluto.utilities.selector.SelectorOption
+import com.pluto.utilities.setOnDebounceClickListener
+import com.pluto.utilities.spannable.setSpan
+
+internal class MultiSelectorAdapter(
+ private val listener: OnActionListener,
+ private val selectedLiveData: SingleLiveEvent>
+) : BaseAdapter() {
+
+ override fun getItemViewType(item: ListItem): Int = 1
+
+ override fun onViewHolderCreated(parent: ViewGroup, viewType: Int) = MultiSelectorItemHolder(parent, listener, selectedLiveData)
+}
+
+internal class MultiSelectorItemHolder(
+ parent: ViewGroup,
+ listener: DiffAwareAdapter.OnActionListener,
+ private val selectedLiveData: SingleLiveEvent>
+) : DiffAwareHolder(parent.inflate(R.layout.pluto___multi_choice_selector_item), listener) {
+
+ private val binding = PlutoMultiChoiceSelectorItemBinding.bind(itemView)
+
+ override fun onBind(item: ListItem) {
+ if (item is SelectorOption) {
+ binding.title.setSpan {
+ append(item.displayText())
+ }
+
+ binding.root.setOnDebounceClickListener {
+ onAction("click")
+ }
+ }
+ }
+
+ override fun onAttachViewHolder() {
+ super.onAttachViewHolder()
+ selectedLiveData.observeForever(selectedChoicesObserver)
+ }
+
+ override fun onDetachViewHolder() {
+ super.onDetachViewHolder()
+ selectedLiveData.removeObserver(selectedChoicesObserver)
+ }
+
+ private val selectedChoicesObserver = Observer> { list ->
+ binding.checkbox.isSelected =
+ item is SelectorOption && list?.any { it.displayTextString() == (item as SelectorOption).displayTextString() } ?: run { false }
+ }
+}
diff --git a/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/selector/ui/SingleSelectorAdapter.kt b/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/selector/ui/SingleSelectorAdapter.kt
new file mode 100644
index 000000000..b5b883ac8
--- /dev/null
+++ b/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/selector/ui/SingleSelectorAdapter.kt
@@ -0,0 +1,41 @@
+package com.pluto.utilities.selector.ui
+
+import android.view.ViewGroup
+import com.pluto.plugin.R
+import com.pluto.plugin.databinding.PlutoSingleChoiceSelectorItemBinding
+import com.pluto.utilities.extensions.inflate
+import com.pluto.utilities.list.BaseAdapter
+import com.pluto.utilities.list.DiffAwareAdapter
+import com.pluto.utilities.list.DiffAwareHolder
+import com.pluto.utilities.list.ListItem
+import com.pluto.utilities.selector.SelectorOption
+import com.pluto.utilities.setOnDebounceClickListener
+import com.pluto.utilities.spannable.setSpan
+
+internal class SingleSelectorAdapter(private val listener: OnActionListener, private val selected: SelectorOption?) : BaseAdapter() {
+
+ override fun getItemViewType(item: ListItem): Int = 1
+
+ override fun onViewHolderCreated(parent: ViewGroup, viewType: Int): DiffAwareHolder = SingleSelectorItemHolder(parent, listener, selected)
+}
+
+internal class SingleSelectorItemHolder(
+ parent: ViewGroup,
+ listener: DiffAwareAdapter.OnActionListener,
+ private val selected: SelectorOption?
+) : DiffAwareHolder(parent.inflate(R.layout.pluto___single_choice_selector_item), listener) {
+
+ private val binding = PlutoSingleChoiceSelectorItemBinding.bind(itemView)
+
+ override fun onBind(item: ListItem) {
+ if (item is SelectorOption) {
+ binding.title.setSpan {
+ append(item.displayText())
+ }
+ binding.radio.isSelected = selected?.let { it.displayTextString() == item.displayTextString() } ?: run { false }
+ binding.root.setOnDebounceClickListener {
+ onAction("click")
+ }
+ }
+ }
+}
diff --git a/pluto/src/main/java/com/mocklets/pluto/core/ui/spannable/CustomTypefaceSpan.kt b/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/spannable/CustomTypefaceSpan.kt
similarity index 93%
rename from pluto/src/main/java/com/mocklets/pluto/core/ui/spannable/CustomTypefaceSpan.kt
rename to pluto-plugins/base/lib/src/main/java/com/pluto/utilities/spannable/CustomTypefaceSpan.kt
index ce80ad6fc..1dec4bc99 100644
--- a/pluto/src/main/java/com/mocklets/pluto/core/ui/spannable/CustomTypefaceSpan.kt
+++ b/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/spannable/CustomTypefaceSpan.kt
@@ -1,4 +1,4 @@
-package com.mocklets.pluto.core.ui.spannable
+package com.pluto.utilities.spannable
import android.graphics.Typeface
import android.text.TextPaint
diff --git a/pluto/src/main/java/com/mocklets/pluto/core/ui/spannable/SpanUtil.kt b/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/spannable/SpanUtil.kt
similarity index 78%
rename from pluto/src/main/java/com/mocklets/pluto/core/ui/spannable/SpanUtil.kt
rename to pluto-plugins/base/lib/src/main/java/com/pluto/utilities/spannable/SpanUtil.kt
index 014fc1cfb..bc1855f8f 100644
--- a/pluto/src/main/java/com/mocklets/pluto/core/ui/spannable/SpanUtil.kt
+++ b/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/spannable/SpanUtil.kt
@@ -1,4 +1,4 @@
-package com.mocklets.pluto.core.ui.spannable
+package com.pluto.utilities.spannable
import android.content.Context
import android.graphics.Typeface
@@ -12,12 +12,12 @@ import android.text.style.ForegroundColorSpan
import android.text.style.StyleSpan
import android.text.style.UnderlineSpan
import android.widget.TextView
-import com.mocklets.pluto.R
-import com.mocklets.pluto.core.extensions.color
-import com.mocklets.pluto.core.extensions.font
+import com.pluto.plugin.R
+import com.pluto.utilities.extensions.color
+import com.pluto.utilities.extensions.font
import java.text.Normalizer
-internal inline fun TextView.setSpan(
+inline fun TextView.setSpan(
bufferType: TextView.BufferType? = null,
spanBuilder: Builder.() -> Unit
) {
@@ -30,13 +30,13 @@ internal inline fun TextView.setSpan(
}
}
-internal inline fun Context.createSpan(spanBuilder: Builder.() -> Unit): CharSequence {
+inline fun Context.createSpan(spanBuilder: Builder.() -> Unit): CharSequence {
val builder = Builder(this)
builder.spanBuilder()
return builder.build()
}
-internal class Builder(val context: Context) {
+class Builder(val context: Context) {
private val spanBuilder = SpannableStringBuilder()
fun append(text: String) {
@@ -55,12 +55,15 @@ internal class Builder(val context: Context) {
is String -> SpannableString(s).apply {
setSpan(o, 0, length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
}
+
is SpannableStringBuilder -> s.apply {
setSpan(o, 0, length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
}
+
is SpannableString -> s.apply {
setSpan(o, 0, length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
}
+
else -> throw IllegalArgumentException("unhandled type $o")
}
@@ -92,14 +95,20 @@ internal class Builder(val context: Context) {
fun highlight(span: CharSequence, search: String?): CharSequence {
if (search.isNullOrEmpty()) return span
- val normalizedText = Normalizer.normalize(span, Normalizer.Form.NFD)
- .replace("\\p{InCombiningDiacriticalMarks}+".toRegex(), "")
- .lowercase()
+ val normalizedText = span.normalise().lowercase()
val startIndexes = normalizedText.allOccurrences(search)
if (startIndexes.isNotEmpty()) {
+ return highlight(span, search, startIndexes)
+ }
+ return span
+ }
+
+ fun highlight(span: CharSequence, search: String?, indexes: List): CharSequence {
+ if (search.isNullOrEmpty()) return span
+ if (indexes.isNotEmpty()) {
val highlighted: Spannable = SpannableString(span)
- startIndexes.forEach {
+ indexes.forEach {
highlighted.setSpan(
BackgroundColorSpan(context.color(R.color.pluto___text_highlight)),
it,
@@ -112,6 +121,12 @@ internal class Builder(val context: Context) {
return span
}
+ fun occurrences(span: CharSequence, search: String?): List {
+ if (search.isNullOrEmpty()) return emptyList()
+ val normalizedText = span.normalise().lowercase()
+ return normalizedText.allOccurrences(search)
+ }
+
fun clickable(span: CharSequence, listener: ClickableSpan): CharSequence {
return span(span, listener)
}
@@ -124,6 +139,11 @@ internal class Builder(val context: Context) {
return span(span, StyleSpan(Typeface.ITALIC))
}
+ private fun CharSequence.normalise(): String {
+ return Normalizer.normalize(this, Normalizer.Form.NFD)
+ .replace("\\p{InCombiningDiacriticalMarks}+".toRegex(), "")
+ }
+
fun build(): CharSequence {
return spanBuilder
}
diff --git a/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/views/TabularDataView.kt b/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/views/TabularDataView.kt
new file mode 100644
index 000000000..b61584ce5
--- /dev/null
+++ b/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/views/TabularDataView.kt
@@ -0,0 +1,24 @@
+package com.pluto.utilities.views
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import androidx.constraintlayout.widget.ConstraintLayout
+import com.pluto.plugin.databinding.PlutoViewTabularDataBinding
+import com.pluto.utilities.views.keyvalue.KeyValuePairData
+import com.pluto.utilities.views.keyvalue.KeyValuePairView
+
+class TabularDataView : ConstraintLayout {
+
+ private val binding = PlutoViewTabularDataBinding.inflate(LayoutInflater.from(context), this, true)
+
+ constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle)
+ constructor(context: Context, attrs: AttributeSet) : super(context, attrs, 0)
+ constructor(context: Context) : super(context, null, 0)
+
+ fun set(title: String, keyValuePairs: List = arrayListOf()) {
+ binding.tabularDataTitle.text = title
+ binding.tabularDataContainer.removeAllViews()
+ keyValuePairs.forEach { binding.tabularDataContainer.addView(KeyValuePairView(context).apply { set(it) }) }
+ }
+}
diff --git a/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/views/keyvalue/KeyValuePairData.kt b/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/views/keyvalue/KeyValuePairData.kt
new file mode 100644
index 000000000..853fc8498
--- /dev/null
+++ b/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/views/keyvalue/KeyValuePairData.kt
@@ -0,0 +1,53 @@
+package com.pluto.utilities.views.keyvalue
+
+import android.text.InputType
+import androidx.annotation.Keep
+import com.pluto.utilities.list.ListItem
+
+@Keep
+data class KeyValuePairData(
+ val key: String,
+ val value: CharSequence?,
+ val iconStart: Int? = null,
+ val showClickIndicator: Boolean = false,
+ val onClick: (() -> Unit)? = null
+)
+
+@Keep
+data class KeyValuePairEditRequest(
+ val key: String,
+ val value: String? = null,
+ val hint: String = "enter value",
+ private val candidateOptions: List? = null,
+ val inputType: KeyValuePairEditInputType = KeyValuePairEditInputType.String,
+ val metaData: KeyValuePairEditMetaData
+) : ListItem() {
+ val shouldAllowFreeEdit: Boolean = inputType != KeyValuePairEditInputType.Selection && inputType != KeyValuePairEditInputType.Boolean
+ val processedCandidateOptions: List? = if (inputType == KeyValuePairEditInputType.Boolean) listOf("true", "false") else candidateOptions
+ fun isValidValue(text: String?) = when (inputType) {
+ is KeyValuePairEditInputType.Integer, is KeyValuePairEditInputType.Float -> !text.isNullOrEmpty()
+ else -> true
+ }
+}
+
+@Keep
+data class KeyValuePairEditResult(
+ val key: String,
+ val value: String?,
+ val metaData: KeyValuePairEditMetaData
+)
+
+interface KeyValuePairEditMetaData
+
+@Keep
+sealed class KeyValuePairEditInputType(val type: Int? = null) {
+ object Integer : KeyValuePairEditInputType(InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_SIGNED)
+
+ object Float : KeyValuePairEditInputType(InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_SIGNED or InputType.TYPE_NUMBER_FLAG_DECIMAL)
+
+ object Selection : KeyValuePairEditInputType()
+
+ object Boolean : KeyValuePairEditInputType()
+
+ object String : KeyValuePairEditInputType(InputType.TYPE_CLASS_TEXT)
+}
diff --git a/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/views/keyvalue/KeyValuePairView.kt b/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/views/keyvalue/KeyValuePairView.kt
new file mode 100644
index 000000000..609f71e96
--- /dev/null
+++ b/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/views/keyvalue/KeyValuePairView.kt
@@ -0,0 +1,39 @@
+package com.pluto.utilities.views.keyvalue
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import androidx.constraintlayout.widget.ConstraintLayout
+import com.pluto.plugin.R
+import com.pluto.plugin.databinding.PlutoViewKeyValuePairBinding
+import com.pluto.utilities.extensions.color
+import com.pluto.utilities.setOnDebounceClickListener
+import com.pluto.utilities.spannable.setSpan
+
+class KeyValuePairView : ConstraintLayout {
+
+ private val binding = PlutoViewKeyValuePairBinding.inflate(LayoutInflater.from(context), this, true)
+
+ constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle)
+ constructor(context: Context, attrs: AttributeSet) : super(context, attrs, 0)
+ constructor(context: Context) : super(context, null, 0)
+
+ fun set(data: KeyValuePairData) {
+ binding.key.text = data.key
+ binding.value.setSpan {
+ data.value?.let {
+ append(it)
+ } ?: run {
+ append(italic(fontColor("~ null ~", context.color(R.color.pluto___text_dark_40))))
+ }
+ }
+
+ binding.value.setCompoundDrawablesWithIntrinsicBounds(0, 0, if (data.showClickIndicator) R.drawable.pluto___ic_chevron_right else 0, 0)
+ data.onClick?.let {
+ binding.root.isClickable = true
+ binding.root.setOnDebounceClickListener { data.onClick.invoke() }
+ } ?: run {
+ binding.root.isClickable = false
+ }
+ }
+}
diff --git a/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/views/keyvalue/edit/KeyValuePairEditDialog.kt b/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/views/keyvalue/edit/KeyValuePairEditDialog.kt
new file mode 100644
index 000000000..04dd61255
--- /dev/null
+++ b/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/views/keyvalue/edit/KeyValuePairEditDialog.kt
@@ -0,0 +1,136 @@
+package com.pluto.utilities.views.keyvalue.edit
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.View.GONE
+import android.view.View.VISIBLE
+import android.view.ViewGroup
+import android.view.inputmethod.EditorInfo
+import androidx.core.content.ContextCompat
+import androidx.core.widget.doOnTextChanged
+import com.google.android.material.bottomsheet.BottomSheetBehavior
+import com.google.android.material.bottomsheet.BottomSheetDialog
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment
+import com.google.android.material.chip.Chip
+import com.pluto.plugin.R
+import com.pluto.plugin.databinding.PlutoFragmentKeyValuePairEditBinding
+import com.pluto.utilities.device.Device
+import com.pluto.utilities.extensions.color
+import com.pluto.utilities.extensions.dp
+import com.pluto.utilities.setOnDebounceClickListener
+import com.pluto.utilities.spannable.setSpan
+import com.pluto.utilities.viewBinding
+import com.pluto.utilities.views.keyvalue.KeyValuePairEditRequest
+import com.pluto.utilities.views.keyvalue.KeyValuePairEditResult
+
+class KeyValuePairEditDialog : BottomSheetDialogFragment() {
+
+ private val binding by viewBinding(PlutoFragmentKeyValuePairEditBinding::bind)
+ private val keyValuePairEditor: KeyValuePairEditor by lazyKeyValuePairEditor()
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
+ inflater.inflate(R.layout.pluto___fragment_key_value_pair_edit, container, false)
+
+ override fun getTheme(): Int = R.style.PlutoBottomSheetDialogTheme
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ dialog?.setOnShowListener {
+ val dialog = it as BottomSheetDialog
+ val bottomSheet = dialog.findViewById(com.google.android.material.R.id.design_bottom_sheet)
+ bottomSheet?.let {
+ dialog.behavior.peekHeight = Device(requireContext()).screen.heightPx
+ dialog.behavior.state = BottomSheetBehavior.STATE_EXPANDED
+ keyValuePairEditor.data.value?.let { data ->
+ handleUI(data)
+ }
+ }
+ }
+
+ binding.close.setOnDebounceClickListener {
+ dismiss()
+ }
+ }
+
+ private fun handleUI(data: KeyValuePairEditRequest) {
+ binding.editKeyDescription.setSpan {
+ append(getString(R.string.pluto___key_value_editor_description))
+ append(" ")
+ append(bold(fontColor(data.key, context.color(R.color.pluto___text_dark_80))))
+ }
+ if (data.shouldAllowFreeEdit) {
+ handleInputType(data)
+ } else {
+ binding.editGroup.visibility = GONE
+ }
+ handleCandidateOptions(data)
+ binding.saveCta.setOnDebounceClickListener {
+ saveResult(data.key, binding.editValue.text.toString())
+ }
+ }
+
+ private fun handleCandidateOptions(data: KeyValuePairEditRequest) {
+ data.processedCandidateOptions?.let { list ->
+ binding.candidateOptionDescription.visibility = if (data.shouldAllowFreeEdit) VISIBLE else GONE
+ binding.candidateOptions.visibility = VISIBLE
+ list.forEach {
+ binding.candidateOptions.addView(
+ Chip(context).apply {
+ text = it.trim()
+ setTextAppearance(R.style.PlutoChipTextStyle)
+ if (it == data.value) {
+ chipIcon = ContextCompat.getDrawable(context, R.drawable.pluto___ic_edit_option_selected)
+ chipIconSize = 18f.dp
+ iconStartPadding = CHIP_ICON_PADDING
+ textStartPadding = CHIP_TEXT_PADDING
+ }
+ setOnDebounceClickListener { _ ->
+ saveResult(data.key, it)
+ }
+ }
+ )
+ }
+ }
+ }
+
+ private fun handleInputType(data: KeyValuePairEditRequest) {
+ binding.editGroup.visibility = VISIBLE
+ binding.editValue.setText(data.value)
+ binding.editValue.hint = data.hint
+ binding.editValue.requestFocus()
+ binding.editValue.isFocusableInTouchMode = true
+ binding.editValue.isFocusable = true
+ binding.editValue.post {
+ binding.editValue.setSelection(data.value?.length ?: 0)
+ }
+ data.inputType.type?.let {
+ binding.editValue.inputType = it
+ }
+ binding.editValue.doOnTextChanged { text, _, _, _ ->
+ val isValid = data.isValidValue(text?.toString())
+ binding.saveCta.isEnabled = isValid
+ binding.saveCtaIcon.setBackgroundColor(
+ requireContext().color(if (isValid) R.color.pluto___emerald else R.color.pluto___disabled_cta)
+ )
+ }
+ binding.editValue.setOnEditorActionListener { _, actionId, _ ->
+ if (data.isValidValue(binding.editValue.text?.toString()) && actionId == EditorInfo.IME_ACTION_DONE) {
+ saveResult(data.key, binding.editValue.text.toString())
+ true
+ } else false
+ }
+ }
+
+ private fun saveResult(key: String, value: String) {
+ keyValuePairEditor.data.value?.let { data ->
+ keyValuePairEditor.saveResult(KeyValuePairEditResult(key, value, data.metaData))
+ }
+ dismiss()
+ }
+
+ companion object {
+ private const val CHIP_TEXT_PADDING = 8f
+ private const val CHIP_ICON_PADDING = 10f
+ }
+}
diff --git a/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/views/keyvalue/edit/KeyValuePairEditor.kt b/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/views/keyvalue/edit/KeyValuePairEditor.kt
new file mode 100644
index 000000000..da26e5cd0
--- /dev/null
+++ b/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/views/keyvalue/edit/KeyValuePairEditor.kt
@@ -0,0 +1,34 @@
+package com.pluto.utilities.views.keyvalue.edit
+
+import androidx.activity.ComponentActivity
+import androidx.activity.viewModels
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.ViewModel
+import com.pluto.utilities.SingleLiveEvent
+import com.pluto.utilities.views.keyvalue.KeyValuePairEditRequest
+import com.pluto.utilities.views.keyvalue.KeyValuePairEditResult
+
+class KeyValuePairEditor : ViewModel() {
+
+ val data: LiveData
+ get() = _data
+ private val _data = SingleLiveEvent()
+
+ val result: LiveData
+ get() = _result
+ private val _result = SingleLiveEvent()
+
+ fun edit(data: KeyValuePairEditRequest) {
+ _data.postValue(data)
+ }
+
+ fun saveResult(result: KeyValuePairEditResult) {
+ _result.postValue(result)
+ }
+}
+
+fun Fragment.lazyKeyValuePairEditor(): Lazy = activityViewModels()
+
+fun ComponentActivity.lazyKeyValuePairEditor(): Lazy = viewModels()
diff --git a/pluto/src/main/res/anim/pluto___fragment_default_enter.xml b/pluto-plugins/base/lib/src/main/res/anim/pluto___fragment_default_enter.xml
similarity index 100%
rename from pluto/src/main/res/anim/pluto___fragment_default_enter.xml
rename to pluto-plugins/base/lib/src/main/res/anim/pluto___fragment_default_enter.xml
diff --git a/pluto/src/main/res/anim/pluto___fragment_default_exit.xml b/pluto-plugins/base/lib/src/main/res/anim/pluto___fragment_default_exit.xml
similarity index 100%
rename from pluto/src/main/res/anim/pluto___fragment_default_exit.xml
rename to pluto-plugins/base/lib/src/main/res/anim/pluto___fragment_default_exit.xml
diff --git a/pluto/src/main/res/anim/pluto___fragment_default_reenter.xml b/pluto-plugins/base/lib/src/main/res/anim/pluto___fragment_default_reenter.xml
similarity index 100%
rename from pluto/src/main/res/anim/pluto___fragment_default_reenter.xml
rename to pluto-plugins/base/lib/src/main/res/anim/pluto___fragment_default_reenter.xml
diff --git a/pluto/src/main/res/anim/pluto___fragment_default_return.xml b/pluto-plugins/base/lib/src/main/res/anim/pluto___fragment_default_return.xml
similarity index 100%
rename from pluto/src/main/res/anim/pluto___fragment_default_return.xml
rename to pluto-plugins/base/lib/src/main/res/anim/pluto___fragment_default_return.xml
diff --git a/pluto-plugins/base/lib/src/main/res/drawable/pluto___bg_bottom_sheet.xml b/pluto-plugins/base/lib/src/main/res/drawable/pluto___bg_bottom_sheet.xml
new file mode 100644
index 000000000..48e20428b
--- /dev/null
+++ b/pluto-plugins/base/lib/src/main/res/drawable/pluto___bg_bottom_sheet.xml
@@ -0,0 +1,21 @@
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pluto-plugins/base/lib/src/main/res/drawable/pluto___bg_key_value_pair_input.xml b/pluto-plugins/base/lib/src/main/res/drawable/pluto___bg_key_value_pair_input.xml
new file mode 100644
index 000000000..5fd263b79
--- /dev/null
+++ b/pluto-plugins/base/lib/src/main/res/drawable/pluto___bg_key_value_pair_input.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pluto-plugins/base/lib/src/main/res/drawable/pluto___ic_check.xml b/pluto-plugins/base/lib/src/main/res/drawable/pluto___ic_check.xml
new file mode 100644
index 000000000..98f52f1ee
--- /dev/null
+++ b/pluto-plugins/base/lib/src/main/res/drawable/pluto___ic_check.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/pluto/src/main/res/drawable/pluto___ic_check_box.xml b/pluto-plugins/base/lib/src/main/res/drawable/pluto___ic_check_box.xml
similarity index 100%
rename from pluto/src/main/res/drawable/pluto___ic_check_box.xml
rename to pluto-plugins/base/lib/src/main/res/drawable/pluto___ic_check_box.xml
diff --git a/pluto/src/main/res/drawable/pluto___ic_check_box_selected.xml b/pluto-plugins/base/lib/src/main/res/drawable/pluto___ic_check_box_selected.xml
similarity index 100%
rename from pluto/src/main/res/drawable/pluto___ic_check_box_selected.xml
rename to pluto-plugins/base/lib/src/main/res/drawable/pluto___ic_check_box_selected.xml
diff --git a/pluto/src/main/res/drawable/pluto___ic_check_box_unselected.xml b/pluto-plugins/base/lib/src/main/res/drawable/pluto___ic_check_box_unselected.xml
similarity index 100%
rename from pluto/src/main/res/drawable/pluto___ic_check_box_unselected.xml
rename to pluto-plugins/base/lib/src/main/res/drawable/pluto___ic_check_box_unselected.xml
diff --git a/pluto-plugins/base/lib/src/main/res/drawable/pluto___ic_chevron_right.xml b/pluto-plugins/base/lib/src/main/res/drawable/pluto___ic_chevron_right.xml
new file mode 100644
index 000000000..7c382f777
--- /dev/null
+++ b/pluto-plugins/base/lib/src/main/res/drawable/pluto___ic_chevron_right.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/pluto/src/main/res/drawable/pluto___ic_close.xml b/pluto-plugins/base/lib/src/main/res/drawable/pluto___ic_close.xml
similarity index 86%
rename from pluto/src/main/res/drawable/pluto___ic_close.xml
rename to pluto-plugins/base/lib/src/main/res/drawable/pluto___ic_close.xml
index 52ec74ab6..82eaa9f97 100644
--- a/pluto/src/main/res/drawable/pluto___ic_close.xml
+++ b/pluto-plugins/base/lib/src/main/res/drawable/pluto___ic_close.xml
@@ -4,6 +4,6 @@
android:viewportWidth="24.0"
android:viewportHeight="24.0">
diff --git a/pluto/src/main/res/drawable/pluto___ic_success.xml b/pluto-plugins/base/lib/src/main/res/drawable/pluto___ic_edit_option_selected.xml
similarity index 100%
rename from pluto/src/main/res/drawable/pluto___ic_success.xml
rename to pluto-plugins/base/lib/src/main/res/drawable/pluto___ic_edit_option_selected.xml
diff --git a/pluto-plugins/base/lib/src/main/res/drawable/pluto___ic_plugin_group_placeholder_icon.xml b/pluto-plugins/base/lib/src/main/res/drawable/pluto___ic_plugin_group_placeholder_icon.xml
new file mode 100644
index 000000000..3a88c8217
--- /dev/null
+++ b/pluto-plugins/base/lib/src/main/res/drawable/pluto___ic_plugin_group_placeholder_icon.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/pluto-plugins/base/lib/src/main/res/drawable/pluto___ic_plugin_placeholder_icon.xml b/pluto-plugins/base/lib/src/main/res/drawable/pluto___ic_plugin_placeholder_icon.xml
new file mode 100644
index 000000000..16740b74d
--- /dev/null
+++ b/pluto-plugins/base/lib/src/main/res/drawable/pluto___ic_plugin_placeholder_icon.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/pluto-plugins/base/lib/src/main/res/drawable/pluto___ic_radio.xml b/pluto-plugins/base/lib/src/main/res/drawable/pluto___ic_radio.xml
new file mode 100644
index 000000000..64ede9fbf
--- /dev/null
+++ b/pluto-plugins/base/lib/src/main/res/drawable/pluto___ic_radio.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/pluto-plugins/base/lib/src/main/res/drawable/pluto___ic_radio_selected.xml b/pluto-plugins/base/lib/src/main/res/drawable/pluto___ic_radio_selected.xml
new file mode 100644
index 000000000..05375ff7d
--- /dev/null
+++ b/pluto-plugins/base/lib/src/main/res/drawable/pluto___ic_radio_selected.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/pluto-plugins/base/lib/src/main/res/drawable/pluto___ic_radio_unselected.xml b/pluto-plugins/base/lib/src/main/res/drawable/pluto___ic_radio_unselected.xml
new file mode 100644
index 000000000..9a4d80c2e
--- /dev/null
+++ b/pluto-plugins/base/lib/src/main/res/drawable/pluto___ic_radio_unselected.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/pluto/src/main/res/drawable/pluto___ic_search.xml b/pluto-plugins/base/lib/src/main/res/drawable/pluto___ic_search.xml
similarity index 100%
rename from pluto/src/main/res/drawable/pluto___ic_search.xml
rename to pluto-plugins/base/lib/src/main/res/drawable/pluto___ic_search.xml
diff --git a/pluto/src/main/res/drawable/pluto___ic_share_as_copy.xml b/pluto-plugins/base/lib/src/main/res/drawable/pluto___ic_share_as_copy.xml
similarity index 100%
rename from pluto/src/main/res/drawable/pluto___ic_share_as_copy.xml
rename to pluto-plugins/base/lib/src/main/res/drawable/pluto___ic_share_as_copy.xml
diff --git a/pluto/src/main/res/drawable/pluto___ic_share_as_file.xml b/pluto-plugins/base/lib/src/main/res/drawable/pluto___ic_share_as_file.xml
similarity index 100%
rename from pluto/src/main/res/drawable/pluto___ic_share_as_file.xml
rename to pluto-plugins/base/lib/src/main/res/drawable/pluto___ic_share_as_file.xml
diff --git a/pluto/src/main/res/drawable/pluto___ic_share_as_text.xml b/pluto-plugins/base/lib/src/main/res/drawable/pluto___ic_share_as_text.xml
similarity index 100%
rename from pluto/src/main/res/drawable/pluto___ic_share_as_text.xml
rename to pluto-plugins/base/lib/src/main/res/drawable/pluto___ic_share_as_text.xml
diff --git a/pluto/src/main/res/drawable/pluto___line_divider.xml b/pluto-plugins/base/lib/src/main/res/drawable/pluto___line_divider.xml
similarity index 100%
rename from pluto/src/main/res/drawable/pluto___line_divider.xml
rename to pluto-plugins/base/lib/src/main/res/drawable/pluto___line_divider.xml
diff --git a/pluto/src/main/res/font/muli.ttf b/pluto-plugins/base/lib/src/main/res/font/muli.ttf
similarity index 100%
rename from pluto/src/main/res/font/muli.ttf
rename to pluto-plugins/base/lib/src/main/res/font/muli.ttf
diff --git a/pluto/src/main/res/font/muli_bold.ttf b/pluto-plugins/base/lib/src/main/res/font/muli_bold.ttf
similarity index 100%
rename from pluto/src/main/res/font/muli_bold.ttf
rename to pluto-plugins/base/lib/src/main/res/font/muli_bold.ttf
diff --git a/pluto/src/main/res/font/muli_light.ttf b/pluto-plugins/base/lib/src/main/res/font/muli_light.ttf
similarity index 100%
rename from pluto/src/main/res/font/muli_light.ttf
rename to pluto-plugins/base/lib/src/main/res/font/muli_light.ttf
diff --git a/pluto/src/main/res/font/muli_semibold.ttf b/pluto-plugins/base/lib/src/main/res/font/muli_semibold.ttf
similarity index 100%
rename from pluto/src/main/res/font/muli_semibold.ttf
rename to pluto-plugins/base/lib/src/main/res/font/muli_semibold.ttf
diff --git a/pluto-plugins/base/lib/src/main/res/layout/pluto___fragment_key_value_pair_edit.xml b/pluto-plugins/base/lib/src/main/res/layout/pluto___fragment_key_value_pair_edit.xml
new file mode 100644
index 000000000..0a7e19406
--- /dev/null
+++ b/pluto-plugins/base/lib/src/main/res/layout/pluto___fragment_key_value_pair_edit.xml
@@ -0,0 +1,157 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pluto-plugins/base/lib/src/main/res/layout/pluto___fragment_share.xml b/pluto-plugins/base/lib/src/main/res/layout/pluto___fragment_share.xml
new file mode 100644
index 000000000..198e00187
--- /dev/null
+++ b/pluto-plugins/base/lib/src/main/res/layout/pluto___fragment_share.xml
@@ -0,0 +1,142 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pluto/src/main/res/layout/pluto___layout_content_share_options.xml b/pluto-plugins/base/lib/src/main/res/layout/pluto___layout_content_share_options.xml
similarity index 99%
rename from pluto/src/main/res/layout/pluto___layout_content_share_options.xml
rename to pluto-plugins/base/lib/src/main/res/layout/pluto___layout_content_share_options.xml
index a1f915a41..198e00187 100644
--- a/pluto/src/main/res/layout/pluto___layout_content_share_options.xml
+++ b/pluto-plugins/base/lib/src/main/res/layout/pluto___layout_content_share_options.xml
@@ -82,7 +82,6 @@
android:drawableLeft="@drawable/pluto___ic_share_as_file"
android:drawablePadding="@dimen/pluto___margin_xsmall"
android:fontFamily="@font/muli_semibold"
-
android:gravity="center_vertical"
android:paddingHorizontal="@dimen/pluto___margin_medium"
android:paddingTop="@dimen/pluto___margin_small"
diff --git a/pluto-plugins/base/lib/src/main/res/layout/pluto___multi_choice_selector_item.xml b/pluto-plugins/base/lib/src/main/res/layout/pluto___multi_choice_selector_item.xml
new file mode 100644
index 000000000..f2015b62e
--- /dev/null
+++ b/pluto-plugins/base/lib/src/main/res/layout/pluto___multi_choice_selector_item.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pluto-plugins/base/lib/src/main/res/layout/pluto___selector_dialog.xml b/pluto-plugins/base/lib/src/main/res/layout/pluto___selector_dialog.xml
new file mode 100644
index 000000000..cde426cf1
--- /dev/null
+++ b/pluto-plugins/base/lib/src/main/res/layout/pluto___selector_dialog.xml
@@ -0,0 +1,131 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pluto-plugins/base/lib/src/main/res/layout/pluto___single_choice_selector_item.xml b/pluto-plugins/base/lib/src/main/res/layout/pluto___single_choice_selector_item.xml
new file mode 100644
index 000000000..1890b6779
--- /dev/null
+++ b/pluto-plugins/base/lib/src/main/res/layout/pluto___single_choice_selector_item.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pluto-plugins/base/lib/src/main/res/layout/pluto___view_key_value_pair.xml b/pluto-plugins/base/lib/src/main/res/layout/pluto___view_key_value_pair.xml
new file mode 100644
index 000000000..31c7a9620
--- /dev/null
+++ b/pluto-plugins/base/lib/src/main/res/layout/pluto___view_key_value_pair.xml
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pluto-plugins/base/lib/src/main/res/layout/pluto___view_tabular_data.xml b/pluto-plugins/base/lib/src/main/res/layout/pluto___view_tabular_data.xml
new file mode 100644
index 000000000..607443761
--- /dev/null
+++ b/pluto-plugins/base/lib/src/main/res/layout/pluto___view_tabular_data.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
diff --git a/pluto-plugins/base/lib/src/main/res/values/colors.xml b/pluto-plugins/base/lib/src/main/res/values/colors.xml
new file mode 100644
index 000000000..55451af4b
--- /dev/null
+++ b/pluto-plugins/base/lib/src/main/res/values/colors.xml
@@ -0,0 +1,69 @@
+
+
+ #008577
+ #00574B
+ @color/pluto___text_dark_60
+
+ #f8f8f8
+ #eeecec
+ #f0f0f0
+ #f6f6f6
+ #ffffff
+ #11141c
+ #0D11141c
+ #1A11141c
+ #3311141c
+ #6611141c
+ #9911141c
+ #cc11141c
+
+ #231f40
+ #33231f40
+ #66231f40
+ #99231f40
+ #cc231f40
+ @color/pluto___dark
+ #0Dffffff
+ #1Affffff
+ #33ffffff
+ #66ffffff
+ #99ffffff
+ #ccffffff
+ #e50914
+ #ff5733
+ #0Dff5733
+ #33ff5733
+ #66ff5733
+ #99ff5733
+ #ccff5733
+ #ffa000
+ #0Dffa000
+ #66ffa000
+ #99ffa000
+ #ccffa000
+ #009463
+ #20c997
+ #00000000
+ #eeecec
+ #73a964
+ #1873a964
+ #0D73a964
+
+ #f5f5f5
+ #366BD1
+ #cc366BD1
+ #66366BD1
+
+ #aaff9100
+
+ #eeecec
+ #ff5733
+
+ #11141c
+ #ff9100
+
+ #a9a9a9
+
+ #11141c
+ #6611141c
+
diff --git a/pluto-plugins/base/lib/src/main/res/values/dimens.xml b/pluto-plugins/base/lib/src/main/res/values/dimens.xml
new file mode 100644
index 000000000..3bf372447
--- /dev/null
+++ b/pluto-plugins/base/lib/src/main/res/values/dimens.xml
@@ -0,0 +1,36 @@
+
+
+ 70dp
+ 8dp
+ 60dp
+ 44dp
+
+ 1dp
+
+ 2dp
+ 4dp
+ 8dp
+ 12dp
+ 16dp
+ 20dp
+ 24dp
+ 28dp
+ 32dp
+ 36dp
+ 40dp
+ 72dp
+
+ 10sp
+ 11sp
+ 12sp
+ 13sp
+ 14sp
+ 15sp
+ 16sp
+ 18sp
+ 20sp
+ 24sp
+ 30sp
+
+ 48dp
+
\ No newline at end of file
diff --git a/pluto/src/main/res/values/integers.xml b/pluto-plugins/base/lib/src/main/res/values/integers.xml
similarity index 100%
rename from pluto/src/main/res/values/integers.xml
rename to pluto-plugins/base/lib/src/main/res/values/integers.xml
diff --git a/pluto-plugins/base/lib/src/main/res/values/strings.xml b/pluto-plugins/base/lib/src/main/res/values/strings.xml
new file mode 100644
index 000000000..a14e6fd75
--- /dev/null
+++ b/pluto-plugins/base/lib/src/main/res/values/strings.xml
@@ -0,0 +1,14 @@
+
+ Edit Value
+ editing value of
+ enter value here
+ or choose from these options
+
+ Share As
+ Share as Text
+ Share as File
+ Copy to Clipboard
+ Prefer this option while sharing on Slack.
+ Clear
+ Done
+
diff --git a/pluto-plugins/base/lib/src/main/res/values/styles.xml b/pluto-plugins/base/lib/src/main/res/values/styles.xml
new file mode 100644
index 000000000..ccec36e90
--- /dev/null
+++ b/pluto-plugins/base/lib/src/main/res/values/styles.xml
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pluto-plugins/bundle/lib-no-op/.gitignore b/pluto-plugins/bundle/lib-no-op/.gitignore
new file mode 100644
index 000000000..42afabfd2
--- /dev/null
+++ b/pluto-plugins/bundle/lib-no-op/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/pluto-plugins/bundle/lib-no-op/build.gradle.kts b/pluto-plugins/bundle/lib-no-op/build.gradle.kts
new file mode 100644
index 000000000..10325711d
--- /dev/null
+++ b/pluto-plugins/bundle/lib-no-op/build.gradle.kts
@@ -0,0 +1,98 @@
+import com.vanniktech.maven.publish.SonatypeHost
+
+plugins {
+ alias(libs.plugins.android.library)
+ alias(libs.plugins.kotlin.android)
+ alias(libs.plugins.maven.publish)
+}
+
+val version = Versioning.loadVersioningData()
+val verCode = version["code"] as Int
+val verPublish = version["publish"] as String
+val verGitSHA = version["gitSha"] as String
+
+android {
+ namespace = "com.pluto.plugins.bundle.core"
+ compileSdk = libs.versions.compileSdk.get().toInt()
+ buildToolsVersion = libs.versions.buildTools.get()
+
+ buildFeatures {
+ buildConfig = true
+ viewBinding = true
+ }
+
+ defaultConfig {
+ minSdk = libs.versions.minSdk.get().toInt()
+ }
+
+ buildTypes {
+ getByName("release") {
+// isDebuggable = true
+ isMinifyEnabled = false
+ isShrinkResources = false
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.toVersion(libs.versions.java.get())
+ targetCompatibility = JavaVersion.toVersion(libs.versions.java.get())
+ }
+
+ kotlinOptions {
+ jvmTarget = libs.versions.java.get()
+ }
+
+ lint {
+ abortOnError = false
+ targetSdk = libs.versions.targetSdk.get().toInt()
+ }
+}
+
+extra["PUBLISH_GROUP_ID"] = "com.androidpluto.plugins"
+extra["PUBLISH_ARTIFACT_ID"] = "bundle-core-no-op"
+extra["PUBLISH_ARTIFACT_NAME"] = "Android Pluto Plugin Bundle"
+extra["PUBLISH_ARTIFACT_DESCRIPTION"] = "Bundle module for Android Pluto plugins"
+
+mavenPublishing {
+ coordinates(
+ groupId = extra["PUBLISH_GROUP_ID"] as String,
+ artifactId = extra["PUBLISH_ARTIFACT_ID"] as String,
+ version = verPublish
+ )
+ pom {
+ name.set(extra["PUBLISH_ARTIFACT_NAME"] as String)
+ description.set(extra["PUBLISH_ARTIFACT_DESCRIPTION"] as String)
+ inceptionYear.set(project.findProperty("pom.inceptionYear") as? String)
+ url.set(project.findProperty("pom.url") as? String)
+ licenses {
+ license {
+ name.set(project.findProperty("pom.license.name") as? String)
+ url.set(project.findProperty("pom.license.url") as? String)
+ }
+ }
+ developers {
+ developer {
+ id.set(project.findProperty("pom.developer.id") as? String)
+ name.set(project.findProperty("pom.developer.name") as? String)
+ email.set(project.findProperty("pom.developer.email") as? String)
+ }
+ }
+ scm {
+ connection.set(project.findProperty("pom.scm.connection") as? String)
+ developerConnection.set(project.findProperty("pom.scm.developerConnection") as? String)
+ url.set(project.findProperty("pom.scm.url") as? String)
+ }
+ }
+ publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL)
+ signAllPublications()
+}
+
+dependencies {
+ api(project(":pluto-plugins:plugins:exceptions:lib-no-op"))
+ api(project(":pluto-plugins:plugins:network:lib-no-op"))
+ api(project(":pluto-plugins:plugins:shared-preferences:lib-no-op"))
+ api(project(":pluto-plugins:plugins:logger:lib-no-op"))
+ api(project(":pluto-plugins:plugins:datastore:lib-no-op"))
+ api(project(":pluto-plugins:plugins:rooms-database:lib-no-op"))
+ api(project(":pluto-plugins:plugins:layout-inspector:lib-no-op"))
+}
diff --git a/pluto-plugins/bundle/lib-no-op/src/main/AndroidManifest.xml b/pluto-plugins/bundle/lib-no-op/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..44008a433
--- /dev/null
+++ b/pluto-plugins/bundle/lib-no-op/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/pluto-plugins/bundle/lib/.gitignore b/pluto-plugins/bundle/lib/.gitignore
new file mode 100644
index 000000000..42afabfd2
--- /dev/null
+++ b/pluto-plugins/bundle/lib/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/pluto-plugins/bundle/lib/build.gradle.kts b/pluto-plugins/bundle/lib/build.gradle.kts
new file mode 100644
index 000000000..715329e28
--- /dev/null
+++ b/pluto-plugins/bundle/lib/build.gradle.kts
@@ -0,0 +1,100 @@
+import com.vanniktech.maven.publish.SonatypeHost
+
+plugins {
+ alias(libs.plugins.android.library)
+ alias(libs.plugins.kotlin.android)
+ alias(libs.plugins.maven.publish)
+}
+
+val version = Versioning.loadVersioningData()
+val verCode = version["code"] as Int
+val verPublish = version["publish"] as String
+val verGitSHA = version["gitSha"] as String
+
+android {
+ namespace = "com.pluto.plugins.bundle.core"
+ compileSdk = libs.versions.compileSdk.get().toInt()
+ buildToolsVersion = libs.versions.buildTools.get()
+
+ buildFeatures {
+ buildConfig = true
+ viewBinding = true
+ }
+
+ defaultConfig {
+ minSdk = libs.versions.minSdk.get().toInt()
+ }
+
+ buildTypes {
+ getByName("release") {
+// isDebuggable = true
+ isMinifyEnabled = false
+ isShrinkResources = false
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.toVersion(libs.versions.java.get())
+ targetCompatibility = JavaVersion.toVersion(libs.versions.java.get())
+ }
+
+ kotlinOptions {
+ jvmTarget = libs.versions.java.get()
+ }
+
+ lint {
+ abortOnError = false
+ targetSdk = libs.versions.targetSdk.get().toInt()
+ }
+}
+
+extra["PUBLISH_GROUP_ID"] = "com.androidpluto.plugins"
+extra["PUBLISH_ARTIFACT_ID"] = "bundle-core"
+extra["PUBLISH_ARTIFACT_NAME"] = "Android Pluto Plugin Bundle"
+extra["PUBLISH_ARTIFACT_DESCRIPTION"] = "Bundle module for Android Pluto plugins"
+
+mavenPublishing {
+ coordinates(
+ groupId = extra["PUBLISH_GROUP_ID"] as String,
+ artifactId = extra["PUBLISH_ARTIFACT_ID"] as String,
+ version = verPublish
+ )
+ pom {
+ name.set(extra["PUBLISH_ARTIFACT_NAME"] as String)
+ description.set(extra["PUBLISH_ARTIFACT_DESCRIPTION"] as String)
+ inceptionYear.set(project.findProperty("pom.inceptionYear") as? String)
+ url.set(project.findProperty("pom.url") as? String)
+ licenses {
+ license {
+ name.set(project.findProperty("pom.license.name") as? String)
+ url.set(project.findProperty("pom.license.url") as? String)
+ }
+ }
+ developers {
+ developer {
+ id.set(project.findProperty("pom.developer.id") as? String)
+ name.set(project.findProperty("pom.developer.name") as? String)
+ email.set(project.findProperty("pom.developer.email") as? String)
+ }
+ }
+ scm {
+ connection.set(project.findProperty("pom.scm.connection") as? String)
+ developerConnection.set(project.findProperty("pom.scm.developerConnection") as? String)
+ url.set(project.findProperty("pom.scm.url") as? String)
+ }
+ }
+ publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL)
+ signAllPublications()
+}
+
+dependencies {
+ api(project(":pluto-plugins:plugins:exceptions:lib"))
+ api(project(":pluto-plugins:plugins:network:lib"))
+// api(project(":pluto-plugins:plugins:network:interceptor-ktor:lib"))
+// api(project(":pluto-plugins:plugins:network:interceptor-okhttp:lib"))
+ api(project(":pluto-plugins:plugins:shared-preferences:lib"))
+ api(project(":pluto-plugins:plugins:logger:lib"))
+ api(project(":pluto-plugins:plugins:datastore:lib"))
+ api(project(":pluto-plugins:plugins:rooms-database:lib"))
+ api(project(":pluto-plugins:plugins:layout-inspector:lib"))
+}
diff --git a/pluto-plugins/bundle/lib/src/main/AndroidManifest.xml b/pluto-plugins/bundle/lib/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..44008a433
--- /dev/null
+++ b/pluto-plugins/bundle/lib/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/pluto-plugins/plugins/datastore/README.md b/pluto-plugins/plugins/datastore/README.md
new file mode 100644
index 000000000..99745bb8a
--- /dev/null
+++ b/pluto-plugins/plugins/datastore/README.md
@@ -0,0 +1,51 @@
+## Integrate Datastore Preferences Plugin in your application
+
+[DataStore Preferences Demo.webm](https://github.com/user-attachments/assets/0b4dfda2-f597-4bcd-8600-2802d0595d0c)
+
+
+### Add Gradle Dependencies
+Pluto Datastore Preferences is distributed through [***mavenCentral***](https://central.sonatype.com/artifact/com.androidpluto.plugins/datastore-pref). To use it, you need to add the following Gradle dependency to your build.gradle file of you android app module.
+
+> Note: add the `no-op` variant to isolate the plugin from release builds.
+```groovy
+dependencies {
+ debugImplementation "com.androidpluto.plugins:datastore-pref:$plutoVersion"
+ releaseImplementation "com.androidpluto.plugins:datastore-pref-no-op:$plutoVersion"
+}
+```
+
+
+### Install plugin to Pluto
+
+Now to start using the plugin, add it to Pluto
+```kotlin
+Pluto.Installer(this)
+ .addPlugin(PlutoDatastorePreferencesPlugin())
+ .install()
+```
+
+
+### Start watching Datastore Preference
+
+Create intance of DataStore Preferences and start watching in Pluto.
+```kotlin
+val Context.appPreferences by preferencesDataStore(
+ name = PREF_NAME
+)
+
+PlutoDatastoreWatcher.watch(PREF_NAME, appPreferences)
+```
+
+
+π You are all done!
+
+Now re-build and run your app and open Pluto, you will see the Datastore Preferences plugin installed.
+
+
+
+### Open Plugin view programmatically
+To open Datastore plugin screen via code, use this
+```kotlin
+Pluto.open(PlutoDatastorePreferencesPlugin.ID)
+```
+
diff --git a/pluto-plugins/plugins/datastore/lib-no-op/.gitignore b/pluto-plugins/plugins/datastore/lib-no-op/.gitignore
new file mode 100644
index 000000000..42afabfd2
--- /dev/null
+++ b/pluto-plugins/plugins/datastore/lib-no-op/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/pluto-plugins/plugins/datastore/lib-no-op/build.gradle.kts b/pluto-plugins/plugins/datastore/lib-no-op/build.gradle.kts
new file mode 100644
index 000000000..f1bb514c0
--- /dev/null
+++ b/pluto-plugins/plugins/datastore/lib-no-op/build.gradle.kts
@@ -0,0 +1,92 @@
+import com.vanniktech.maven.publish.SonatypeHost
+
+plugins {
+ alias(libs.plugins.android.library)
+ alias(libs.plugins.kotlin.android)
+ alias(libs.plugins.maven.publish)
+}
+
+val version = Versioning.loadVersioningData()
+val verCode = version["code"] as Int
+val verPublish = version["publish"] as String
+val verGitSHA = version["gitSha"] as String
+
+android {
+ namespace = "com.pluto.plugins.datastore.pref"
+
+ compileSdk = libs.versions.compileSdk.get().toInt()
+ buildToolsVersion = libs.versions.buildTools.get()
+
+ buildFeatures {
+ viewBinding = true
+ }
+
+ defaultConfig {
+ minSdk = libs.versions.minSdk.get().toInt()
+ }
+
+ buildTypes {
+ getByName("release") {
+// isDebuggable = true
+ isMinifyEnabled = false
+ isShrinkResources = false
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.toVersion(libs.versions.java.get())
+ targetCompatibility = JavaVersion.toVersion(libs.versions.java.get())
+ }
+
+ kotlinOptions {
+ jvmTarget = libs.versions.java.get()
+ }
+
+ lint {
+ abortOnError = false
+ targetSdk = libs.versions.targetSdk.get().toInt()
+ }
+}
+
+extra["PUBLISH_GROUP_ID"] = "com.androidpluto.plugins"
+extra["PUBLISH_ARTIFACT_ID"] = "datastore-pref-no-op"
+extra["PUBLISH_ARTIFACT_NAME"] = "Android Pluto Datastore Preferences Plugin"
+extra["PUBLISH_ARTIFACT_DESCRIPTION"] = "Plugin to manage Datastore preferences in Android Pluto"
+
+mavenPublishing {
+ coordinates(
+ groupId = extra["PUBLISH_GROUP_ID"] as String,
+ artifactId = extra["PUBLISH_ARTIFACT_ID"] as String,
+ version = verPublish
+ )
+ pom {
+ name.set(extra["PUBLISH_ARTIFACT_NAME"] as String)
+ description.set(extra["PUBLISH_ARTIFACT_DESCRIPTION"] as String)
+ inceptionYear.set(project.findProperty("pom.inceptionYear") as? String)
+ url.set(project.findProperty("pom.url") as? String)
+ licenses {
+ license {
+ name.set(project.findProperty("pom.license.name") as? String)
+ url.set(project.findProperty("pom.license.url") as? String)
+ }
+ }
+ developers {
+ developer {
+ id.set(project.findProperty("pom.developer.id") as? String)
+ name.set(project.findProperty("pom.developer.name") as? String)
+ email.set(project.findProperty("pom.developer.email") as? String)
+ }
+ }
+ scm {
+ connection.set(project.findProperty("pom.scm.connection") as? String)
+ developerConnection.set(project.findProperty("pom.scm.developerConnection") as? String)
+ url.set(project.findProperty("pom.scm.url") as? String)
+ }
+ }
+ publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL)
+ signAllPublications()
+}
+
+dependencies {
+ implementation(libs.datastore.preferences)
+}
diff --git a/pluto-plugins/plugins/datastore/lib-no-op/src/main/AndroidManifest.xml b/pluto-plugins/plugins/datastore/lib-no-op/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..44008a433
--- /dev/null
+++ b/pluto-plugins/plugins/datastore/lib-no-op/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/pluto-plugins/plugins/datastore/lib-no-op/src/main/java/com/pluto/plugins/datastore/pref/PlutoDatastorePreferencesPlugin.kt b/pluto-plugins/plugins/datastore/lib-no-op/src/main/java/com/pluto/plugins/datastore/pref/PlutoDatastorePreferencesPlugin.kt
new file mode 100644
index 000000000..b0270034f
--- /dev/null
+++ b/pluto-plugins/plugins/datastore/lib-no-op/src/main/java/com/pluto/plugins/datastore/pref/PlutoDatastorePreferencesPlugin.kt
@@ -0,0 +1,8 @@
+package com.pluto.plugins.datastore.pref
+
+@SuppressWarnings("UnusedPrivateMember")
+class PlutoDatastorePreferencesPlugin @JvmOverloads constructor(identifier: String = ID) {
+ companion object {
+ const val ID = "datastore-preferences"
+ }
+}
diff --git a/pluto-plugins/plugins/datastore/lib-no-op/src/main/java/com/pluto/plugins/datastore/pref/PlutoDatastoreWatcher.kt b/pluto-plugins/plugins/datastore/lib-no-op/src/main/java/com/pluto/plugins/datastore/pref/PlutoDatastoreWatcher.kt
new file mode 100644
index 000000000..31607ad61
--- /dev/null
+++ b/pluto-plugins/plugins/datastore/lib-no-op/src/main/java/com/pluto/plugins/datastore/pref/PlutoDatastoreWatcher.kt
@@ -0,0 +1,10 @@
+package com.pluto.plugins.datastore.pref
+
+import androidx.datastore.core.DataStore
+import androidx.datastore.preferences.core.Preferences
+
+@Suppress("UnusedPrivateMember", "EmptyFunctionBlock")
+object PlutoDatastoreWatcher {
+ fun watch(name: String, store: DataStore) { } // noop
+ fun remove(name: String) { } // noop
+}
diff --git a/pluto-plugins/plugins/datastore/lib/.gitignore b/pluto-plugins/plugins/datastore/lib/.gitignore
new file mode 100644
index 000000000..42afabfd2
--- /dev/null
+++ b/pluto-plugins/plugins/datastore/lib/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/pluto-plugins/plugins/datastore/lib/build.gradle.kts b/pluto-plugins/plugins/datastore/lib/build.gradle.kts
new file mode 100644
index 000000000..88be0581f
--- /dev/null
+++ b/pluto-plugins/plugins/datastore/lib/build.gradle.kts
@@ -0,0 +1,105 @@
+import com.vanniktech.maven.publish.SonatypeHost
+
+plugins {
+ alias(libs.plugins.android.library)
+ alias(libs.plugins.kotlin.android)
+ alias(libs.plugins.maven.publish)
+ alias(libs.plugins.ksp)
+}
+
+val version = Versioning.loadVersioningData()
+val verCode = version["code"] as Int
+val verPublish = version["publish"] as String
+val verGitSHA = version["gitSha"] as String
+
+android {
+ namespace = "com.pluto.plugins.datastore.pref"
+ resourcePrefix = "pluto_dts___"
+
+ compileSdk = libs.versions.compileSdk.get().toInt()
+ buildToolsVersion = libs.versions.buildTools.get()
+
+ buildFeatures {
+ buildConfig = true
+ viewBinding = true
+ }
+
+ defaultConfig {
+ minSdk = libs.versions.minSdk.get().toInt()
+
+ buildConfigField("String", "VERSION_NAME", "\"$verPublish\"")
+ buildConfigField("long", "VERSION_CODE", "$verCode")
+ buildConfigField("String", "GIT_SHA", "\"$verGitSHA\"")
+ }
+
+ buildTypes {
+ getByName("release") {
+// isDebuggable = true
+ isMinifyEnabled = false
+ isShrinkResources = false
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.toVersion(libs.versions.java.get())
+ targetCompatibility = JavaVersion.toVersion(libs.versions.java.get())
+ }
+
+ kotlinOptions {
+ jvmTarget = libs.versions.java.get()
+ }
+
+ lint {
+ abortOnError = false
+ targetSdk = libs.versions.targetSdk.get().toInt()
+ }
+}
+
+extra["PUBLISH_GROUP_ID"] = "com.androidpluto.plugins"
+extra["PUBLISH_ARTIFACT_ID"] = "datastore-pref"
+extra["PUBLISH_ARTIFACT_NAME"] = "Android Pluto Datastore Preferences Plugin"
+extra["PUBLISH_ARTIFACT_DESCRIPTION"] = "Plugin to manage Datastore preferences in Android Pluto"
+
+mavenPublishing {
+ coordinates(
+ groupId = extra["PUBLISH_GROUP_ID"] as String,
+ artifactId = extra["PUBLISH_ARTIFACT_ID"] as String,
+ version = verPublish
+ )
+ pom {
+ name.set(extra["PUBLISH_ARTIFACT_NAME"] as String)
+ description.set(extra["PUBLISH_ARTIFACT_DESCRIPTION"] as String)
+ inceptionYear.set(project.findProperty("pom.inceptionYear") as? String)
+ url.set(project.findProperty("pom.url") as? String)
+ licenses {
+ license {
+ name.set(project.findProperty("pom.license.name") as? String)
+ url.set(project.findProperty("pom.license.url") as? String)
+ }
+ }
+ developers {
+ developer {
+ id.set(project.findProperty("pom.developer.id") as? String)
+ name.set(project.findProperty("pom.developer.name") as? String)
+ email.set(project.findProperty("pom.developer.email") as? String)
+ }
+ }
+ scm {
+ connection.set(project.findProperty("pom.scm.connection") as? String)
+ developerConnection.set(project.findProperty("pom.scm.developerConnection") as? String)
+ url.set(project.findProperty("pom.scm.url") as? String)
+ }
+ }
+ publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL)
+ signAllPublications()
+}
+
+dependencies {
+ implementation(project(":pluto-plugins:base:lib"))
+
+ implementation(libs.androidx.core)
+ implementation(libs.datastore.preferences)
+
+ implementation(libs.moshi)
+ ksp(libs.moshi.codegen)
+}
diff --git a/pluto-plugins/plugins/datastore/lib/src/main/AndroidManifest.xml b/pluto-plugins/plugins/datastore/lib/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..44008a433
--- /dev/null
+++ b/pluto-plugins/plugins/datastore/lib/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/pluto-plugins/plugins/datastore/lib/src/main/java/com/pluto/plugins/datastore/pref/BaseFragment.kt b/pluto-plugins/plugins/datastore/lib/src/main/java/com/pluto/plugins/datastore/pref/BaseFragment.kt
new file mode 100644
index 000000000..1aa50dc07
--- /dev/null
+++ b/pluto-plugins/plugins/datastore/lib/src/main/java/com/pluto/plugins/datastore/pref/BaseFragment.kt
@@ -0,0 +1,5 @@
+package com.pluto.plugins.datastore.pref
+
+import androidx.fragment.app.Fragment
+
+internal class BaseFragment : Fragment(R.layout.pluto_dts___fragment_base)
diff --git a/pluto-plugins/plugins/datastore/lib/src/main/java/com/pluto/plugins/datastore/pref/PlutoDatastorePreferencesPlugin.kt b/pluto-plugins/plugins/datastore/lib/src/main/java/com/pluto/plugins/datastore/pref/PlutoDatastorePreferencesPlugin.kt
new file mode 100644
index 000000000..d4ecd5216
--- /dev/null
+++ b/pluto-plugins/plugins/datastore/lib/src/main/java/com/pluto/plugins/datastore/pref/PlutoDatastorePreferencesPlugin.kt
@@ -0,0 +1,39 @@
+package com.pluto.plugins.datastore.pref
+
+import androidx.fragment.app.Fragment
+import com.pluto.plugin.DeveloperDetails
+import com.pluto.plugin.Plugin
+import com.pluto.plugin.PluginConfiguration
+
+class PlutoDatastorePreferencesPlugin() : Plugin(ID) {
+
+ @SuppressWarnings("UnusedPrivateMember")
+ @Deprecated("Use the default constructor PlutoDatastorePreferencesPlugin() instead.")
+ constructor(identifier: String) : this()
+
+ override fun getConfig() = PluginConfiguration(
+ name = "DataStore Preferences",
+ version = BuildConfig.VERSION_NAME,
+ icon = R.drawable.pluto_dts___ic_logo
+ )
+
+ override fun getDeveloperDetails(): DeveloperDetails {
+ return DeveloperDetails(
+ website = "https://androidpluto.com",
+ vcsLink = "https://github.com/androidPluto/pluto",
+ twitter = "https://twitter.com/android_pluto"
+ )
+ }
+
+ override fun getView(): Fragment {
+ return BaseFragment()
+ }
+
+ override fun onPluginInstalled() {}
+
+ override fun onPluginDataCleared() {}
+
+ companion object {
+ const val ID = "datastore-preferences"
+ }
+}
diff --git a/pluto-plugins/plugins/datastore/lib/src/main/java/com/pluto/plugins/datastore/pref/PlutoDatastoreWatcher.kt b/pluto-plugins/plugins/datastore/lib/src/main/java/com/pluto/plugins/datastore/pref/PlutoDatastoreWatcher.kt
new file mode 100644
index 000000000..52eae9caa
--- /dev/null
+++ b/pluto-plugins/plugins/datastore/lib/src/main/java/com/pluto/plugins/datastore/pref/PlutoDatastoreWatcher.kt
@@ -0,0 +1,41 @@
+package com.pluto.plugins.datastore.pref
+
+import androidx.annotation.Keep
+import androidx.datastore.core.DataStore
+import androidx.datastore.preferences.core.Preferences
+import com.pluto.utilities.selector.SelectorOption
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.update
+
+object PlutoDatastoreWatcher {
+
+ internal val sources = MutableStateFlow>(emptySet())
+
+ fun watch(name: String, store: DataStore) {
+ sources.update { oldSet ->
+ mutableSetOf().apply {
+ addAll(oldSet)
+ add(PreferenceHolder(name, store))
+ }
+ }
+ }
+
+ fun remove(name: String) {
+ sources.update { oldSet ->
+ mutableSetOf().apply {
+ oldSet.forEach {
+ if (it.name != name) add(it)
+ }
+ }
+ }
+ }
+
+ internal fun getSource(name: String): PreferenceHolder {
+ return sources.value.toList().first { it.name == name }
+ }
+}
+
+@Keep
+internal data class PreferenceHolder(val name: String, val preferences: DataStore) : SelectorOption() {
+ override fun displayText(): CharSequence = name
+}
diff --git a/pluto-plugins/plugins/datastore/lib/src/main/java/com/pluto/plugins/datastore/pref/Session.kt b/pluto-plugins/plugins/datastore/lib/src/main/java/com/pluto/plugins/datastore/pref/Session.kt
new file mode 100644
index 000000000..62c672477
--- /dev/null
+++ b/pluto-plugins/plugins/datastore/lib/src/main/java/com/pluto/plugins/datastore/pref/Session.kt
@@ -0,0 +1,5 @@
+package com.pluto.plugins.datastore.pref
+
+internal object Session {
+ var searchText: String? = null
+}
diff --git a/pluto-plugins/plugins/datastore/lib/src/main/java/com/pluto/plugins/datastore/pref/internal/DatastorePrefUtils.kt b/pluto-plugins/plugins/datastore/lib/src/main/java/com/pluto/plugins/datastore/pref/internal/DatastorePrefUtils.kt
new file mode 100644
index 000000000..6b41d5500
--- /dev/null
+++ b/pluto-plugins/plugins/datastore/lib/src/main/java/com/pluto/plugins/datastore/pref/internal/DatastorePrefUtils.kt
@@ -0,0 +1,38 @@
+package com.pluto.plugins.datastore.pref.internal
+
+import android.content.Context
+import com.pluto.plugins.datastore.pref.PlutoDatastoreWatcher
+import com.pluto.plugins.datastore.pref.PreferenceHolder
+import com.pluto.utilities.list.ListItem
+import com.pluto.utilities.views.keyvalue.KeyValuePairEditMetaData
+import com.squareup.moshi.JsonAdapter
+import com.squareup.moshi.Moshi
+import com.squareup.moshi.Types
+
+@SuppressWarnings("UseDataClass")
+internal class DatastorePrefUtils(context: Context) {
+
+ private val preferences: Preferences = Preferences(context)
+ private val moshi: Moshi = Moshi.Builder().build()
+ private val moshiAdapter: JsonAdapter?> = moshi.adapter(Types.newParameterizedType(List::class.java, String::class.java))
+
+ internal var selectedPreferenceFiles: List = arrayListOf()
+ get() {
+ return preferences.selectedPreferenceFiles?.let {
+ moshiAdapter.fromJson(it)?.map { label -> PlutoDatastoreWatcher.getSource(label) }
+ } ?: run {
+ selectedPreferenceFiles = PlutoDatastoreWatcher.sources.value.toList()
+ selectedPreferenceFiles
+ }
+ }
+ set(value) {
+ preferences.selectedPreferenceFiles = moshiAdapter.toJson(value.map { it.name })
+ field = value
+ }
+}
+
+internal data class DatastorePrefKeyValuePair(
+ val key: String,
+ val value: Any?,
+ val prefLabel: String?
+) : ListItem(), KeyValuePairEditMetaData
diff --git a/pluto-plugins/plugins/datastore/lib/src/main/java/com/pluto/plugins/datastore/pref/internal/EditProcessor.kt b/pluto-plugins/plugins/datastore/lib/src/main/java/com/pluto/plugins/datastore/pref/internal/EditProcessor.kt
new file mode 100644
index 000000000..b85a501e4
--- /dev/null
+++ b/pluto-plugins/plugins/datastore/lib/src/main/java/com/pluto/plugins/datastore/pref/internal/EditProcessor.kt
@@ -0,0 +1,35 @@
+package com.pluto.plugins.datastore.pref.internal
+
+import com.pluto.utilities.views.keyvalue.KeyValuePairEditInputType
+import com.pluto.utilities.views.keyvalue.KeyValuePairEditRequest
+
+internal fun DatastorePrefKeyValuePair.toEditorData(): KeyValuePairEditRequest {
+ return KeyValuePairEditRequest(
+ key = key,
+ value = value?.toString(),
+ hint = when (value) {
+ is Int, is Long -> "12345"
+ is Boolean -> "true / false"
+ is Float, is Double -> "1234.89"
+ else -> "abcde 123"
+ },
+ inputType = when (value) {
+ is Int, is Long -> KeyValuePairEditInputType.Integer
+ is Float, is Double -> KeyValuePairEditInputType.Float
+ is Boolean -> KeyValuePairEditInputType.Boolean
+ else -> KeyValuePairEditInputType.String
+ },
+ metaData = this
+ )
+}
+
+internal fun DatastorePrefKeyValuePair.fromEditorData(text: String): Any {
+ return when (value) {
+ is Int -> text.toInt()
+ is Double -> text.toDouble()
+ is Long -> text.toLong()
+ is Float -> text.toFloat()
+ is Boolean -> text.toBoolean()
+ else -> text
+ }
+}
diff --git a/pluto-plugins/plugins/datastore/lib/src/main/java/com/pluto/plugins/datastore/pref/internal/Preferences.kt b/pluto-plugins/plugins/datastore/lib/src/main/java/com/pluto/plugins/datastore/pref/internal/Preferences.kt
new file mode 100644
index 000000000..ffb0a8a23
--- /dev/null
+++ b/pluto-plugins/plugins/datastore/lib/src/main/java/com/pluto/plugins/datastore/pref/internal/Preferences.kt
@@ -0,0 +1,18 @@
+package com.pluto.plugins.datastore.pref.internal
+
+import android.content.Context
+
+internal class Preferences(context: Context) {
+
+ private val settingsPrefs by lazy { context.preferences("_pluto_datastore_pref_settings") }
+
+ internal var selectedPreferenceFiles: String?
+ get() = settingsPrefs.getString(SELECTED_PREF_FILE, null)
+ set(value) = settingsPrefs.edit().putString(SELECTED_PREF_FILE, value).apply()
+
+ companion object {
+ private const val SELECTED_PREF_FILE = "selected_datastore_pref_file"
+ }
+}
+
+private fun Context.preferences(name: String, mode: Int = Context.MODE_PRIVATE) = getSharedPreferences(name, mode)
diff --git a/pluto-plugins/plugins/datastore/lib/src/main/java/com/pluto/plugins/datastore/pref/internal/ui/DatastorePrefAdapter.kt b/pluto-plugins/plugins/datastore/lib/src/main/java/com/pluto/plugins/datastore/pref/internal/ui/DatastorePrefAdapter.kt
new file mode 100644
index 000000000..7217a39ed
--- /dev/null
+++ b/pluto-plugins/plugins/datastore/lib/src/main/java/com/pluto/plugins/datastore/pref/internal/ui/DatastorePrefAdapter.kt
@@ -0,0 +1,28 @@
+package com.pluto.plugins.datastore.pref.internal.ui
+
+import android.view.ViewGroup
+import com.pluto.plugins.datastore.pref.internal.DatastorePrefKeyValuePair
+import com.pluto.utilities.list.BaseAdapter
+import com.pluto.utilities.list.DiffAwareHolder
+import com.pluto.utilities.list.ListItem
+
+internal class DatastorePrefAdapter(private val listener: OnActionListener) : BaseAdapter() {
+
+ override fun getItemViewType(item: ListItem): Int? {
+ return when (item) {
+ is DatastorePrefKeyValuePair -> ITEM_TYPE_PAIR
+ else -> null
+ }
+ }
+
+ override fun onViewHolderCreated(parent: ViewGroup, viewType: Int): DiffAwareHolder? {
+ return when (viewType) {
+ ITEM_TYPE_PAIR -> KeyValueItemHolder(parent, listener)
+ else -> null
+ }
+ }
+
+ companion object {
+ const val ITEM_TYPE_PAIR = 1001
+ }
+}
diff --git a/pluto-plugins/plugins/datastore/lib/src/main/java/com/pluto/plugins/datastore/pref/internal/ui/DatastorePrefViewModel.kt b/pluto-plugins/plugins/datastore/lib/src/main/java/com/pluto/plugins/datastore/pref/internal/ui/DatastorePrefViewModel.kt
new file mode 100644
index 000000000..ed77e9e5b
--- /dev/null
+++ b/pluto-plugins/plugins/datastore/lib/src/main/java/com/pluto/plugins/datastore/pref/internal/ui/DatastorePrefViewModel.kt
@@ -0,0 +1,72 @@
+package com.pluto.plugins.datastore.pref.internal.ui
+
+import android.app.Application
+import androidx.datastore.preferences.core.booleanPreferencesKey
+import androidx.datastore.preferences.core.doublePreferencesKey
+import androidx.datastore.preferences.core.edit
+import androidx.datastore.preferences.core.floatPreferencesKey
+import androidx.datastore.preferences.core.intPreferencesKey
+import androidx.datastore.preferences.core.longPreferencesKey
+import androidx.datastore.preferences.core.stringPreferencesKey
+import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.viewModelScope
+import com.pluto.plugins.datastore.pref.PlutoDatastoreWatcher
+import com.pluto.plugins.datastore.pref.PreferenceHolder
+import com.pluto.plugins.datastore.pref.internal.DatastorePrefKeyValuePair
+import com.pluto.plugins.datastore.pref.internal.DatastorePrefUtils
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.launch
+
+internal class DatastorePrefViewModel(application: Application) : AndroidViewModel(application) {
+
+ val preferenceList: LiveData>
+ get() = _preferences
+ private val _preferences = MutableLiveData>()
+
+ private val sharePrefUtils = DatastorePrefUtils(application.applicationContext)
+
+ fun getSelectedPrefFiles(): List = sharePrefUtils.selectedPreferenceFiles
+
+ fun setSelectedPrefFiles(files: List) {
+ sharePrefUtils.selectedPreferenceFiles = files
+ refresh()
+ }
+
+ fun setPrefData(pair: DatastorePrefKeyValuePair, value: Any) {
+ viewModelScope.launch {
+ val preferences = PlutoDatastoreWatcher.sources.value.find {
+ it.name == pair.prefLabel
+ }?.preferences
+
+ preferences?.edit { preference ->
+ when (pair.value) {
+ is Boolean -> preference[booleanPreferencesKey(pair.key)] = value as Boolean
+ is Double -> preference[doublePreferencesKey(pair.key)] = value as Double
+ is Int -> preference[intPreferencesKey(pair.key)] = value as Int
+ is Float -> preference[floatPreferencesKey(pair.key)] = value as Float
+ is Long -> preference[longPreferencesKey(pair.key)] = value as Long
+ is String -> preference[stringPreferencesKey(pair.key)] = value as String
+ else -> {
+ // show some error
+ // add validation before sending data here
+ }
+ }
+ }
+ refresh()
+ }
+ }
+
+ fun refresh() {
+ viewModelScope.launch {
+ val list = arrayListOf()
+ getSelectedPrefFiles().forEach {
+ it.preferences.data.first().asMap().map { (key, value) ->
+ list.add(DatastorePrefKeyValuePair(key.name, value, it.name))
+ }
+ }
+ _preferences.postValue(list)
+ }
+ }
+}
diff --git a/pluto-plugins/plugins/datastore/lib/src/main/java/com/pluto/plugins/datastore/pref/internal/ui/KeyValueItemHolder.kt b/pluto-plugins/plugins/datastore/lib/src/main/java/com/pluto/plugins/datastore/pref/internal/ui/KeyValueItemHolder.kt
new file mode 100644
index 000000000..8e30e50e0
--- /dev/null
+++ b/pluto-plugins/plugins/datastore/lib/src/main/java/com/pluto/plugins/datastore/pref/internal/ui/KeyValueItemHolder.kt
@@ -0,0 +1,55 @@
+package com.pluto.plugins.datastore.pref.internal.ui
+
+import android.view.ViewGroup
+import com.pluto.plugins.datastore.pref.R
+import com.pluto.plugins.datastore.pref.databinding.PlutoDtsItemSharedPrefKeyValueBinding
+import com.pluto.plugins.datastore.pref.internal.DatastorePrefKeyValuePair
+import com.pluto.utilities.extensions.color
+import com.pluto.utilities.extensions.inflate
+import com.pluto.utilities.list.DiffAwareAdapter
+import com.pluto.utilities.list.DiffAwareHolder
+import com.pluto.utilities.list.ListItem
+import com.pluto.utilities.setOnDebounceClickListener
+import com.pluto.utilities.spannable.createSpan
+
+internal class KeyValueItemHolder(
+ parent: ViewGroup,
+ actionListener: DiffAwareAdapter.OnActionListener
+) : DiffAwareHolder(parent.inflate(R.layout.pluto_dts___item_shared_pref_key_value), actionListener) {
+
+ private val binding = PlutoDtsItemSharedPrefKeyValueBinding.bind(itemView)
+ private val key = binding.key
+ private val value = binding.value
+ private val file = binding.file
+
+ override fun onBind(item: ListItem) {
+ if (item is DatastorePrefKeyValuePair) {
+ key.text = item.key
+ val fileName = item.prefLabel
+ file.text = if (fileName != null) {
+ if (fileName.length > MAX_FILENAME_LENGTH) {
+ "${fileName.substring(0, MAX_FILENAME_LENGTH - 2)}..."
+ } else {
+ fileName
+ }
+ } else {
+ itemView.context.createSpan {
+ append(fontColor(light(italic("null")), context.color(com.pluto.plugin.R.color.pluto___text_dark_40)))
+ }
+ }
+ item.value?.let { value.text = it.toString() }
+
+ itemView.setOnDebounceClickListener {
+ onAction("click")
+ }
+ itemView.setOnLongClickListener {
+ onAction("long_click")
+ return@setOnLongClickListener true
+ }
+ }
+ }
+
+ companion object {
+ const val MAX_FILENAME_LENGTH = 18
+ }
+}
diff --git a/pluto-plugins/plugins/datastore/lib/src/main/java/com/pluto/plugins/datastore/pref/internal/ui/ListFragment.kt b/pluto-plugins/plugins/datastore/lib/src/main/java/com/pluto/plugins/datastore/pref/internal/ui/ListFragment.kt
new file mode 100644
index 000000000..0f76f541a
--- /dev/null
+++ b/pluto-plugins/plugins/datastore/lib/src/main/java/com/pluto/plugins/datastore/pref/internal/ui/ListFragment.kt
@@ -0,0 +1,139 @@
+package com.pluto.plugins.datastore.pref.internal.ui
+
+import android.os.Bundle
+import android.view.View
+import androidx.core.widget.doOnTextChanged
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.Observer
+import androidx.lifecycle.lifecycleScope
+import com.pluto.plugin.share.Shareable
+import com.pluto.plugin.share.lazyContentSharer
+import com.pluto.plugins.datastore.pref.PlutoDatastoreWatcher
+import com.pluto.plugins.datastore.pref.PreferenceHolder
+import com.pluto.plugins.datastore.pref.R
+import com.pluto.plugins.datastore.pref.Session
+import com.pluto.plugins.datastore.pref.databinding.PlutoDtsFragmentListBinding
+import com.pluto.plugins.datastore.pref.internal.DatastorePrefKeyValuePair
+import com.pluto.plugins.datastore.pref.internal.fromEditorData
+import com.pluto.plugins.datastore.pref.internal.toEditorData
+import com.pluto.utilities.autoClearInitializer
+import com.pluto.utilities.extensions.hideKeyboard
+import com.pluto.utilities.extensions.linearLayoutManager
+import com.pluto.utilities.list.BaseAdapter
+import com.pluto.utilities.list.CustomItemDecorator
+import com.pluto.utilities.list.DiffAwareAdapter
+import com.pluto.utilities.list.DiffAwareHolder
+import com.pluto.utilities.list.ListItem
+import com.pluto.utilities.selector.lazyDataSelector
+import com.pluto.utilities.setOnDebounceClickListener
+import com.pluto.utilities.viewBinding
+import com.pluto.utilities.views.keyvalue.KeyValuePairEditResult
+import com.pluto.utilities.views.keyvalue.edit.KeyValuePairEditor
+import com.pluto.utilities.views.keyvalue.edit.lazyKeyValuePairEditor
+
+internal class ListFragment : Fragment(R.layout.pluto_dts___fragment_list) {
+ private val binding by viewBinding(PlutoDtsFragmentListBinding::bind)
+ private val viewModel: DatastorePrefViewModel by activityViewModels()
+ private val keyValuePairEditor: KeyValuePairEditor by lazyKeyValuePairEditor()
+ private val prefAdapter: BaseAdapter by autoClearInitializer {
+ DatastorePrefAdapter(onActionListener)
+ }
+ private val contentSharer by lazyContentSharer()
+ private val dataSelector by lazyDataSelector()
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ viewModel.refresh()
+
+ binding.list.apply {
+ adapter = prefAdapter
+ addItemDecoration(CustomItemDecorator(requireContext()))
+ }
+
+ binding.search.doOnTextChanged { text, _, _, _ ->
+ viewLifecycleOwner.lifecycleScope.launchWhenResumed {
+ text?.toString()?.let {
+ Session.searchText = it
+ prefAdapter.list = filteredPrefs(it)
+ if (it.isEmpty()) {
+ binding.list.linearLayoutManager()?.scrollToPositionWithOffset(0, 0)
+ }
+ }
+ }
+ }
+ binding.filter.setOnDebounceClickListener { openFilterView() }
+ binding.search.setText(Session.searchText)
+ viewModel.preferenceList.removeObserver(sharedPrefObserver)
+ viewModel.preferenceList.observe(viewLifecycleOwner, sharedPrefObserver)
+ keyValuePairEditor.result.removeObserver(keyValuePairEditObserver)
+ keyValuePairEditor.result.observe(viewLifecycleOwner, keyValuePairEditObserver)
+
+ binding.close.setOnDebounceClickListener {
+ activity?.finish()
+ }
+ }
+
+ private fun openFilterView() {
+ dataSelector.selectMultiple(
+ title = getString(R.string.pluto_dts___datastore_pref_filter),
+ list = PlutoDatastoreWatcher.sources.value.toList(),
+ preSelected = viewModel.getSelectedPrefFiles()
+ ).observe(viewLifecycleOwner) {
+ val listOfSharePrefFiles = arrayListOf()
+ it.forEach { option ->
+ if (option is PreferenceHolder) {
+ listOfSharePrefFiles.add(option)
+ }
+ }
+ viewModel.setSelectedPrefFiles(listOfSharePrefFiles)
+ }
+ }
+
+ private fun filteredPrefs(search: String): List {
+ var list = emptyList()
+ viewModel.preferenceList.value?.let {
+ list = it.filter { pref ->
+ pref.key.contains(search, true) ||
+ pref.value.toString().contains(search, ignoreCase = true)
+ }
+ }
+ binding.noItemText.visibility = if (list.isEmpty()) View.VISIBLE else View.GONE
+ return list
+ }
+
+ private val keyValuePairEditObserver = Observer {
+ it.value?.let { value ->
+ if (it.metaData is DatastorePrefKeyValuePair) {
+ val pref: DatastorePrefKeyValuePair = it.metaData as DatastorePrefKeyValuePair
+ viewModel.setPrefData(pref, pref.fromEditorData(value))
+ }
+ }
+ }
+
+ private val sharedPrefObserver = Observer> {
+ prefAdapter.list = filteredPrefs(binding.search.text.toString())
+ }
+
+ private val onActionListener = object : DiffAwareAdapter.OnActionListener {
+ override fun onAction(action: String, data: ListItem, holder: DiffAwareHolder) {
+ if (data is DatastorePrefKeyValuePair) {
+ when (action) {
+ "click" -> activity?.let {
+ it.hideKeyboard(viewLifecycleOwner.lifecycleScope) {
+ keyValuePairEditor.edit(data.toEditorData())
+ }
+ }
+
+ "long_click" -> contentSharer.share(
+ Shareable(
+ content = "${data.key} : ${data.value}",
+ title = "Share Shared Preference",
+ fileName = "Preference data from Pluto"
+ )
+ )
+ }
+ }
+ }
+ }
+}
diff --git a/pluto/src/main/res/drawable/pluto___bg_shared_pref_file_badge.xml b/pluto-plugins/plugins/datastore/lib/src/main/res/drawable/pluto_dts___bg_shared_pref_file_badge.xml
similarity index 100%
rename from pluto/src/main/res/drawable/pluto___bg_shared_pref_file_badge.xml
rename to pluto-plugins/plugins/datastore/lib/src/main/res/drawable/pluto_dts___bg_shared_pref_file_badge.xml
diff --git a/pluto-plugins/plugins/datastore/lib/src/main/res/drawable/pluto_dts___ic_back.xml b/pluto-plugins/plugins/datastore/lib/src/main/res/drawable/pluto_dts___ic_back.xml
new file mode 100644
index 000000000..204c2bc76
--- /dev/null
+++ b/pluto-plugins/plugins/datastore/lib/src/main/res/drawable/pluto_dts___ic_back.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/pluto-plugins/plugins/datastore/lib/src/main/res/drawable/pluto_dts___ic_check.xml b/pluto-plugins/plugins/datastore/lib/src/main/res/drawable/pluto_dts___ic_check.xml
new file mode 100644
index 000000000..74ef80b31
--- /dev/null
+++ b/pluto-plugins/plugins/datastore/lib/src/main/res/drawable/pluto_dts___ic_check.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/pluto-plugins/plugins/datastore/lib/src/main/res/drawable/pluto_dts___ic_clear.xml b/pluto-plugins/plugins/datastore/lib/src/main/res/drawable/pluto_dts___ic_clear.xml
new file mode 100644
index 000000000..27e240025
--- /dev/null
+++ b/pluto-plugins/plugins/datastore/lib/src/main/res/drawable/pluto_dts___ic_clear.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/pluto-plugins/plugins/datastore/lib/src/main/res/drawable/pluto_dts___ic_close.xml b/pluto-plugins/plugins/datastore/lib/src/main/res/drawable/pluto_dts___ic_close.xml
new file mode 100644
index 000000000..82eaa9f97
--- /dev/null
+++ b/pluto-plugins/plugins/datastore/lib/src/main/res/drawable/pluto_dts___ic_close.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/pluto-plugins/plugins/datastore/lib/src/main/res/drawable/pluto_dts___ic_expand.xml b/pluto-plugins/plugins/datastore/lib/src/main/res/drawable/pluto_dts___ic_expand.xml
new file mode 100644
index 000000000..0b3592968
--- /dev/null
+++ b/pluto-plugins/plugins/datastore/lib/src/main/res/drawable/pluto_dts___ic_expand.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/pluto-plugins/plugins/datastore/lib/src/main/res/drawable/pluto_dts___ic_filter.xml b/pluto-plugins/plugins/datastore/lib/src/main/res/drawable/pluto_dts___ic_filter.xml
new file mode 100644
index 000000000..38550c5be
--- /dev/null
+++ b/pluto-plugins/plugins/datastore/lib/src/main/res/drawable/pluto_dts___ic_filter.xml
@@ -0,0 +1,14 @@
+
+
+
+
diff --git a/pluto-plugins/plugins/datastore/lib/src/main/res/drawable/pluto_dts___ic_logo.xml b/pluto-plugins/plugins/datastore/lib/src/main/res/drawable/pluto_dts___ic_logo.xml
new file mode 100644
index 000000000..bd55d420d
--- /dev/null
+++ b/pluto-plugins/plugins/datastore/lib/src/main/res/drawable/pluto_dts___ic_logo.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/pluto-plugins/plugins/datastore/lib/src/main/res/layout/pluto_dts___fragment_base.xml b/pluto-plugins/plugins/datastore/lib/src/main/res/layout/pluto_dts___fragment_base.xml
new file mode 100644
index 000000000..84204aaa1
--- /dev/null
+++ b/pluto-plugins/plugins/datastore/lib/src/main/res/layout/pluto_dts___fragment_base.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pluto-plugins/plugins/datastore/lib/src/main/res/layout/pluto_dts___fragment_list.xml b/pluto-plugins/plugins/datastore/lib/src/main/res/layout/pluto_dts___fragment_list.xml
new file mode 100644
index 000000000..850fe4a2c
--- /dev/null
+++ b/pluto-plugins/plugins/datastore/lib/src/main/res/layout/pluto_dts___fragment_list.xml
@@ -0,0 +1,141 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pluto-plugins/plugins/datastore/lib/src/main/res/layout/pluto_dts___item_shared_pref_key_value.xml b/pluto-plugins/plugins/datastore/lib/src/main/res/layout/pluto_dts___item_shared_pref_key_value.xml
new file mode 100644
index 000000000..b475ea891
--- /dev/null
+++ b/pluto-plugins/plugins/datastore/lib/src/main/res/layout/pluto_dts___item_shared_pref_key_value.xml
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pluto-plugins/plugins/datastore/lib/src/main/res/navigation/pluto_dts___navigation.xml b/pluto-plugins/plugins/datastore/lib/src/main/res/navigation/pluto_dts___navigation.xml
new file mode 100644
index 000000000..876f4842a
--- /dev/null
+++ b/pluto-plugins/plugins/datastore/lib/src/main/res/navigation/pluto_dts___navigation.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pluto-plugins/plugins/datastore/lib/src/main/res/values/strings.xml b/pluto-plugins/plugins/datastore/lib/src/main/res/values/strings.xml
new file mode 100644
index 000000000..fb26cedc3
--- /dev/null
+++ b/pluto-plugins/plugins/datastore/lib/src/main/res/values/strings.xml
@@ -0,0 +1,6 @@
+
+ Datastore Preferences
+ Choose Datastore
+ No DataStore Preferences present.\nAdd a DataStore Preference or check Filter settings.
+ Search Preferences
+
\ No newline at end of file
diff --git a/pluto-plugins/plugins/exceptions/README.md b/pluto-plugins/plugins/exceptions/README.md
new file mode 100644
index 000000000..e3df1b787
--- /dev/null
+++ b/pluto-plugins/plugins/exceptions/README.md
@@ -0,0 +1,59 @@
+## Integrate Exceptions Plugin in your application
+
+[Crashes and ANRs Demo.webm](https://github.com/user-attachments/assets/ed690b3b-5d7e-415e-9373-6f6d183a4def)
+
+### Add Gradle Dependencies
+Pluto Exceptions is distributed through [***mavenCentral***](https://central.sonatype.com/artifact/com.androidpluto.plugins/exceptions). To use it, you need to add the following Gradle dependency to your build.gradle file of you android app module.
+
+> Note: add the `no-op` variant to isolate the plugin from release builds.
+```groovy
+dependencies {
+ debugImplementation "com.androidpluto.plugins:exceptions:$plutoVersion"
+ releaseImplementation "com.androidpluto.plugins:exceptions-no-op:$plutoVersion"
+}
+```
+
+
+### Install plugin to Pluto
+
+Now to start using the plugin, add it to Pluto
+```kotlin
+Pluto.Installer(this)
+ .addPlugin(PlutoExceptionsPlugin())
+ .install()
+```
+
+
+### Set Global Exception Handler
+
+To intercept uncaught exceptions in your app, attach `UncaughtExceptionHandler` to PlutoExceptions
+```kotlin
+PlutoExceptions.setExceptionHandler { thread, throwable ->
+ Log.d("exception_demo", "uncaught exception handled on thread: " + thread.name, throwable)
+}
+```
+
+To intercept & report potential ANRs in your app, attach `UncaughtANRHandler` to PlutoExceptions
+```kotlin
+PlutoExceptions.setANRHandler { thread, exception ->
+ Log.d("anr_demo", "potential ANR detected on thread: " + thread.name, exception)
+}
+```
+
+You can also modify the Main thread response time, after which the above callback will be triggered.
+```kotlin
+PlutoExceptions.mainThreadResponseThreshold = 10_000
+```
+
+
+π You are all done!
+
+Now re-build and run your app and open Pluto, you will see the Exceptions plugin installed.
+
+
+
+### Open Plugin view programmatically
+To open Exceptions plugin screen via code, use this
+```kotlin
+Pluto.open(PlutoExceptionsPlugin.ID)
+```
diff --git a/pluto-plugins/plugins/exceptions/lib-no-op/.gitignore b/pluto-plugins/plugins/exceptions/lib-no-op/.gitignore
new file mode 100644
index 000000000..42afabfd2
--- /dev/null
+++ b/pluto-plugins/plugins/exceptions/lib-no-op/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/pluto-plugins/plugins/exceptions/lib-no-op/build.gradle.kts b/pluto-plugins/plugins/exceptions/lib-no-op/build.gradle.kts
new file mode 100644
index 000000000..04cdf7563
--- /dev/null
+++ b/pluto-plugins/plugins/exceptions/lib-no-op/build.gradle.kts
@@ -0,0 +1,91 @@
+import com.vanniktech.maven.publish.SonatypeHost
+
+plugins {
+ alias(libs.plugins.android.library)
+ alias(libs.plugins.kotlin.android)
+ alias(libs.plugins.maven.publish)
+}
+
+val version = Versioning.loadVersioningData()
+val verCode = version["code"] as Int
+val verPublish = version["publish"] as String
+val verGitSHA = version["gitSha"] as String
+
+android {
+ namespace = "com.pluto.plugins.exceptions"
+
+ compileSdk = libs.versions.compileSdk.get().toInt()
+ buildToolsVersion = libs.versions.buildTools.get()
+
+ buildFeatures {
+ viewBinding = true
+ }
+
+ defaultConfig {
+ minSdk = libs.versions.minSdk.get().toInt()
+ }
+
+ buildTypes {
+ getByName("release") {
+// isDebuggable = true
+ isMinifyEnabled = false
+ isShrinkResources = false
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.toVersion(libs.versions.java.get())
+ targetCompatibility = JavaVersion.toVersion(libs.versions.java.get())
+ }
+
+ kotlinOptions {
+ jvmTarget = libs.versions.java.get()
+ }
+
+ lint {
+ abortOnError = false
+ targetSdk = libs.versions.targetSdk.get().toInt()
+ }
+}
+
+extra["PUBLISH_GROUP_ID"] = "com.androidpluto.plugins"
+extra["PUBLISH_ARTIFACT_ID"] = "exceptions-no-op"
+extra["PUBLISH_ARTIFACT_NAME"] = "Android Pluto Exceptions Plugin"
+extra["PUBLISH_ARTIFACT_DESCRIPTION"] = "Plugin to capture expections & ANRs in Android Pluto"
+
+mavenPublishing {
+ coordinates(
+ groupId = extra["PUBLISH_GROUP_ID"] as String,
+ artifactId = extra["PUBLISH_ARTIFACT_ID"] as String,
+ version = verPublish
+ )
+ pom {
+ name.set(extra["PUBLISH_ARTIFACT_NAME"] as String)
+ description.set(extra["PUBLISH_ARTIFACT_DESCRIPTION"] as String)
+ inceptionYear.set(project.findProperty("pom.inceptionYear") as? String)
+ url.set(project.findProperty("pom.url") as? String)
+ licenses {
+ license {
+ name.set(project.findProperty("pom.license.name") as? String)
+ url.set(project.findProperty("pom.license.url") as? String)
+ }
+ }
+ developers {
+ developer {
+ id.set(project.findProperty("pom.developer.id") as? String)
+ name.set(project.findProperty("pom.developer.name") as? String)
+ email.set(project.findProperty("pom.developer.email") as? String)
+ }
+ }
+ scm {
+ connection.set(project.findProperty("pom.scm.connection") as? String)
+ developerConnection.set(project.findProperty("pom.scm.developerConnection") as? String)
+ url.set(project.findProperty("pom.scm.url") as? String)
+ }
+ }
+ publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL)
+ signAllPublications()
+}
+
+dependencies {
+}
diff --git a/pluto-plugins/plugins/exceptions/lib-no-op/src/main/AndroidManifest.xml b/pluto-plugins/plugins/exceptions/lib-no-op/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..44008a433
--- /dev/null
+++ b/pluto-plugins/plugins/exceptions/lib-no-op/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/pluto-plugins/plugins/exceptions/lib-no-op/src/main/java/com/pluto/plugins/exceptions/ANRException.kt b/pluto-plugins/plugins/exceptions/lib-no-op/src/main/java/com/pluto/plugins/exceptions/ANRException.kt
new file mode 100644
index 000000000..0101d7da6
--- /dev/null
+++ b/pluto-plugins/plugins/exceptions/lib-no-op/src/main/java/com/pluto/plugins/exceptions/ANRException.kt
@@ -0,0 +1,7 @@
+package com.pluto.plugins.exceptions
+
+import androidx.annotation.Keep
+
+@Keep
+@SuppressWarnings("UnusedPrivateMember", "EmptyFunctionBlock")
+class ANRException(thread: Thread) : Exception("ANR detected")
diff --git a/pluto-plugins/plugins/exceptions/lib-no-op/src/main/java/com/pluto/plugins/exceptions/PlutoExceptions.kt b/pluto-plugins/plugins/exceptions/lib-no-op/src/main/java/com/pluto/plugins/exceptions/PlutoExceptions.kt
new file mode 100644
index 000000000..63b544242
--- /dev/null
+++ b/pluto-plugins/plugins/exceptions/lib-no-op/src/main/java/com/pluto/plugins/exceptions/PlutoExceptions.kt
@@ -0,0 +1,17 @@
+package com.pluto.plugins.exceptions
+
+@SuppressWarnings("UnusedPrivateMember", "EmptyFunctionBlock")
+object PlutoExceptions {
+
+ /**
+ * The threshold for main thread response
+ * time before resulting in ANR.
+ */
+ var mainThreadResponseThreshold = 0
+
+ fun setExceptionHandler(uncaughtExceptionHandler: Thread.UncaughtExceptionHandler) {
+ }
+
+ fun setANRHandler(anrHandler: UncaughtANRHandler) {
+ }
+}
diff --git a/pluto-plugins/plugins/exceptions/lib-no-op/src/main/java/com/pluto/plugins/exceptions/PlutoExceptionsPlugin.kt b/pluto-plugins/plugins/exceptions/lib-no-op/src/main/java/com/pluto/plugins/exceptions/PlutoExceptionsPlugin.kt
new file mode 100644
index 000000000..06cd65719
--- /dev/null
+++ b/pluto-plugins/plugins/exceptions/lib-no-op/src/main/java/com/pluto/plugins/exceptions/PlutoExceptionsPlugin.kt
@@ -0,0 +1,8 @@
+package com.pluto.plugins.exceptions
+
+@SuppressWarnings("UnusedPrivateMember")
+class PlutoExceptionsPlugin @JvmOverloads constructor(identifier: String = ID) {
+ companion object {
+ const val ID = "exceptions"
+ }
+}
diff --git a/pluto-plugins/plugins/exceptions/lib-no-op/src/main/java/com/pluto/plugins/exceptions/UncaughtANRHandler.kt b/pluto-plugins/plugins/exceptions/lib-no-op/src/main/java/com/pluto/plugins/exceptions/UncaughtANRHandler.kt
new file mode 100644
index 000000000..ed716d5da
--- /dev/null
+++ b/pluto-plugins/plugins/exceptions/lib-no-op/src/main/java/com/pluto/plugins/exceptions/UncaughtANRHandler.kt
@@ -0,0 +1,5 @@
+package com.pluto.plugins.exceptions
+
+fun interface UncaughtANRHandler {
+ fun uncaughtANR(thread: Thread, exception: ANRException)
+}
diff --git a/pluto-plugins/plugins/exceptions/lib/.gitignore b/pluto-plugins/plugins/exceptions/lib/.gitignore
new file mode 100644
index 000000000..42afabfd2
--- /dev/null
+++ b/pluto-plugins/plugins/exceptions/lib/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/pluto-plugins/plugins/exceptions/lib/build.gradle.kts b/pluto-plugins/plugins/exceptions/lib/build.gradle.kts
new file mode 100644
index 000000000..bb6085b9d
--- /dev/null
+++ b/pluto-plugins/plugins/exceptions/lib/build.gradle.kts
@@ -0,0 +1,108 @@
+import com.vanniktech.maven.publish.SonatypeHost
+
+plugins {
+ alias(libs.plugins.android.library)
+ alias(libs.plugins.kotlin.android)
+ alias(libs.plugins.kotlin.parcelize)
+ alias(libs.plugins.ksp)
+ alias(libs.plugins.maven.publish)
+}
+
+val version = Versioning.loadVersioningData()
+val verCode = version["code"] as Int
+val verPublish = version["publish"] as String
+val verGitSHA = version["gitSha"] as String
+
+android {
+ namespace = "com.pluto.plugins.exceptions"
+ resourcePrefix = "pluto_excep___"
+
+ compileSdk = libs.versions.compileSdk.get().toInt()
+ buildToolsVersion = libs.versions.buildTools.get()
+
+ buildFeatures {
+ buildConfig = true
+ viewBinding = true
+ }
+
+ defaultConfig {
+ minSdk = libs.versions.minSdk.get().toInt()
+
+ buildConfigField("String", "VERSION_NAME", "\"$verPublish\"")
+ buildConfigField("long", "VERSION_CODE", "$verCode")
+ buildConfigField("String", "GIT_SHA", "\"$verGitSHA\"")
+ }
+
+ buildTypes {
+ getByName("release") {
+// isDebuggable = true
+ isMinifyEnabled = false
+ isShrinkResources = false
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.toVersion(libs.versions.java.get())
+ targetCompatibility = JavaVersion.toVersion(libs.versions.java.get())
+ }
+
+ kotlinOptions {
+ jvmTarget = libs.versions.java.get()
+ }
+
+ lint {
+ abortOnError = false
+ targetSdk = libs.versions.targetSdk.get().toInt()
+ }
+}
+
+extra["PUBLISH_GROUP_ID"] = "com.androidpluto.plugins"
+extra["PUBLISH_ARTIFACT_ID"] = "exceptions"
+extra["PUBLISH_ARTIFACT_NAME"] = "Android Pluto Exceptions Plugin"
+extra["PUBLISH_ARTIFACT_DESCRIPTION"] = "Plugin to capture expections & ANRs in Android Pluto"
+
+mavenPublishing {
+ coordinates(
+ groupId = extra["PUBLISH_GROUP_ID"] as String,
+ artifactId = extra["PUBLISH_ARTIFACT_ID"] as String,
+ version = verPublish
+ )
+ pom {
+ name.set(extra["PUBLISH_ARTIFACT_NAME"] as String)
+ description.set(extra["PUBLISH_ARTIFACT_DESCRIPTION"] as String)
+ inceptionYear.set(project.findProperty("pom.inceptionYear") as? String)
+ url.set(project.findProperty("pom.url") as? String)
+ licenses {
+ license {
+ name.set(project.findProperty("pom.license.name") as? String)
+ url.set(project.findProperty("pom.license.url") as? String)
+ }
+ }
+ developers {
+ developer {
+ id.set(project.findProperty("pom.developer.id") as? String)
+ name.set(project.findProperty("pom.developer.name") as? String)
+ email.set(project.findProperty("pom.developer.email") as? String)
+ }
+ }
+ scm {
+ connection.set(project.findProperty("pom.scm.connection") as? String)
+ developerConnection.set(project.findProperty("pom.scm.developerConnection") as? String)
+ url.set(project.findProperty("pom.scm.url") as? String)
+ }
+ }
+ publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL)
+ signAllPublications()
+}
+
+dependencies {
+ implementation(project(":pluto-plugins:base:lib"))
+
+ implementation(libs.androidx.core)
+
+ implementation(libs.moshi)
+ ksp(libs.moshi.codegen)
+
+ implementation(libs.room)
+ ksp(libs.room.compiler)
+}
diff --git a/pluto-plugins/plugins/exceptions/lib/detekt-baseline.xml b/pluto-plugins/plugins/exceptions/lib/detekt-baseline.xml
new file mode 100644
index 000000000..126e7dbf4
--- /dev/null
+++ b/pluto-plugins/plugins/exceptions/lib/detekt-baseline.xml
@@ -0,0 +1,8 @@
+
+
+
+
+ TopLevelPropertyNaming:DetailsFragment.kt$private const val SHARE_SECTION_DIVIDER = "\n\n==================\n\n"
+ TopLevelPropertyNaming:DetailsFragment.kt$private const val STACK_TRACE_LENGTH = 25
+
+
diff --git a/pluto-plugins/plugins/exceptions/lib/src/main/AndroidManifest.xml b/pluto-plugins/plugins/exceptions/lib/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..44008a433
--- /dev/null
+++ b/pluto-plugins/plugins/exceptions/lib/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/ANRException.kt b/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/ANRException.kt
new file mode 100644
index 000000000..5225a93db
--- /dev/null
+++ b/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/ANRException.kt
@@ -0,0 +1,78 @@
+package com.pluto.plugins.exceptions
+
+import androidx.annotation.Keep
+import com.pluto.plugins.exceptions.internal.ProcessThread
+import com.pluto.plugins.exceptions.internal.asStringArray
+import java.io.ByteArrayOutputStream
+import java.io.PrintStream
+import java.util.Locale
+
+@Keep
+class ANRException(thread: Thread) : Exception("ANR detected") {
+
+ val threadStateMap: String
+ get() = generateProcessMap()
+ internal val threadStateList: List
+ get() = generateProcessList()
+
+ init {
+ stackTrace = thread.stackTrace
+ }
+
+ /**
+ * Logs the current process and all its threads
+ */
+ private fun generateProcessMap(): String {
+ val bos = ByteArrayOutputStream()
+ val ps = PrintStream(bos)
+ printProcessMap(ps)
+ return String(bos.toByteArray())
+ }
+
+ /**
+ * Prints the current process and all its threads
+ *
+ * @param ps the [PrintStream] to which the
+ * info is written
+ */
+ private fun printProcessMap(ps: PrintStream) {
+ // Get all stack traces in the system
+ val stackTraces = Thread.getAllStackTraces()
+ ps.println("Process map:")
+ for (thread in stackTraces.keys) {
+ if (!stackTraces[thread].isNullOrEmpty()) {
+ printThread(ps, Locale.getDefault(), thread, stackTraces[thread]!!)
+ ps.println()
+ }
+ }
+ }
+
+ /**
+ * Prints the given thread
+ * @param ps the [PrintStream] to which the
+ * info is written
+ * @param l the [Locale] to use
+ * @param thread the [Thread] to print
+ * @param stack the [Thread]'s stack trace
+ */
+ private fun printThread(ps: PrintStream, l: Locale, thread: Thread, stack: Array) {
+ ps.println(String.format(l, "\t%s (%s)", thread.name, thread.state))
+ for (element in stack) {
+ element.apply {
+ ps.println(String.format(l, "\t\t%s.%s(%s:%d)", className, methodName, fileName, lineNumber))
+ }
+ }
+ }
+
+ private fun generateProcessList(): List {
+ val list = arrayListOf()
+ val stackTraces = Thread.getAllStackTraces()
+ for (thread in stackTraces.keys) {
+ if (!stackTraces[thread].isNullOrEmpty()) {
+ val process = ProcessThread(thread.name, thread.state.name, stackTraces[thread]!!.asStringArray())
+ list.add(process)
+ }
+ }
+ return list
+ }
+}
diff --git a/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/PlutoExceptions.kt b/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/PlutoExceptions.kt
new file mode 100644
index 000000000..2187e03e6
--- /dev/null
+++ b/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/PlutoExceptions.kt
@@ -0,0 +1,52 @@
+package com.pluto.plugins.exceptions
+
+import android.content.Context
+import com.pluto.plugins.exceptions.internal.Session
+import com.pluto.plugins.exceptions.internal.anr.AnrSupervisor
+import com.pluto.plugins.exceptions.internal.anr.AnrSupervisor.Companion.DEFAULT_MAIN_THREAD_RESPONSE_THRESHOLD
+import com.pluto.plugins.exceptions.internal.crash.CrashHandler
+import com.pluto.plugins.exceptions.internal.persistence.ExceptionDBHandler
+
+object PlutoExceptions {
+
+ private var crashHandler: CrashHandler? = null
+ private var anrHandler: AnrSupervisor? = null
+ internal lateinit var devIdentifier: String
+ private set
+ internal val session = Session()
+ internal lateinit var appPackageName: String
+
+ internal fun initialize(context: Context, identifier: String) {
+ appPackageName = context.packageName
+ crashHandler = CrashHandler(context)
+ anrHandler = AnrSupervisor().apply { start() }
+ devIdentifier = identifier
+ Thread.setDefaultUncaughtExceptionHandler(crashHandler)
+ }
+
+ /**
+ * The threshold for main thread response
+ * time before resulting in ANR.
+ */
+ var mainThreadResponseThreshold = DEFAULT_MAIN_THREAD_RESPONSE_THRESHOLD
+
+ fun setExceptionHandler(uncaughtExceptionHandler: Thread.UncaughtExceptionHandler) {
+ this.crashHandler?.let {
+ it.setExceptionHandler(uncaughtExceptionHandler)
+ return
+ }
+ throw IllegalStateException("UncaughtExceptionHandler cannot be set as Pluto is not initialised.")
+ }
+
+ fun setANRHandler(anrHandler: UncaughtANRHandler) {
+ this.anrHandler?.let {
+ it.setListener(anrHandler)
+ return
+ }
+ throw IllegalStateException("UncaughtANRHandler cannot be set as Pluto is not initialised.")
+ }
+
+ internal fun clear() {
+ ExceptionDBHandler.flush()
+ }
+}
diff --git a/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/PlutoExceptionsPlugin.kt b/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/PlutoExceptionsPlugin.kt
new file mode 100644
index 000000000..3a8dd0e85
--- /dev/null
+++ b/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/PlutoExceptionsPlugin.kt
@@ -0,0 +1,44 @@
+package com.pluto.plugins.exceptions
+
+import androidx.fragment.app.Fragment
+import com.pluto.plugin.DeveloperDetails
+import com.pluto.plugin.Plugin
+import com.pluto.plugin.PluginConfiguration
+import com.pluto.plugins.exceptions.internal.BaseFragment
+import com.pluto.plugins.exceptions.internal.persistence.ExceptionDBHandler
+
+class PlutoExceptionsPlugin() : Plugin(ID) {
+
+ @SuppressWarnings("UnusedPrivateMember")
+ @Deprecated("Use the default constructor PlutoExceptionsPlugin() instead.")
+ constructor(identifier: String) : this()
+
+ override fun getConfig() = PluginConfiguration(
+ name = context.getString(R.string.pluto_excep___plugin_name),
+ icon = R.drawable.pluto_excep___ic_plugin_logo,
+ version = BuildConfig.VERSION_NAME
+ )
+
+ override fun getView(): Fragment = BaseFragment()
+
+ override fun getDeveloperDetails(): DeveloperDetails {
+ return DeveloperDetails(
+ website = "https://androidpluto.com",
+ vcsLink = "https://github.com/androidPluto/pluto",
+ twitter = "https://twitter.com/android_pluto"
+ )
+ }
+
+ override fun onPluginDataCleared() {
+ PlutoExceptions.clear()
+ }
+
+ override fun onPluginInstalled() {
+ ExceptionDBHandler.initialize(context)
+ PlutoExceptions.initialize(context, ID)
+ }
+
+ companion object {
+ const val ID = "exceptions"
+ }
+}
diff --git a/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/UncaughtANRHandler.kt b/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/UncaughtANRHandler.kt
new file mode 100644
index 000000000..ed716d5da
--- /dev/null
+++ b/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/UncaughtANRHandler.kt
@@ -0,0 +1,5 @@
+package com.pluto.plugins.exceptions
+
+fun interface UncaughtANRHandler {
+ fun uncaughtANR(thread: Thread, exception: ANRException)
+}
diff --git a/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/BaseFragment.kt b/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/BaseFragment.kt
new file mode 100644
index 000000000..1ad2053ae
--- /dev/null
+++ b/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/BaseFragment.kt
@@ -0,0 +1,6 @@
+package com.pluto.plugins.exceptions.internal
+
+import androidx.fragment.app.Fragment
+import com.pluto.plugins.exceptions.R
+
+class BaseFragment : Fragment(R.layout.pluto_excep___fragment_base)
diff --git a/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/DataModel.kt b/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/DataModel.kt
new file mode 100644
index 000000000..85014f091
--- /dev/null
+++ b/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/DataModel.kt
@@ -0,0 +1,182 @@
+package com.pluto.plugins.exceptions.internal
+
+import android.content.Context
+import androidx.annotation.Keep
+import com.pluto.plugins.exceptions.BuildConfig
+import com.pluto.plugins.exceptions.R
+import com.pluto.plugins.exceptions.internal.extensions.getPriorityString
+import com.pluto.utilities.device.Device
+import com.pluto.utilities.extensions.color
+import com.pluto.utilities.list.ListItem
+import com.pluto.utilities.spannable.createSpan
+import com.squareup.moshi.JsonClass
+
+@Keep
+@JsonClass(generateAdapter = true)
+internal data class ExceptionAllData(
+ val thread: ThreadData? = null,
+ val exception: ExceptionData,
+ val threadStateList: ThreadStates? = null
+)
+
+@Keep
+@JsonClass(generateAdapter = true)
+internal data class ThreadData(
+ val id: Long,
+ val name: String,
+ val priority: Int,
+ val isDaemon: Boolean,
+ val state: String,
+ val group: ThreadGroupData?
+) : ListItem() {
+ val priorityString: String = getPriorityString(priority)
+}
+
+@Keep
+@JsonClass(generateAdapter = true)
+internal data class ThreadGroupData(
+ val name: String,
+ val parent: String,
+ val activeCount: Int
+)
+
+@Keep
+@JsonClass(generateAdapter = true)
+internal data class ExceptionData(
+ val message: String?,
+ val name: String?,
+ val file: String?,
+ val lineNumber: Int,
+ val stackTrace: List,
+ val stackTraceAdditionalLineCount: Int,
+ val timeStamp: Long = System.currentTimeMillis(),
+ val isANRException: Boolean = false
+) : ListItem()
+
+@Keep
+@JsonClass(generateAdapter = true)
+internal data class ThreadStates(
+ val states: List
+) : ListItem()
+
+@Keep
+@JsonClass(generateAdapter = true)
+internal data class ProcessThread(
+ val name: String,
+ val state: String,
+ val stackTrace: List
+) : ListItem()
+
+@Keep
+@JsonClass(generateAdapter = true)
+internal data class DeviceInfo(
+ val appVersionName: String,
+ val appVersionCode: Long,
+ val androidOs: String?,
+ val androidAPILevel: String?,
+ val isRooted: Boolean,
+ val buildBrand: String?,
+ val buildModel: String?,
+ val screenHeightPx: Int,
+ val screenWidthPx: Int,
+ val screenDensityDpi: Int,
+ val screenSizeInch: Double,
+ val screenOrientation: String
+) : ListItem()
+
+internal fun Throwable.asExceptionData(isANR: Boolean = false): ExceptionData {
+ val truncatedStackTrace = stackTrace.asStringArray()
+ return ExceptionData(
+ name = this.toString().replace(": $message", "", true),
+ message = message,
+ stackTrace = truncatedStackTrace,
+ stackTraceAdditionalLineCount = stackTrace.size - truncatedStackTrace.size,
+ file = stackTrace.getOrNull(0)?.fileName,
+ lineNumber = stackTrace.getOrNull(0)?.lineNumber ?: Int.MIN_VALUE,
+ isANRException = isANR
+ )
+}
+
+internal fun Array.asStringArray(stackTraceSize: Int = STACK_TRACE_LENGTH): ArrayList {
+ val array = arrayListOf()
+ take(stackTraceSize).forEach {
+ if (it.isNativeMethod) {
+ array.add("${it.className}.${it.methodName}(Native Method)")
+ } else {
+ array.add("${it.className}.${it.methodName}(${it.fileName}:${it.lineNumber})")
+ }
+ }
+ return array
+}
+
+internal fun Thread.asThreadData(): ThreadData {
+ return ThreadData(
+ id = id,
+ name = name,
+ isDaemon = isDaemon,
+ state = state.name,
+ group = threadGroup.convert(),
+ priority = priority
+ )
+}
+
+internal fun Device.asDeviceInfo(): DeviceInfo {
+ return DeviceInfo(
+ appVersionName = app.version.name,
+ appVersionCode = app.version.code,
+ androidOs = software.androidOs,
+ androidAPILevel = software.androidAPILevel,
+ isRooted = software.isRooted,
+ buildBrand = build.brand,
+ buildModel = build.model,
+ screenHeightPx = screen.heightPx,
+ screenWidthPx = screen.widthPx,
+ screenDensityDpi = screen.density,
+ screenSizeInch = screen.sizeInches,
+ screenOrientation = screen.orientation
+ )
+}
+
+private fun ThreadGroup?.convert(): ThreadGroupData? {
+ this?.let {
+ ThreadGroupData(
+ name = it.name,
+ parent = it.parent.name,
+ activeCount = it.activeCount()
+ )
+ }
+ return null
+}
+
+internal fun getStateStringSpan(context: Context, state: String): CharSequence {
+ return context.createSpan {
+ append(
+ bold(
+ fontColor(
+ state.uppercase(),
+ context.color(
+ when (state) {
+ Thread.State.BLOCKED.name -> com.pluto.plugin.R.color.pluto___red_dark
+ Thread.State.WAITING.name -> com.pluto.plugin.R.color.pluto___orange
+ else -> com.pluto.plugin.R.color.pluto___text_dark_80
+ }
+ )
+ )
+ )
+ )
+ }
+}
+
+@Keep
+@JsonClass(generateAdapter = true)
+data class ReportData(
+ val message: String?,
+ val name: String?,
+ val stackTrace: List,
+ val client: String?,
+ val gitSha: String = BuildConfig.GIT_SHA,
+ val version: String = BuildConfig.VERSION_NAME,
+ val buildType: String = BuildConfig.BUILD_TYPE
+)
+
+internal const val STACK_TRACE_LENGTH = 25
diff --git a/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/Session.kt b/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/Session.kt
new file mode 100644
index 000000000..723d74487
--- /dev/null
+++ b/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/Session.kt
@@ -0,0 +1,5 @@
+package com.pluto.plugins.exceptions.internal
+
+class Session {
+ var lastSearchText: String? = null
+}
diff --git a/pluto/src/main/java/com/mocklets/pluto/modules/exceptions/anrs/AnrSupervisor.kt b/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/anr/AnrSupervisor.kt
similarity index 81%
rename from pluto/src/main/java/com/mocklets/pluto/modules/exceptions/anrs/AnrSupervisor.kt
rename to pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/anr/AnrSupervisor.kt
index 00d37a9cd..d4ec84956 100644
--- a/pluto/src/main/java/com/mocklets/pluto/modules/exceptions/anrs/AnrSupervisor.kt
+++ b/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/anr/AnrSupervisor.kt
@@ -1,6 +1,6 @@
-package com.mocklets.pluto.modules.exceptions.anrs
+package com.pluto.plugins.exceptions.internal.anr
-import com.mocklets.pluto.modules.exceptions.ANRListener
+import com.pluto.plugins.exceptions.UncaughtANRHandler
import java.util.concurrent.Executors
/**
@@ -41,14 +41,14 @@ internal class AnrSupervisor {
mSupervisor.stop()
}
- fun setListener(listener: ANRListener) {
- mSupervisor.setListener(listener)
+ fun setListener(handler: UncaughtANRHandler) {
+ mSupervisor.setListener(handler)
}
companion object {
const val LOGTAG = "Pluto-ANR-watcher"
const val ANR_WATCHER_THREAD_NAME = "pluto-anr-watcher"
const val ANR_WATCHER_TIMEOUT: Long = 10_000
- const val MAIN_THREAD_RESPONSE_THRESHOLD: Long = 2_000
+ const val DEFAULT_MAIN_THREAD_RESPONSE_THRESHOLD: Long = 2_000
}
}
diff --git a/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/anr/AnrSupervisorCallback.kt b/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/anr/AnrSupervisorCallback.kt
new file mode 100644
index 000000000..6a1ec3792
--- /dev/null
+++ b/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/anr/AnrSupervisorCallback.kt
@@ -0,0 +1,26 @@
+package com.pluto.plugins.exceptions.internal.anr
+
+import com.pluto.plugins.exceptions.internal.extensions.notifyAll
+
+/**
+ * A [Runnable] which calls [.notifyAll] when run.
+ */
+internal class AnrSupervisorCallback : Runnable {
+ /**
+ * Returns whether [.run] was called yet
+ *
+ * @return true if called, false if not
+ */
+ /**
+ * Flag storing whether [.run] was called
+ */
+ @get:Synchronized
+ var isCalled = false
+ private set
+
+ @Synchronized
+ override fun run() {
+ isCalled = true
+ this.notifyAll()
+ }
+}
diff --git a/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/anr/AnrSupervisorRunnable.kt b/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/anr/AnrSupervisorRunnable.kt
new file mode 100644
index 000000000..6888efc58
--- /dev/null
+++ b/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/anr/AnrSupervisorRunnable.kt
@@ -0,0 +1,132 @@
+package com.pluto.plugins.exceptions.internal.anr
+
+import android.os.Handler
+import android.os.Looper
+import com.pluto.plugins.exceptions.ANRException
+import com.pluto.plugins.exceptions.PlutoExceptions
+import com.pluto.plugins.exceptions.UncaughtANRHandler
+import com.pluto.plugins.exceptions.internal.ExceptionAllData
+import com.pluto.plugins.exceptions.internal.ThreadStates
+import com.pluto.plugins.exceptions.internal.anr.AnrSupervisor.Companion.ANR_WATCHER_THREAD_NAME
+import com.pluto.plugins.exceptions.internal.anr.AnrSupervisor.Companion.ANR_WATCHER_TIMEOUT
+import com.pluto.plugins.exceptions.internal.anr.AnrSupervisor.Companion.LOGTAG
+import com.pluto.plugins.exceptions.internal.asExceptionData
+import com.pluto.plugins.exceptions.internal.asThreadData
+import com.pluto.plugins.exceptions.internal.extensions.wait
+import com.pluto.plugins.exceptions.internal.persistence.ExceptionDBHandler
+import com.pluto.utilities.DebugLog
+
+/**
+ * A [Runnable] testing the UI thread every 10s until [ ][.stop] is called
+ */
+internal class AnrSupervisorRunnable : Runnable {
+ /**
+ * The [Handler] to access the UI threads message queue
+ */
+ private val mHandler = Handler(Looper.getMainLooper())
+ private var anrHandler: UncaughtANRHandler? = null
+
+ /**
+ * The stop flag
+ */
+ private var mStopped = false
+ /**
+ * Returns whether the stop is completed
+ *
+ * @return true if stop is completed, false if not
+ */
+ /**
+ * Flag indicating the stop was performed
+ */
+ @get:Synchronized
+ var isStopped = true
+ private set
+
+ override fun run() {
+ Thread.currentThread().name = ANR_WATCHER_THREAD_NAME
+ isStopped = false
+
+ while (!Thread.interrupted()) {
+ try {
+ DebugLog.d(LOGTAG, "Check for ANR...")
+
+ // Create new callback
+ val callback = AnrSupervisorCallback()
+
+ // Perform test, Handler should run the callback within 1s
+ synchronized(callback) {
+ mHandler.post(callback)
+ callback.wait(PlutoExceptions.mainThreadResponseThreshold)
+
+ // Check if called
+ if (!callback.isCalled) {
+ val e = ANRException(mHandler.looper.thread)
+ anrHandler?.uncaughtANR(mHandler.looper.thread, e)
+ persistException(mHandler.looper.thread, e)
+// ExceptionDBHandler.persist(e.asExceptionData())
+ // todo save exception to db
+ // Pluto.exceptionRepo.saveANR(e)
+ /** Wait until the thread responds again */
+ callback.wait()
+ } else {
+ DebugLog.d(LOGTAG, "UI Thread responded within 1s")
+ }
+ }
+ // Check if stopped
+ checkStopped()
+ /** Sleep for next test */
+ Thread.sleep(ANR_WATCHER_TIMEOUT)
+ } catch (e: InterruptedException) {
+ break
+ }
+ }
+
+ // Set stop completed flag
+ isStopped = true
+ DebugLog.d(LOGTAG, "ANR supervision stopped")
+ }
+
+ private fun persistException(thread: Thread, exception: ANRException) {
+ ExceptionDBHandler.persist(
+ timestamp = System.currentTimeMillis(),
+ exception = ExceptionAllData(
+ thread = thread.asThreadData(),
+ exception = exception.asExceptionData(isANR = true),
+ threadStateList = ThreadStates(exception.threadStateList)
+ )
+ )
+ }
+
+ @Synchronized
+ @Throws(InterruptedException::class)
+ private fun checkStopped() {
+ if (mStopped) {
+ Thread.sleep(PlutoExceptions.mainThreadResponseThreshold)
+ if (mStopped) {
+ throw InterruptedException()
+ }
+ }
+ }
+
+ /**
+ * Stops the check
+ */
+ @Synchronized
+ fun stop() {
+ DebugLog.d(LOGTAG, "Stopping...")
+ mStopped = true
+ }
+
+ /**
+ * Stops the check
+ */
+ @Synchronized
+ fun unstop() {
+ DebugLog.d(LOGTAG, "Revert stopping...")
+ mStopped = false
+ }
+
+ fun setListener(handler: UncaughtANRHandler) {
+ anrHandler = handler
+ }
+}
diff --git a/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/crash/CrashHandler.kt b/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/crash/CrashHandler.kt
new file mode 100644
index 000000000..ae7c763cd
--- /dev/null
+++ b/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/crash/CrashHandler.kt
@@ -0,0 +1,20 @@
+package com.pluto.plugins.exceptions.internal.crash
+
+import android.content.Context
+import com.pluto.plugins.exceptions.internal.persistence.ExceptionDBHandler
+
+internal class CrashHandler(context: Context) : Thread.UncaughtExceptionHandler {
+
+ private var handler: Thread.UncaughtExceptionHandler? = Thread.getDefaultUncaughtExceptionHandler()
+ private val crashNotification: CrashNotification = CrashNotification(context)
+
+ override fun uncaughtException(t: Thread, e: Throwable) {
+ ExceptionDBHandler.tempPersist(e, t)
+ crashNotification.add()
+ handler?.uncaughtException(t, e)
+ }
+
+ internal fun setExceptionHandler(handler: Thread.UncaughtExceptionHandler) {
+ this.handler = handler
+ }
+}
diff --git a/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/crash/CrashNotification.kt b/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/crash/CrashNotification.kt
new file mode 100644
index 000000000..ff5b100cb
--- /dev/null
+++ b/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/crash/CrashNotification.kt
@@ -0,0 +1,69 @@
+package com.pluto.plugins.exceptions.internal.crash
+
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.content.Context
+import android.os.Build
+import androidx.core.app.NotificationCompat
+import com.pluto.plugin.libinterface.PlutoInterface
+import com.pluto.plugins.exceptions.PlutoExceptions
+import com.pluto.plugins.exceptions.R
+import com.pluto.utilities.device.Device
+
+internal class CrashNotification(private val context: Context) {
+
+ private val manager: NotificationManager? =
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ context.getSystemService(NotificationManager::class.java)
+ } else {
+ context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager?
+ }
+ private val device = Device(context)
+
+ fun add() {
+ createChannel()
+ val notification: Notification = NotificationCompat.Builder(context, CHANNEL_ID)
+ .setPriority(NotificationCompat.PRIORITY_DEFAULT)
+ .setContentTitle(context.getString(R.string.pluto_excep___notification_title, device.app.name))
+ .setContentText(context.getString(R.string.pluto_excep___notification_subtitle))
+ .setSmallIcon(R.drawable.pluto_excep___ic_plugin_logo)
+ .setContentIntent(PlutoInterface.notification.getPendingIntent(PlutoExceptions.devIdentifier))
+ .setOngoing(false)
+ .setOnlyAlertOnce(true)
+ .setAutoCancel(true)
+ .setSilent(true)
+ .setSound(null)
+ .build()
+ manager?.notify(NOTIFICATION_ID, notification)
+ }
+
+ fun remove() {
+ manager?.cancel(NOTIFICATION_ID)
+ }
+
+ private fun createNotificationChannel(channel: NotificationChannel) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ manager?.createNotificationChannel(channel)
+ }
+ }
+
+ private fun createChannel() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val channel = NotificationChannel(
+ CHANNEL_ID,
+ CHANNEL_NAME,
+ NotificationManager.IMPORTANCE_DEFAULT
+ )
+ channel.setShowBadge(false)
+ createNotificationChannel(channel)
+ }
+ }
+
+ companion object {
+ const val NOTIFICATION_ID = 10_001
+ const val CHANNEL_ID = "pluto_notifications"
+ const val GROUP_ID = "pluto_notifications_group"
+ const val CHANNEL_NAME = "Pluto Notifications"
+ }
+}
diff --git a/pluto/src/main/java/com/mocklets/pluto/core/extensions/ConcurrencyKtx.kt b/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/extensions/ConcurrencyKtx.kt
similarity index 79%
rename from pluto/src/main/java/com/mocklets/pluto/core/extensions/ConcurrencyKtx.kt
rename to pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/extensions/ConcurrencyKtx.kt
index e1dd839d9..c29ae8125 100644
--- a/pluto/src/main/java/com/mocklets/pluto/core/extensions/ConcurrencyKtx.kt
+++ b/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/extensions/ConcurrencyKtx.kt
@@ -1,4 +1,4 @@
-package com.mocklets.pluto.core.extensions
+package com.pluto.plugins.exceptions.internal.extensions
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
internal fun Any.wait(timeout: Long = 0) = (this as Object).wait(timeout)
diff --git a/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/extensions/ThreadKtx.kt b/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/extensions/ThreadKtx.kt
new file mode 100644
index 000000000..822ed3379
--- /dev/null
+++ b/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/extensions/ThreadKtx.kt
@@ -0,0 +1,10 @@
+package com.pluto.plugins.exceptions.internal.extensions
+
+import com.pluto.utilities.extensions.capitalizeText
+
+fun getPriorityString(priority: Int) =
+ when (priority) {
+ Thread.MAX_PRIORITY -> "maximum"
+ Thread.MIN_PRIORITY -> "minimum"
+ else -> "normal"
+ }.capitalizeText()
diff --git a/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/persistence/EntityConverters.kt b/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/persistence/EntityConverters.kt
new file mode 100644
index 000000000..bdf1fce62
--- /dev/null
+++ b/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/persistence/EntityConverters.kt
@@ -0,0 +1,36 @@
+package com.pluto.plugins.exceptions.internal.persistence
+
+import androidx.room.TypeConverter
+import com.pluto.plugins.exceptions.internal.DeviceInfo
+import com.pluto.plugins.exceptions.internal.ExceptionAllData
+import com.squareup.moshi.JsonAdapter
+import com.squareup.moshi.Moshi
+
+internal class EntityConverters {
+
+ private var moshi = Moshi.Builder().build()
+ private var exceptionMoshiAdapter: JsonAdapter = moshi.adapter(ExceptionAllData::class.java)
+ private var deviceMoshiAdapter: JsonAdapter = moshi.adapter(DeviceInfo::class.java)
+
+ @TypeConverter
+ fun stringToException(data: String?): ExceptionAllData? {
+ data?.let { return exceptionMoshiAdapter.fromJson(data) }
+ return null
+ }
+
+ @TypeConverter
+ fun exceptionToString(data: ExceptionAllData): String {
+ return exceptionMoshiAdapter.toJson(data)
+ }
+
+ @TypeConverter
+ fun stringToDevice(data: String?): DeviceInfo? {
+ data?.let { return deviceMoshiAdapter.fromJson(data) }
+ return null
+ }
+
+ @TypeConverter
+ fun deviceToString(data: DeviceInfo): String {
+ return deviceMoshiAdapter.toJson(data)
+ }
+}
diff --git a/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/persistence/ExceptionDBHandler.kt b/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/persistence/ExceptionDBHandler.kt
new file mode 100644
index 000000000..e7e53c5c7
--- /dev/null
+++ b/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/persistence/ExceptionDBHandler.kt
@@ -0,0 +1,81 @@
+package com.pluto.plugins.exceptions.internal.persistence
+
+import android.content.Context
+import androidx.annotation.Keep
+import com.pluto.plugins.exceptions.internal.DeviceInfo
+import com.pluto.plugins.exceptions.internal.ExceptionAllData
+import com.pluto.plugins.exceptions.internal.asDeviceInfo
+import com.pluto.plugins.exceptions.internal.asExceptionData
+import com.pluto.plugins.exceptions.internal.asThreadData
+import com.pluto.plugins.exceptions.internal.persistence.database.DatabaseManager
+import com.pluto.utilities.device.Device
+import com.squareup.moshi.JsonAdapter
+import com.squareup.moshi.JsonClass
+import com.squareup.moshi.Moshi
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+
+internal object ExceptionDBHandler {
+
+ private lateinit var exceptionDao: ExceptionDao
+ private val coroutineScope = CoroutineScope(Dispatchers.IO)
+ private lateinit var deviceInfo: DeviceInfo
+ private lateinit var preferences: Preferences
+ private var moshi = Moshi.Builder().build()
+ private var moshiAdapter: JsonAdapter = moshi.adapter(ExceptionTempData::class.java)
+
+ fun initialize(context: Context) {
+ deviceInfo = Device(context).asDeviceInfo()
+ exceptionDao = DatabaseManager(context).db.exceptionDao()
+ preferences = Preferences(context)
+ checkAndSaveCrash()
+ }
+
+ private fun checkAndSaveCrash() {
+ preferences.lastSessionCrash?.let {
+ moshiAdapter.fromJson(it)?.let { exception ->
+ persist(exception.exception, exception.timestamp) {
+ preferences.lastSessionCrash = null
+ }
+ }
+ }
+ }
+
+ fun persist(exception: ExceptionAllData, timestamp: Long, onSuccess: (() -> Unit)? = null) {
+ coroutineScope.launch {
+ exceptionDao.save(
+ ExceptionEntity(
+ timestamp = timestamp,
+ data = exception,
+ device = deviceInfo
+ )
+ )
+ onSuccess?.invoke()
+ }
+ }
+
+ fun flush() {
+ coroutineScope.launch {
+ exceptionDao.deleteAll()
+ }
+ }
+
+ fun tempPersist(exception: Throwable, thread: Thread) {
+ val exceptionData = ExceptionTempData(
+ exception = ExceptionAllData(
+ thread = thread.asThreadData(),
+ exception = exception.asExceptionData()
+ ),
+ timestamp = System.currentTimeMillis()
+ )
+ preferences.lastSessionCrash = moshiAdapter.toJson(exceptionData)
+ }
+}
+
+@Keep
+@JsonClass(generateAdapter = true)
+internal data class ExceptionTempData(
+ val timestamp: Long,
+ val exception: ExceptionAllData
+)
diff --git a/pluto/src/main/java/com/mocklets/pluto/modules/exceptions/dao/ExceptionDao.kt b/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/persistence/ExceptionDao.kt
similarity index 91%
rename from pluto/src/main/java/com/mocklets/pluto/modules/exceptions/dao/ExceptionDao.kt
rename to pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/persistence/ExceptionDao.kt
index d1140796c..da2d3f308 100644
--- a/pluto/src/main/java/com/mocklets/pluto/modules/exceptions/dao/ExceptionDao.kt
+++ b/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/persistence/ExceptionDao.kt
@@ -1,4 +1,4 @@
-package com.mocklets.pluto.modules.exceptions.dao
+package com.pluto.plugins.exceptions.internal.persistence
import androidx.room.Dao
import androidx.room.Insert
diff --git a/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/persistence/ExceptionEntity.kt b/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/persistence/ExceptionEntity.kt
new file mode 100644
index 000000000..f26f9bd1f
--- /dev/null
+++ b/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/persistence/ExceptionEntity.kt
@@ -0,0 +1,27 @@
+package com.pluto.plugins.exceptions.internal.persistence
+
+import androidx.annotation.Keep
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+import androidx.room.TypeConverters
+import com.pluto.plugins.exceptions.internal.DeviceInfo
+import com.pluto.plugins.exceptions.internal.ExceptionAllData
+import com.pluto.utilities.list.ListItem
+import com.squareup.moshi.JsonClass
+
+@Keep
+@JsonClass(generateAdapter = true)
+@TypeConverters(EntityConverters::class)
+@Entity(tableName = "exceptions")
+internal data class ExceptionEntity(
+ @PrimaryKey(autoGenerate = true)
+ @ColumnInfo(name = "id")
+ val id: Int? = null,
+ @ColumnInfo(name = "timestamp")
+ val timestamp: Long,
+ @ColumnInfo(name = "data")
+ val data: ExceptionAllData,
+ @ColumnInfo(name = "device")
+ val device: DeviceInfo
+) : ListItem()
diff --git a/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/persistence/Preferences.kt b/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/persistence/Preferences.kt
new file mode 100644
index 000000000..eb3cff45f
--- /dev/null
+++ b/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/persistence/Preferences.kt
@@ -0,0 +1,23 @@
+package com.pluto.plugins.exceptions.internal.persistence
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.SharedPreferences
+
+@SuppressWarnings("UseDataClass")
+internal class Preferences(context: Context) {
+
+ private val statePrefs: SharedPreferences = context.getSharedPreferences("_pluto_pref_exception", Context.MODE_PRIVATE)
+
+ internal var lastSessionCrash: String?
+ get() = statePrefs.getString(LAST_SESSION_CRASH, null)
+ @SuppressLint("ApplySharedPref")
+ /* added commit, as apply() is getting missed on crash */
+ set(value) {
+ statePrefs.edit().putString(LAST_SESSION_CRASH, value).commit()
+ }
+
+ private companion object {
+ const val LAST_SESSION_CRASH = "last_session_crash"
+ }
+}
diff --git a/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/persistence/database/DatabaseManager.kt b/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/persistence/database/DatabaseManager.kt
new file mode 100644
index 000000000..01e49c54a
--- /dev/null
+++ b/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/persistence/database/DatabaseManager.kt
@@ -0,0 +1,18 @@
+package com.pluto.plugins.exceptions.internal.persistence.database
+
+import android.content.Context
+import androidx.room.Room
+
+internal class DatabaseManager(context: Context) {
+
+ val db by lazy {
+ Room.databaseBuilder(context, PlutoDatabase::class.java, DATABASE_NAME)
+ .addMigrations()
+ .fallbackToDestructiveMigration()
+ .build()
+ }
+
+ companion object {
+ private const val DATABASE_NAME = "_pluto_exception_database"
+ }
+}
diff --git a/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/persistence/database/PlutoDatabase.kt b/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/persistence/database/PlutoDatabase.kt
new file mode 100644
index 000000000..f65b39d91
--- /dev/null
+++ b/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/persistence/database/PlutoDatabase.kt
@@ -0,0 +1,16 @@
+package com.pluto.plugins.exceptions.internal.persistence.database
+
+import androidx.room.RoomDatabase
+import com.pluto.plugins.exceptions.internal.persistence.ExceptionDao
+import com.pluto.plugins.exceptions.internal.persistence.ExceptionEntity
+
+@androidx.room.Database(
+ entities = [
+ ExceptionEntity::class,
+ ],
+ version = 4,
+ exportSchema = false
+)
+internal abstract class PlutoDatabase : RoomDatabase() {
+ abstract fun exceptionDao(): ExceptionDao
+}
diff --git a/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/ui/CrashesAdapter.kt b/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/ui/CrashesAdapter.kt
new file mode 100644
index 000000000..362843be0
--- /dev/null
+++ b/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/ui/CrashesAdapter.kt
@@ -0,0 +1,48 @@
+package com.pluto.plugins.exceptions.internal.ui
+
+import android.view.ViewGroup
+import com.pluto.plugins.exceptions.internal.DeviceInfo
+import com.pluto.plugins.exceptions.internal.ExceptionData
+import com.pluto.plugins.exceptions.internal.ThreadData
+import com.pluto.plugins.exceptions.internal.ThreadStates
+import com.pluto.plugins.exceptions.internal.persistence.ExceptionEntity
+import com.pluto.plugins.exceptions.internal.ui.holder.CrashItemDetailsDeviceHolder
+import com.pluto.plugins.exceptions.internal.ui.holder.CrashItemDetailsHeaderHolder
+import com.pluto.plugins.exceptions.internal.ui.holder.CrashItemDetailsThreadHolder
+import com.pluto.plugins.exceptions.internal.ui.holder.CrashItemDetailsThreadStackThreadHolder
+import com.pluto.plugins.exceptions.internal.ui.holder.CrashItemHolder
+import com.pluto.utilities.list.BaseAdapter
+import com.pluto.utilities.list.DiffAwareHolder
+import com.pluto.utilities.list.ListItem
+
+internal class CrashesAdapter(private val listener: OnActionListener) : BaseAdapter() {
+ override fun getItemViewType(item: ListItem): Int? {
+ return when (item) {
+ is ExceptionEntity -> ITEM_TYPE_CRASH
+ is ExceptionData -> ITEM_DETAILS_TYPE_HEADER
+ is ThreadData -> ITEM_DETAILS_TYPE_THREAD
+ is DeviceInfo -> ITEM_DETAILS_TYPE_DEVICE
+ is ThreadStates -> ITEM_DETAILS_TYPE_THREAD_STATES
+ else -> null
+ }
+ }
+
+ override fun onViewHolderCreated(parent: ViewGroup, viewType: Int): DiffAwareHolder? {
+ return when (viewType) {
+ ITEM_TYPE_CRASH -> CrashItemHolder(parent, listener)
+ ITEM_DETAILS_TYPE_HEADER -> CrashItemDetailsHeaderHolder(parent, listener)
+ ITEM_DETAILS_TYPE_THREAD -> CrashItemDetailsThreadHolder(parent, listener)
+ ITEM_DETAILS_TYPE_DEVICE -> CrashItemDetailsDeviceHolder(parent, listener)
+ ITEM_DETAILS_TYPE_THREAD_STATES -> CrashItemDetailsThreadStackThreadHolder(parent, listener)
+ else -> null
+ }
+ }
+
+ companion object {
+ const val ITEM_TYPE_CRASH = 1000
+ const val ITEM_DETAILS_TYPE_HEADER = 1100
+ const val ITEM_DETAILS_TYPE_THREAD = 1101
+ const val ITEM_DETAILS_TYPE_DEVICE = 1102
+ const val ITEM_DETAILS_TYPE_THREAD_STATES = 1103
+ }
+}
diff --git a/pluto/src/main/java/com/mocklets/pluto/modules/exceptions/ui/CrashesViewModel.kt b/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/ui/CrashesViewModel.kt
similarity index 83%
rename from pluto/src/main/java/com/mocklets/pluto/modules/exceptions/ui/CrashesViewModel.kt
rename to pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/ui/CrashesViewModel.kt
index 26f03c9a0..7993bb504 100644
--- a/pluto/src/main/java/com/mocklets/pluto/modules/exceptions/ui/CrashesViewModel.kt
+++ b/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/ui/CrashesViewModel.kt
@@ -1,13 +1,13 @@
-package com.mocklets.pluto.modules.exceptions.ui
+package com.pluto.plugins.exceptions.internal.ui
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
-import com.mocklets.pluto.core.database.DatabaseManager
-import com.mocklets.pluto.modules.exceptions.dao.ExceptionDao
-import com.mocklets.pluto.modules.exceptions.dao.ExceptionEntity
+import com.pluto.plugins.exceptions.internal.persistence.ExceptionDao
+import com.pluto.plugins.exceptions.internal.persistence.ExceptionEntity
+import com.pluto.plugins.exceptions.internal.persistence.database.DatabaseManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -15,10 +15,6 @@ internal class CrashesViewModel(application: Application) : AndroidViewModel(app
private val exceptionDao: ExceptionDao by lazy { DatabaseManager(application.applicationContext).db.exceptionDao() }
- init {
- fetchAll()
- }
-
val exceptions: LiveData>
get() = _exceptions
private val _exceptions = MutableLiveData>()
@@ -27,7 +23,7 @@ internal class CrashesViewModel(application: Application) : AndroidViewModel(app
get() = _currentException
private val _currentException = MutableLiveData()
- private fun fetchAll() {
+ fun fetchAll() {
viewModelScope.launch(Dispatchers.IO) {
val list = exceptionDao.fetchAll() ?: arrayListOf()
_exceptions.postValue(list)
diff --git a/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/ui/DetailsFragment.kt b/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/ui/DetailsFragment.kt
new file mode 100644
index 000000000..bc4b203ea
--- /dev/null
+++ b/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/ui/DetailsFragment.kt
@@ -0,0 +1,167 @@
+package com.pluto.plugins.exceptions.internal.ui
+
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import android.util.Base64.DEFAULT
+import android.util.Base64.encodeToString
+import android.view.View
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.Observer
+import androidx.lifecycle.lifecycleScope
+import androidx.navigation.fragment.findNavController
+import com.pluto.plugin.share.Shareable
+import com.pluto.plugin.share.lazyContentSharer
+import com.pluto.plugins.exceptions.PlutoExceptions
+import com.pluto.plugins.exceptions.R
+import com.pluto.plugins.exceptions.databinding.PlutoExcepFragmentDetailsBinding
+import com.pluto.plugins.exceptions.internal.ReportData
+import com.pluto.plugins.exceptions.internal.persistence.ExceptionEntity
+import com.pluto.utilities.autoClearInitializer
+import com.pluto.utilities.extensions.capitalizeText
+import com.pluto.utilities.extensions.delayedLaunchWhenResumed
+import com.pluto.utilities.extensions.onBackPressed
+import com.pluto.utilities.extensions.toast
+import com.pluto.utilities.list.BaseAdapter
+import com.pluto.utilities.list.DiffAwareAdapter
+import com.pluto.utilities.list.DiffAwareHolder
+import com.pluto.utilities.list.ListItem
+import com.pluto.utilities.setOnDebounceClickListener
+import com.pluto.utilities.viewBinding
+import com.squareup.moshi.JsonAdapter
+import com.squareup.moshi.Moshi
+import java.net.URLEncoder
+
+internal class DetailsFragment : Fragment(R.layout.pluto_excep___fragment_details) {
+
+ private val binding by viewBinding(PlutoExcepFragmentDetailsBinding::bind)
+ private val viewModel: CrashesViewModel by activityViewModels()
+ private val crashAdapter: BaseAdapter by autoClearInitializer { CrashesAdapter(onActionListener) }
+ private val exceptionCipher by lazy { getCipheredException() }
+ private val contentSharer by lazyContentSharer()
+
+ private var moshi = Moshi.Builder().build()
+ private var moshiAdapter: JsonAdapter = moshi.adapter(ReportData::class.java)
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ onBackPressed { findNavController().navigateUp() }
+ binding.list.apply {
+ adapter = crashAdapter
+ }
+ binding.close.setOnDebounceClickListener {
+ activity?.onBackPressed()
+ }
+
+ binding.delete.setOnDebounceClickListener {
+ viewModel.currentException.value?.id?.let { id ->
+ lifecycleScope.delayedLaunchWhenResumed(SCREEN_CLOSE_DELAY) {
+ viewModel.delete(id)
+ activity?.onBackPressed()
+ context?.toast("Crash logs deleted.")
+ }
+ }
+ }
+
+ binding.share.setOnDebounceClickListener {
+ viewModel.currentException.value?.let {
+ contentSharer.share(Shareable(title = "Share Crash Report", content = it.toShareText(), fileName = "Crash Report from Pluto"))
+ }
+ }
+
+ viewModel.currentException.removeObservers(viewLifecycleOwner)
+ viewModel.currentException.observe(viewLifecycleOwner, exceptionObserver)
+ }
+
+ private fun getCipheredException(): String? {
+ viewModel.currentException.value?.data?.exception?.let {
+ val reportData = ReportData(
+ name = it.name,
+ message = it.message,
+ stackTrace = it.stackTrace.take(STACK_TRACE_SHORT_LENGTH) as ArrayList,
+ client = PlutoExceptions.appPackageName
+ )
+ val exceptionString = moshiAdapter.toJson(reportData)
+
+ val encodedByteArray = exceptionString.toByteArray(Charsets.UTF_8)
+ val encodedString = encodeToString(encodedByteArray, DEFAULT)
+
+ return URLEncoder.encode(encodedString, "utf-8")
+ }
+ return null
+ }
+
+ private val onActionListener = object : DiffAwareAdapter.OnActionListener {
+ override fun onAction(action: String, data: ListItem, holder: DiffAwareHolder) {
+ when (action) {
+ "report_crash" -> exceptionCipher?.let {
+ val url = "https://androidpluto.com/exception/$it/a0bbe9cd-2f02-4a12-b7b7-36fce61a6b48"
+ val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
+ startActivity(browserIntent)
+ }
+ "thread_stack_trace" -> findNavController().navigate(R.id.openStackTrace)
+ }
+ }
+ }
+
+ private val exceptionObserver = Observer {
+ val list = arrayListOf()
+ list.add(it.data.exception)
+ it.data.threadStateList?.let { states -> list.add(states) }
+ it.data.thread?.let { thread -> list.add(thread) }
+ list.add(it.device)
+
+ crashAdapter.list = list
+ }
+
+ private companion object {
+ const val SCREEN_CLOSE_DELAY = 200L
+ private const val STACK_TRACE_SHORT_LENGTH = 15
+ }
+}
+
+private const val SHARE_SECTION_DIVIDER = "\n\n==================\n\n"
+private fun ExceptionEntity.toShareText(): String {
+ val text = StringBuilder()
+ text.append("EXCEPTION : \n")
+ text.append("${this.data.exception.name}: ${this.data.exception.message}\n")
+ this.data.exception.stackTrace.forEach {
+ text.append("\t at $it\n")
+ }
+ if (this.data.exception.stackTraceAdditionalLineCount > 0) {
+ text.append("\t + ${this.data.exception.stackTraceAdditionalLineCount} more lines\n\n")
+ }
+
+ text.append(SHARE_SECTION_DIVIDER)
+
+ this.data.thread?.let {
+ text.append("Thread : ")
+ text.append("${it.name.uppercase()} (")
+ text.append("id : ${it.id}, ")
+ text.append("priority : ${it.priorityString}, ")
+ text.append("is_Daemon : ${it.isDaemon}, ")
+ text.append("state : ${it.state}")
+ text.append(")")
+
+ text.append(SHARE_SECTION_DIVIDER)
+ }
+
+ text.append("APP STATE : \n")
+ this.device.appVersionName.let {
+ text.append("App Version : $it (${this.device.appVersionCode})\n")
+ }
+ text.append("Android (OS : ${this.device.androidOs}, API_Level : ${this.device.androidAPILevel})\n")
+ text.append("Orientation : ${this.device.screenOrientation}\n")
+ text.append("is_Rooted : ${this.device.isRooted}")
+
+ text.append(SHARE_SECTION_DIVIDER)
+
+ text.append("DEVICE INFO : \n")
+ text.append("Model : ${this.device.buildBrand?.capitalizeText()} ${this.device.buildModel}\n")
+ text.append(
+ "Screen : { height : ${this.device.screenHeightPx}px, width : ${this.device.screenWidthPx}px, " +
+ "density : ${this.device.screenDensityDpi}, size : ${this.device.screenSizeInch} inches }"
+ )
+ return text.toString()
+}
diff --git a/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/ui/ListFragment.kt b/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/ui/ListFragment.kt
new file mode 100644
index 000000000..023b86b1d
--- /dev/null
+++ b/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/ui/ListFragment.kt
@@ -0,0 +1,113 @@
+package com.pluto.plugins.exceptions.internal.ui
+
+import android.os.Bundle
+import android.view.View
+import android.view.View.GONE
+import android.view.View.VISIBLE
+import androidx.core.widget.doOnTextChanged
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.Observer
+import androidx.lifecycle.lifecycleScope
+import androidx.navigation.fragment.findNavController
+import com.pluto.plugins.exceptions.PlutoExceptions
+import com.pluto.plugins.exceptions.R
+import com.pluto.plugins.exceptions.databinding.PlutoExcepFragmentListBinding
+import com.pluto.plugins.exceptions.internal.persistence.ExceptionEntity
+import com.pluto.utilities.autoClearInitializer
+import com.pluto.utilities.extensions.hideKeyboard
+import com.pluto.utilities.extensions.linearLayoutManager
+import com.pluto.utilities.extensions.showMoreOptions
+import com.pluto.utilities.extensions.toast
+import com.pluto.utilities.list.BaseAdapter
+import com.pluto.utilities.list.CustomItemDecorator
+import com.pluto.utilities.list.DiffAwareAdapter
+import com.pluto.utilities.list.DiffAwareHolder
+import com.pluto.utilities.list.ListItem
+import com.pluto.utilities.setOnDebounceClickListener
+import com.pluto.utilities.viewBinding
+
+class ListFragment : Fragment(R.layout.pluto_excep___fragment_list) {
+
+ private val binding by viewBinding(PlutoExcepFragmentListBinding::bind)
+ private val viewModel: CrashesViewModel by activityViewModels()
+ private val crashAdapter by autoClearInitializer {
+ CrashesAdapter(onActionListener)
+ }
+ private var isFetchingInProgress: Boolean = false
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ binding.crashList.apply {
+ adapter = crashAdapter
+ addItemDecoration(CustomItemDecorator(requireContext()))
+ }
+ binding.search.doOnTextChanged { text, _, _, _ ->
+ viewLifecycleOwner.lifecycleScope.launchWhenResumed {
+ text?.toString()?.let {
+ PlutoExceptions.session.lastSearchText = it
+ crashAdapter.list = filteredLogs(it)
+ if (it.isEmpty()) {
+ binding.crashList.linearLayoutManager()?.scrollToPositionWithOffset(0, 0)
+ }
+ }
+ }
+ }
+ binding.search.setText(PlutoExceptions.session.lastSearchText)
+ binding.close.setOnDebounceClickListener {
+ requireActivity().finish()
+ }
+ binding.options.setOnDebounceClickListener {
+ context?.showMoreOptions(it, R.menu.pluto_excep___menu_more_options) { item ->
+ when (item.itemId) {
+ R.id.clear -> viewModel.deleteAll()
+ }
+ }
+ }
+
+ isFetchingInProgress = true
+ viewModel.fetchAll()
+
+ viewModel.exceptions.removeObserver(exceptionObserver)
+ viewModel.exceptions.observe(viewLifecycleOwner, exceptionObserver)
+ }
+
+ private val exceptionObserver = Observer> {
+ isFetchingInProgress = false
+ crashAdapter.list = filteredLogs(binding.search.text.toString())
+ }
+
+ private fun filteredLogs(search: String): List {
+ var list = emptyList()
+ if (isFetchingInProgress) {
+ binding.loaderGroup.visibility = VISIBLE
+ } else {
+ binding.loaderGroup.visibility = GONE
+ viewModel.exceptions.value?.let {
+ list = it.filter { exception ->
+ (exception.data.exception.name ?: "").contains(search, true) ||
+ (exception.data.exception.file ?: "").contains(search, true)
+ }
+ }
+ binding.noItemText.text = getString(
+ if (search.isNotEmpty()) R.string.pluto_excep___no_search_result else R.string.pluto_excep___no_crashes_text
+ )
+ binding.noItemText.visibility = if (list.isEmpty()) VISIBLE else GONE
+ }
+ return list
+ }
+
+ private val onActionListener = object : DiffAwareAdapter.OnActionListener {
+ override fun onAction(action: String, data: ListItem, holder: DiffAwareHolder) {
+ if (data is ExceptionEntity) {
+ requireActivity().hideKeyboard()
+ if (data.id != null) {
+ viewModel.fetch(data.id)
+ findNavController().navigate(R.id.openDetails)
+ } else {
+ requireContext().toast(getString(R.string.pluto_excep___invalid_id))
+ }
+ }
+ }
+ }
+}
diff --git a/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/ui/StackTracesAdapter.kt b/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/ui/StackTracesAdapter.kt
new file mode 100644
index 000000000..526dc829c
--- /dev/null
+++ b/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/ui/StackTracesAdapter.kt
@@ -0,0 +1,28 @@
+package com.pluto.plugins.exceptions.internal.ui
+
+import android.view.ViewGroup
+import com.pluto.plugins.exceptions.internal.ProcessThread
+import com.pluto.plugins.exceptions.internal.ui.holder.StackTraceListItemHolder
+import com.pluto.utilities.list.BaseAdapter
+import com.pluto.utilities.list.DiffAwareHolder
+import com.pluto.utilities.list.ListItem
+
+internal class StackTracesAdapter(private val listener: OnActionListener) : BaseAdapter() {
+ override fun getItemViewType(item: ListItem): Int? {
+ return when (item) {
+ is ProcessThread -> ITEM_TYPE_THREAD_STACK_TRACE
+ else -> null
+ }
+ }
+
+ override fun onViewHolderCreated(parent: ViewGroup, viewType: Int): DiffAwareHolder? {
+ return when (viewType) {
+ ITEM_TYPE_THREAD_STACK_TRACE -> StackTraceListItemHolder(parent, listener)
+ else -> null
+ }
+ }
+
+ companion object {
+ const val ITEM_TYPE_THREAD_STACK_TRACE = 1201
+ }
+}
diff --git a/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/ui/ThreadStackTraceFragment.kt b/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/ui/ThreadStackTraceFragment.kt
new file mode 100644
index 000000000..8ec0e9452
--- /dev/null
+++ b/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/ui/ThreadStackTraceFragment.kt
@@ -0,0 +1,116 @@
+package com.pluto.plugins.exceptions.internal.ui
+
+import android.os.Bundle
+import android.view.View
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.Observer
+import androidx.navigation.fragment.findNavController
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.pluto.plugin.share.Shareable
+import com.pluto.plugin.share.lazyContentSharer
+import com.pluto.plugins.exceptions.R
+import com.pluto.plugins.exceptions.databinding.PlutoExcepFragmentThreadStackTraceBinding
+import com.pluto.plugins.exceptions.internal.ProcessThread
+import com.pluto.plugins.exceptions.internal.ThreadStates
+import com.pluto.plugins.exceptions.internal.persistence.ExceptionEntity
+import com.pluto.plugins.exceptions.internal.ui.holder.StackTraceListItemHolder
+import com.pluto.utilities.autoClearInitializer
+import com.pluto.utilities.extensions.onBackPressed
+import com.pluto.utilities.extensions.showMoreOptions
+import com.pluto.utilities.list.BaseAdapter
+import com.pluto.utilities.list.CustomItemDecorator
+import com.pluto.utilities.list.DiffAwareAdapter
+import com.pluto.utilities.list.DiffAwareHolder
+import com.pluto.utilities.list.ListItem
+import com.pluto.utilities.setOnDebounceClickListener
+import com.pluto.utilities.viewBinding
+
+class ThreadStackTraceFragment : Fragment(R.layout.pluto_excep___fragment_thread_stack_trace) {
+ private val binding by viewBinding(PlutoExcepFragmentThreadStackTraceBinding::bind)
+ private val viewModel: CrashesViewModel by activityViewModels()
+ private val threadAdapter: BaseAdapter by autoClearInitializer { StackTracesAdapter(onActionListener) }
+ private var linearLayoutManager: LinearLayoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
+ private val contentSharer by lazyContentSharer()
+ private var filterValue: String? = null
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ onBackPressed { findNavController().navigateUp() }
+ binding.list.apply {
+ adapter = threadAdapter
+ layoutManager = linearLayoutManager
+ addItemDecoration(CustomItemDecorator(requireContext()))
+ }
+ binding.close.setOnDebounceClickListener {
+ activity?.onBackPressed()
+ }
+
+ binding.filterCta.setOnDebounceClickListener {
+ context?.showMoreOptions(it, R.menu.pluto_excep___menu_stack_trace_filter) { item ->
+ filterValue = when (item.itemId) {
+ R.id.filterBlocked -> getString(R.string.pluto_excep___trace_filter_blocked)
+ R.id.filterWaiting -> getString(R.string.pluto_excep___trace_filter_waiting)
+ R.id.filterTimedWaiting -> getString(R.string.pluto_excep___trace_filter_timed_waiting)
+ R.id.filterRunnable -> getString(R.string.pluto_excep___trace_filter_runnable)
+ else -> null
+ }
+ threadAdapter.list = (viewModel.currentException.value?.data?.threadStateList?.states ?: emptyList()).process(filterValue)
+ binding.filterCta.text = filterValue ?: getString(R.string.pluto_excep___trace_filter_all)
+ }
+ }
+ binding.share.setOnDebounceClickListener {
+ viewModel.currentException.value?.data?.threadStateList?.let {
+ contentSharer.share(Shareable(title = "Share Thread Stack Trace", content = it.toShareText(), fileName = "ANR Thread Stack Trace from Pluto"))
+ }
+ }
+
+ viewModel.currentException.removeObservers(viewLifecycleOwner)
+ viewModel.currentException.observe(viewLifecycleOwner, exceptionObserver)
+ }
+
+ private val exceptionObserver = Observer {
+ threadAdapter.list = (it.data.threadStateList?.states ?: emptyList()).process(filterValue)
+ }
+
+ private val onActionListener = object : DiffAwareAdapter.OnActionListener {
+ override fun onAction(action: String, data: ListItem, holder: DiffAwareHolder) {
+ }
+ }
+}
+
+private fun ThreadStates.toShareText(): String {
+ val text = StringBuilder()
+ text.append("ANR Thread Stack Trace - ${states.size} threads : \n")
+ states.process().forEach {
+ text.append("\n* ${it.name}\t ${it.state.uppercase()}\n")
+ it.stackTrace.take(StackTraceListItemHolder.MAX_STACK_TRACE_LINES).forEach { trace ->
+ text.append("\tat $trace\n")
+ }
+ val extraTrace = it.stackTrace.size - StackTraceListItemHolder.MAX_STACK_TRACE_LINES
+ if (extraTrace > 0) {
+ text.append("\t +$extraTrace more lines\n")
+ }
+ }
+ return text.toString()
+}
+
+private fun List.process(filterValue: String? = null): List {
+ val list = arrayListOf()
+ val mainThreadName = "main"
+ var mainThread: ProcessThread? = null
+ forEach {
+ if (filterValue == null || it.state.equals(filterValue, true)) {
+ if (it.name == mainThreadName) {
+ mainThread = it
+ } else {
+ list.add(it)
+ }
+ }
+ }
+ return arrayListOf().apply {
+ mainThread?.let { add(it) }
+ addAll(list)
+ }
+}
diff --git a/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/ui/holder/CrashItemDetailsDeviceHolder.kt b/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/ui/holder/CrashItemDetailsDeviceHolder.kt
new file mode 100644
index 000000000..ed06fab97
--- /dev/null
+++ b/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/ui/holder/CrashItemDetailsDeviceHolder.kt
@@ -0,0 +1,111 @@
+package com.pluto.plugins.exceptions.internal.ui.holder
+
+import android.view.ViewGroup
+import com.pluto.plugins.exceptions.R
+import com.pluto.plugins.exceptions.databinding.PlutoExcepItemCrashDetailsDeviceBinding
+import com.pluto.plugins.exceptions.internal.DeviceInfo
+import com.pluto.utilities.extensions.capitalizeText
+import com.pluto.utilities.extensions.inflate
+import com.pluto.utilities.list.DiffAwareAdapter
+import com.pluto.utilities.list.DiffAwareHolder
+import com.pluto.utilities.list.ListItem
+import com.pluto.utilities.spannable.createSpan
+import com.pluto.utilities.views.keyvalue.KeyValuePairData
+
+internal class CrashItemDetailsDeviceHolder(
+ parent: ViewGroup,
+ actionListener: DiffAwareAdapter.OnActionListener
+) : DiffAwareHolder(parent.inflate(R.layout.pluto_excep___item_crash_details_device), actionListener) {
+
+ private val binding = PlutoExcepItemCrashDetailsDeviceBinding.bind(itemView)
+
+ override fun onBind(item: ListItem) {
+ if (item is DeviceInfo) {
+ setupAppDataTable(item)
+ setupDeviceDataTable(item)
+ }
+ }
+
+ private fun setupAppDataTable(item: DeviceInfo) {
+ val dataList = arrayListOf().apply {
+ add(
+ KeyValuePairData(
+ key = context.getString(R.string.pluto_excep___app_version_label),
+ value = context.createSpan {
+ append(semiBold(item.appVersionName))
+ append(" (${item.appVersionCode})")
+ }
+ )
+ )
+ add(
+ KeyValuePairData(
+ key = context.getString(R.string.pluto_excep___android_os_label),
+ value = item.androidOs
+ )
+ )
+ add(
+ KeyValuePairData(
+ key = context.getString(R.string.pluto_excep___android_api_level_label),
+ value = item.androidAPILevel
+ )
+ )
+ add(
+ KeyValuePairData(
+ key = context.getString(R.string.pluto_excep___orientation_label),
+ value = item.screenOrientation.capitalizeText()
+ )
+ )
+ add(
+ KeyValuePairData(
+ key = context.getString(R.string.pluto_excep___rooted_label),
+ value = context.createSpan {
+ append(bold(item.isRooted.toString()))
+ }
+ )
+ )
+ }
+ binding.appDataTable.set(
+ title = context.getString(R.string.pluto_excep___app_state_label),
+ keyValuePairs = dataList
+ )
+ }
+
+ private fun setupDeviceDataTable(item: DeviceInfo) {
+ val dataList = arrayListOf().apply {
+ add(
+ KeyValuePairData(
+ key = context.getString(R.string.pluto_excep___app_version_label),
+ value = "${item.buildBrand?.capitalizeText()} ${item.buildModel}"
+ )
+ )
+ add(
+ KeyValuePairData(
+ key = context.getString(R.string.pluto_excep___height_label),
+ value = "${item.screenHeightPx} px"
+ )
+ )
+ add(
+ KeyValuePairData(
+ key = context.getString(R.string.pluto_excep___width_label),
+ value = "${item.screenWidthPx} px"
+ )
+ )
+ add(
+ KeyValuePairData(
+ key = context.getString(R.string.pluto_excep___density_label),
+ value = "${item.screenDensityDpi} dpi"
+ )
+ )
+ add(
+ KeyValuePairData(
+ key = context.getString(R.string.pluto_excep___size_label),
+ value = "${item.screenSizeInch} inches"
+ )
+ )
+ }
+ binding.deviceDataTable.set(
+ title = context.getString(R.string.pluto_excep___device_label),
+ keyValuePairs = dataList
+ )
+ }
+}
diff --git a/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/ui/holder/CrashItemDetailsHeaderHolder.kt b/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/ui/holder/CrashItemDetailsHeaderHolder.kt
new file mode 100644
index 000000000..a02ea6578
--- /dev/null
+++ b/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/ui/holder/CrashItemDetailsHeaderHolder.kt
@@ -0,0 +1,103 @@
+package com.pluto.plugins.exceptions.internal.ui.holder
+
+import android.view.ViewGroup
+import com.pluto.plugins.exceptions.PlutoExceptions
+import com.pluto.plugins.exceptions.R
+import com.pluto.plugins.exceptions.databinding.PlutoExcepItemCrashDetailsHeaderBinding
+import com.pluto.plugins.exceptions.internal.ExceptionData
+import com.pluto.utilities.extensions.asFormattedDate
+import com.pluto.utilities.extensions.color
+import com.pluto.utilities.extensions.inflate
+import com.pluto.utilities.list.DiffAwareAdapter
+import com.pluto.utilities.list.DiffAwareHolder
+import com.pluto.utilities.list.ListItem
+import com.pluto.utilities.spannable.setSpan
+
+internal class CrashItemDetailsHeaderHolder(
+ parent: ViewGroup,
+ actionListener: DiffAwareAdapter.OnActionListener
+) : DiffAwareHolder(parent.inflate(R.layout.pluto_excep___item_crash_details_header), actionListener) {
+
+ private val binding = PlutoExcepItemCrashDetailsHeaderBinding.bind(itemView)
+ private val timestamp = binding.timestamp
+ private val stacktrace = binding.stackTrace
+// private val reportCrash = binding.reportCrash.crashReportRoot
+ private val message = binding.message
+ private val title = binding.title
+
+ override fun onBind(item: ListItem) {
+ if (item is ExceptionData) {
+ handleTitle(item)
+ timestamp.text = item.timeStamp.asFormattedDate()
+
+ stacktrace.setSpan {
+ append("${item.name}: ${item.message}")
+ item.stackTrace.forEach {
+ append("\n\t\t\t")
+ append(
+ fontColor(
+ " at ", context.color(com.pluto.plugin.R.color.pluto___text_dark_40)
+ )
+ )
+ append(it)
+ }
+ if (item.stackTraceAdditionalLineCount > 0) {
+ append(
+ fontColor(
+ "\n\t\t\t + ${item.stackTraceAdditionalLineCount} more lines", context.color(com.pluto.plugin.R.color.pluto___text_dark_40)
+ )
+ )
+ }
+ }
+
+// if (isPlutoCrash(item.stackTrace.take(MAX_STACK_TRACE_LINES))) {
+// reportCrash.visibility = VISIBLE
+// reportCrash.setDebounceClickListener {
+// onAction("report_crash")
+// }
+// } else {
+// reportCrash.visibility = GONE
+// reportCrash.setDebounceClickListener {}
+// }
+ }
+ }
+
+ private fun handleTitle(item: ExceptionData) {
+ if (item.isANRException) {
+ message.text =
+ context.getString(R.string.pluto_excep___anr_list_message, PlutoExceptions.mainThreadResponseThreshold)
+ title.setSpan {
+ context.apply {
+ append(
+ fontColor(
+ getString(R.string.pluto_excep___anr_list_title),
+ color(com.pluto.plugin.R.color.pluto___text_dark_80)
+ )
+ )
+ }
+ }
+ } else {
+ title.setSpan {
+ append("${item.file}\t\t")
+ append(
+ fontColor("line: ${item.lineNumber}", context.color(com.pluto.plugin.R.color.pluto___text_dark_80))
+ )
+ }
+ message.setSpan {
+ append("${item.name}\n")
+ append(
+ fontColor("${item.message}", context.color(com.pluto.plugin.R.color.pluto___text_dark_60))
+ )
+ }
+ }
+ }
+
+// private fun isPlutoCrash(trace: List): Boolean {
+// trace.forEach {
+// if (it.startsWith(BuildConfig.LIBRARY_PACKAGE_NAME)) {
+// return true
+// }
+// }
+// return false
+// }
+}
diff --git a/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/ui/holder/CrashItemDetailsThreadHolder.kt b/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/ui/holder/CrashItemDetailsThreadHolder.kt
new file mode 100644
index 000000000..dacda981b
--- /dev/null
+++ b/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/ui/holder/CrashItemDetailsThreadHolder.kt
@@ -0,0 +1,71 @@
+package com.pluto.plugins.exceptions.internal.ui.holder
+
+import android.view.ViewGroup
+import com.pluto.plugins.exceptions.R
+import com.pluto.plugins.exceptions.databinding.PlutoExcepItemCrashDetailsThreadBinding
+import com.pluto.plugins.exceptions.internal.ThreadData
+import com.pluto.plugins.exceptions.internal.getStateStringSpan
+import com.pluto.utilities.extensions.color
+import com.pluto.utilities.extensions.inflate
+import com.pluto.utilities.list.DiffAwareAdapter
+import com.pluto.utilities.list.DiffAwareHolder
+import com.pluto.utilities.list.ListItem
+import com.pluto.utilities.spannable.createSpan
+import com.pluto.utilities.views.keyvalue.KeyValuePairData
+
+internal class CrashItemDetailsThreadHolder(
+ parent: ViewGroup,
+ actionListener: DiffAwareAdapter.OnActionListener
+) : DiffAwareHolder(parent.inflate(R.layout.pluto_excep___item_crash_details_thread), actionListener) {
+
+ private val binding = PlutoExcepItemCrashDetailsThreadBinding.bind(itemView)
+
+ override fun onBind(item: ListItem) {
+ if (item is ThreadData) {
+ setupTabularData(item)
+ }
+ }
+
+ private fun setupTabularData(item: ThreadData) {
+ val dataList = arrayListOf().apply {
+ add(
+ KeyValuePairData(
+ key = context.getString(R.string.pluto_excep___thread_name_label),
+ value = context.createSpan {
+ append(semiBold("${item.name.uppercase()}\t"))
+ append(fontColor("(", context.color(com.pluto.plugin.R.color.pluto___text_dark_60)))
+ append(fontColor("id: ", context.color(com.pluto.plugin.R.color.pluto___text_dark_60)))
+ append(bold(fontColor("${item.id}", context.color(com.pluto.plugin.R.color.pluto___text_dark_60))))
+ append(fontColor(")", context.color(com.pluto.plugin.R.color.pluto___text_dark_60)))
+ }
+ )
+ )
+ add(
+ KeyValuePairData(
+ key = context.getString(R.string.pluto_excep___priority_label),
+ value = context.createSpan {
+ append(item.priorityString)
+ }
+ )
+ )
+ add(
+ KeyValuePairData(
+ key = context.getString(R.string.pluto_excep___daemon_label),
+ value = context.createSpan {
+ append(bold(item.isDaemon.toString()))
+ }
+ )
+ )
+ add(
+ KeyValuePairData(
+ key = context.getString(R.string.pluto_excep___thread_run_state_label),
+ value = getStateStringSpan(context, item.state)
+ )
+ )
+ }
+ binding.table.set(
+ title = context.getString(R.string.pluto_excep___thread_state_label),
+ keyValuePairs = dataList
+ )
+ }
+}
diff --git a/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/ui/holder/CrashItemDetailsThreadStackThreadHolder.kt b/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/ui/holder/CrashItemDetailsThreadStackThreadHolder.kt
new file mode 100644
index 000000000..097d114f5
--- /dev/null
+++ b/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/ui/holder/CrashItemDetailsThreadStackThreadHolder.kt
@@ -0,0 +1,33 @@
+package com.pluto.plugins.exceptions.internal.ui.holder
+
+import android.view.ViewGroup
+import com.pluto.plugins.exceptions.R
+import com.pluto.plugins.exceptions.databinding.PlutoExcepItemCrashDetailsThreadStackTraceBinding
+import com.pluto.plugins.exceptions.internal.ThreadStates
+import com.pluto.utilities.extensions.color
+import com.pluto.utilities.extensions.inflate
+import com.pluto.utilities.list.DiffAwareAdapter
+import com.pluto.utilities.list.DiffAwareHolder
+import com.pluto.utilities.list.ListItem
+import com.pluto.utilities.setOnDebounceClickListener
+import com.pluto.utilities.spannable.setSpan
+
+internal class CrashItemDetailsThreadStackThreadHolder(
+ parent: ViewGroup,
+ actionListener: DiffAwareAdapter.OnActionListener
+) : DiffAwareHolder(parent.inflate(R.layout.pluto_excep___item_crash_details_thread_stack_trace), actionListener) {
+
+ private val binding = PlutoExcepItemCrashDetailsThreadStackTraceBinding.bind(itemView)
+
+ override fun onBind(item: ListItem) {
+ if (item is ThreadStates) {
+ binding.label.setSpan {
+ append(context.getString(R.string.pluto_excep___thread_stack_traces_label))
+ append(fontColor(" (${item.states.size})", context.color(com.pluto.plugin.R.color.pluto___text_dark_40)))
+ }
+ binding.root.setOnDebounceClickListener {
+ onAction("thread_stack_trace")
+ }
+ }
+ }
+}
diff --git a/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/ui/holder/CrashItemHolder.kt b/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/ui/holder/CrashItemHolder.kt
new file mode 100644
index 000000000..46452d49b
--- /dev/null
+++ b/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/ui/holder/CrashItemHolder.kt
@@ -0,0 +1,61 @@
+package com.pluto.plugins.exceptions.internal.ui.holder
+
+import android.view.ViewGroup
+import com.pluto.plugins.exceptions.PlutoExceptions
+import com.pluto.plugins.exceptions.R
+import com.pluto.plugins.exceptions.databinding.PlutoExcepItemCrashBinding
+import com.pluto.plugins.exceptions.internal.persistence.ExceptionEntity
+import com.pluto.utilities.extensions.asTimeElapsed
+import com.pluto.utilities.extensions.color
+import com.pluto.utilities.extensions.inflate
+import com.pluto.utilities.list.DiffAwareAdapter
+import com.pluto.utilities.list.DiffAwareHolder
+import com.pluto.utilities.list.ListItem
+import com.pluto.utilities.setOnDebounceClickListener
+import com.pluto.utilities.spannable.setSpan
+
+internal class CrashItemHolder(
+ parent: ViewGroup,
+ actionListener: DiffAwareAdapter.OnActionListener
+) : DiffAwareHolder(parent.inflate(R.layout.pluto_excep___item_crash), actionListener) {
+
+ private val binding = PlutoExcepItemCrashBinding.bind(itemView)
+ private val timeElapsed = binding.timeElapsed
+
+ override fun onBind(item: ListItem) {
+ if (item is ExceptionEntity) {
+ with(item.data.exception) {
+ if (isANRException) {
+ binding.message.text =
+ context.getString(R.string.pluto_excep___anr_list_message, PlutoExceptions.mainThreadResponseThreshold)
+ binding.title.setSpan {
+ context.apply {
+ append(
+ fontColor(
+ getString(R.string.pluto_excep___anr_list_title), color(com.pluto.plugin.R.color.pluto___text_dark_80)
+ )
+ )
+ }
+ }
+ binding.title.setCompoundDrawablesWithIntrinsicBounds(R.drawable.pluto_excep___ic_anr_warning, 0, 0, 0)
+ } else {
+ binding.message.setSpan {
+ append("${item.data.exception.file}\t\t")
+ append(
+ fontColor(
+ "line:${item.data.exception.lineNumber}",
+ context.color(com.pluto.plugin.R.color.pluto___text_dark_60)
+ )
+ )
+ }
+ binding.title.text = item.data.exception.name
+ binding.title.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0)
+ }
+ }
+ timeElapsed.text = item.timestamp.asTimeElapsed()
+ itemView.setOnDebounceClickListener {
+ onAction("click")
+ }
+ }
+ }
+}
diff --git a/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/ui/holder/StackTraceListItemHolder.kt b/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/ui/holder/StackTraceListItemHolder.kt
new file mode 100644
index 000000000..162f2aecb
--- /dev/null
+++ b/pluto-plugins/plugins/exceptions/lib/src/main/java/com/pluto/plugins/exceptions/internal/ui/holder/StackTraceListItemHolder.kt
@@ -0,0 +1,52 @@
+package com.pluto.plugins.exceptions.internal.ui.holder
+
+import android.view.ViewGroup
+import com.pluto.plugins.exceptions.R
+import com.pluto.plugins.exceptions.databinding.PlutoExcepItemCrashDetailsThreadStackTraceListBinding
+import com.pluto.plugins.exceptions.internal.ProcessThread
+import com.pluto.plugins.exceptions.internal.getStateStringSpan
+import com.pluto.utilities.extensions.color
+import com.pluto.utilities.extensions.dp
+import com.pluto.utilities.extensions.inflate
+import com.pluto.utilities.list.DiffAwareAdapter
+import com.pluto.utilities.list.DiffAwareHolder
+import com.pluto.utilities.list.ListItem
+import com.pluto.utilities.spannable.setSpan
+
+internal class StackTraceListItemHolder(
+ parent: ViewGroup,
+ actionListener: DiffAwareAdapter.OnActionListener
+) : DiffAwareHolder(parent.inflate(R.layout.pluto_excep___item_crash_details_thread_stack_trace_list), actionListener) {
+
+ private val binding = PlutoExcepItemCrashDetailsThreadStackTraceListBinding.bind(itemView)
+
+ override fun onBind(item: ListItem) {
+ if (item is ProcessThread) {
+ binding.thread.text = item.name
+ binding.thread.setCompoundDrawablesWithIntrinsicBounds(
+ if (item.name == "main") R.drawable.pluto_excep___ic_main_thread else R.drawable.pluto_excep___ic_non_main_thread,
+ 0, 0, 0
+ )
+ binding.thread.compoundDrawablePadding = 8f.dp.toInt()
+ binding.threadState.text = getStateStringSpan(context, item.state)
+ binding.stackTrace.setSpan {
+ item.stackTrace.take(MAX_STACK_TRACE_LINES).forEach {
+ append(fontColor(" at ", context.color(com.pluto.plugin.R.color.pluto___text_dark_40)))
+ append("$it\n")
+ }
+ val extraTrace = item.stackTrace.size - MAX_STACK_TRACE_LINES
+ if (extraTrace > 0) {
+ append(
+ fontColor(
+ "\t + $extraTrace more lines\n", context.color(com.pluto.plugin.R.color.pluto___text_dark_40)
+ )
+ )
+ }
+ }
+ }
+ }
+
+ companion object {
+ internal const val MAX_STACK_TRACE_LINES = 10
+ }
+}
diff --git a/pluto-plugins/plugins/exceptions/lib/src/main/res/drawable/pluto_excep___bg_section_body.xml b/pluto-plugins/plugins/exceptions/lib/src/main/res/drawable/pluto_excep___bg_section_body.xml
new file mode 100644
index 000000000..03ed5cb53
--- /dev/null
+++ b/pluto-plugins/plugins/exceptions/lib/src/main/res/drawable/pluto_excep___bg_section_body.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pluto-plugins/plugins/exceptions/lib/src/main/res/drawable/pluto_excep___bg_section_header.xml b/pluto-plugins/plugins/exceptions/lib/src/main/res/drawable/pluto_excep___bg_section_header.xml
new file mode 100644
index 000000000..755f56187
--- /dev/null
+++ b/pluto-plugins/plugins/exceptions/lib/src/main/res/drawable/pluto_excep___bg_section_header.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pluto-plugins/plugins/exceptions/lib/src/main/res/drawable/pluto_excep___bg_section_light.xml b/pluto-plugins/plugins/exceptions/lib/src/main/res/drawable/pluto_excep___bg_section_light.xml
new file mode 100644
index 000000000..9277baf22
--- /dev/null
+++ b/pluto-plugins/plugins/exceptions/lib/src/main/res/drawable/pluto_excep___bg_section_light.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pluto-plugins/plugins/exceptions/lib/src/main/res/drawable/pluto_excep___ic_anr_warning.xml b/pluto-plugins/plugins/exceptions/lib/src/main/res/drawable/pluto_excep___ic_anr_warning.xml
new file mode 100644
index 000000000..c654e361e
--- /dev/null
+++ b/pluto-plugins/plugins/exceptions/lib/src/main/res/drawable/pluto_excep___ic_anr_warning.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/pluto-plugins/plugins/exceptions/lib/src/main/res/drawable/pluto_excep___ic_arrow_back.xml b/pluto-plugins/plugins/exceptions/lib/src/main/res/drawable/pluto_excep___ic_arrow_back.xml
new file mode 100644
index 000000000..bd6a5a9cb
--- /dev/null
+++ b/pluto-plugins/plugins/exceptions/lib/src/main/res/drawable/pluto_excep___ic_arrow_back.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/pluto-plugins/plugins/exceptions/lib/src/main/res/drawable/pluto_excep___ic_chevron_right.xml b/pluto-plugins/plugins/exceptions/lib/src/main/res/drawable/pluto_excep___ic_chevron_right.xml
new file mode 100644
index 000000000..f04321855
--- /dev/null
+++ b/pluto-plugins/plugins/exceptions/lib/src/main/res/drawable/pluto_excep___ic_chevron_right.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/pluto/src/main/res/drawable/pluto___ic_clear_all.xml b/pluto-plugins/plugins/exceptions/lib/src/main/res/drawable/pluto_excep___ic_clear_all.xml
similarity index 100%
rename from pluto/src/main/res/drawable/pluto___ic_clear_all.xml
rename to pluto-plugins/plugins/exceptions/lib/src/main/res/drawable/pluto_excep___ic_clear_all.xml
diff --git a/pluto-plugins/plugins/exceptions/lib/src/main/res/drawable/pluto_excep___ic_close.xml b/pluto-plugins/plugins/exceptions/lib/src/main/res/drawable/pluto_excep___ic_close.xml
new file mode 100644
index 000000000..82eaa9f97
--- /dev/null
+++ b/pluto-plugins/plugins/exceptions/lib/src/main/res/drawable/pluto_excep___ic_close.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/pluto-plugins/plugins/exceptions/lib/src/main/res/drawable/pluto_excep___ic_delete.xml b/pluto-plugins/plugins/exceptions/lib/src/main/res/drawable/pluto_excep___ic_delete.xml
new file mode 100644
index 000000000..af232e55b
--- /dev/null
+++ b/pluto-plugins/plugins/exceptions/lib/src/main/res/drawable/pluto_excep___ic_delete.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/pluto-plugins/plugins/exceptions/lib/src/main/res/drawable/pluto_excep___ic_dropdown_cta.xml b/pluto-plugins/plugins/exceptions/lib/src/main/res/drawable/pluto_excep___ic_dropdown_cta.xml
new file mode 100644
index 000000000..a458d77bf
--- /dev/null
+++ b/pluto-plugins/plugins/exceptions/lib/src/main/res/drawable/pluto_excep___ic_dropdown_cta.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/pluto/src/main/res/drawable/pluto___ic_error.xml b/pluto-plugins/plugins/exceptions/lib/src/main/res/drawable/pluto_excep___ic_error.xml
similarity index 100%
rename from pluto/src/main/res/drawable/pluto___ic_error.xml
rename to pluto-plugins/plugins/exceptions/lib/src/main/res/drawable/pluto_excep___ic_error.xml
diff --git a/pluto-plugins/plugins/exceptions/lib/src/main/res/drawable/pluto_excep___ic_filter.xml b/pluto-plugins/plugins/exceptions/lib/src/main/res/drawable/pluto_excep___ic_filter.xml
new file mode 100644
index 000000000..0f02e925e
--- /dev/null
+++ b/pluto-plugins/plugins/exceptions/lib/src/main/res/drawable/pluto_excep___ic_filter.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/pluto-plugins/plugins/exceptions/lib/src/main/res/drawable/pluto_excep___ic_filter_light.xml b/pluto-plugins/plugins/exceptions/lib/src/main/res/drawable/pluto_excep___ic_filter_light.xml
new file mode 100644
index 000000000..dcfa71067
--- /dev/null
+++ b/pluto-plugins/plugins/exceptions/lib/src/main/res/drawable/pluto_excep___ic_filter_light.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/pluto-plugins/plugins/exceptions/lib/src/main/res/drawable/pluto_excep___ic_main_thread.xml b/pluto-plugins/plugins/exceptions/lib/src/main/res/drawable/pluto_excep___ic_main_thread.xml
new file mode 100644
index 000000000..b335b3580
--- /dev/null
+++ b/pluto-plugins/plugins/exceptions/lib/src/main/res/drawable/pluto_excep___ic_main_thread.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/pluto-plugins/plugins/exceptions/lib/src/main/res/drawable/pluto_excep___ic_more.xml b/pluto-plugins/plugins/exceptions/lib/src/main/res/drawable/pluto_excep___ic_more.xml
new file mode 100644
index 000000000..e5db6edf0
--- /dev/null
+++ b/pluto-plugins/plugins/exceptions/lib/src/main/res/drawable/pluto_excep___ic_more.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/pluto-plugins/plugins/exceptions/lib/src/main/res/drawable/pluto_excep___ic_non_main_thread.xml b/pluto-plugins/plugins/exceptions/lib/src/main/res/drawable/pluto_excep___ic_non_main_thread.xml
new file mode 100644
index 000000000..33fd292f0
--- /dev/null
+++ b/pluto-plugins/plugins/exceptions/lib/src/main/res/drawable/pluto_excep___ic_non_main_thread.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/pluto-plugins/plugins/exceptions/lib/src/main/res/drawable/pluto_excep___ic_plugin_logo.xml b/pluto-plugins/plugins/exceptions/lib/src/main/res/drawable/pluto_excep___ic_plugin_logo.xml
new file mode 100644
index 000000000..70b4e7952
--- /dev/null
+++ b/pluto-plugins/plugins/exceptions/lib/src/main/res/drawable/pluto_excep___ic_plugin_logo.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/pluto-plugins/plugins/exceptions/lib/src/main/res/drawable/pluto_excep___ic_share.xml b/pluto-plugins/plugins/exceptions/lib/src/main/res/drawable/pluto_excep___ic_share.xml
new file mode 100644
index 000000000..85c8262d6
--- /dev/null
+++ b/pluto-plugins/plugins/exceptions/lib/src/main/res/drawable/pluto_excep___ic_share.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/pluto-plugins/plugins/exceptions/lib/src/main/res/drawable/pluto_excep___ic_thread_traces.xml b/pluto-plugins/plugins/exceptions/lib/src/main/res/drawable/pluto_excep___ic_thread_traces.xml
new file mode 100644
index 000000000..96f1496d5
--- /dev/null
+++ b/pluto-plugins/plugins/exceptions/lib/src/main/res/drawable/pluto_excep___ic_thread_traces.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
diff --git a/pluto-plugins/plugins/exceptions/lib/src/main/res/layout/pluto_excep___fragment_base.xml b/pluto-plugins/plugins/exceptions/lib/src/main/res/layout/pluto_excep___fragment_base.xml
new file mode 100644
index 000000000..a2a5f313b
--- /dev/null
+++ b/pluto-plugins/plugins/exceptions/lib/src/main/res/layout/pluto_excep___fragment_base.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pluto-plugins/plugins/exceptions/lib/src/main/res/layout/pluto_excep___fragment_details.xml b/pluto-plugins/plugins/exceptions/lib/src/main/res/layout/pluto_excep___fragment_details.xml
new file mode 100644
index 000000000..a16b3eff2
--- /dev/null
+++ b/pluto-plugins/plugins/exceptions/lib/src/main/res/layout/pluto_excep___fragment_details.xml
@@ -0,0 +1,116 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pluto-plugins/plugins/exceptions/lib/src/main/res/layout/pluto_excep___fragment_list.xml b/pluto-plugins/plugins/exceptions/lib/src/main/res/layout/pluto_excep___fragment_list.xml
new file mode 100644
index 000000000..1d2b2d14f
--- /dev/null
+++ b/pluto-plugins/plugins/exceptions/lib/src/main/res/layout/pluto_excep___fragment_list.xml
@@ -0,0 +1,166 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pluto-plugins/plugins/exceptions/lib/src/main/res/layout/pluto_excep___fragment_thread_stack_trace.xml b/pluto-plugins/plugins/exceptions/lib/src/main/res/layout/pluto_excep___fragment_thread_stack_trace.xml
new file mode 100644
index 000000000..cc118e56a
--- /dev/null
+++ b/pluto-plugins/plugins/exceptions/lib/src/main/res/layout/pluto_excep___fragment_thread_stack_trace.xml
@@ -0,0 +1,144 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pluto-plugins/plugins/exceptions/lib/src/main/res/layout/pluto_excep___item_crash.xml b/pluto-plugins/plugins/exceptions/lib/src/main/res/layout/pluto_excep___item_crash.xml
new file mode 100644
index 000000000..1511b3d40
--- /dev/null
+++ b/pluto-plugins/plugins/exceptions/lib/src/main/res/layout/pluto_excep___item_crash.xml
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pluto-plugins/plugins/exceptions/lib/src/main/res/layout/pluto_excep___item_crash_details_device.xml b/pluto-plugins/plugins/exceptions/lib/src/main/res/layout/pluto_excep___item_crash_details_device.xml
new file mode 100644
index 000000000..284539713
--- /dev/null
+++ b/pluto-plugins/plugins/exceptions/lib/src/main/res/layout/pluto_excep___item_crash_details_device.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
diff --git a/pluto-plugins/plugins/exceptions/lib/src/main/res/layout/pluto_excep___item_crash_details_header.xml b/pluto-plugins/plugins/exceptions/lib/src/main/res/layout/pluto_excep___item_crash_details_header.xml
new file mode 100644
index 000000000..4b4476b6d
--- /dev/null
+++ b/pluto-plugins/plugins/exceptions/lib/src/main/res/layout/pluto_excep___item_crash_details_header.xml
@@ -0,0 +1,98 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pluto-plugins/plugins/exceptions/lib/src/main/res/layout/pluto_excep___item_crash_details_thread.xml b/pluto-plugins/plugins/exceptions/lib/src/main/res/layout/pluto_excep___item_crash_details_thread.xml
new file mode 100644
index 000000000..227280409
--- /dev/null
+++ b/pluto-plugins/plugins/exceptions/lib/src/main/res/layout/pluto_excep___item_crash_details_thread.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
diff --git a/pluto-plugins/plugins/exceptions/lib/src/main/res/layout/pluto_excep___item_crash_details_thread_stack_trace.xml b/pluto-plugins/plugins/exceptions/lib/src/main/res/layout/pluto_excep___item_crash_details_thread_stack_trace.xml
new file mode 100644
index 000000000..0c0e84fab
--- /dev/null
+++ b/pluto-plugins/plugins/exceptions/lib/src/main/res/layout/pluto_excep___item_crash_details_thread_stack_trace.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pluto-plugins/plugins/exceptions/lib/src/main/res/layout/pluto_excep___item_crash_details_thread_stack_trace_list.xml b/pluto-plugins/plugins/exceptions/lib/src/main/res/layout/pluto_excep___item_crash_details_thread_stack_trace_list.xml
new file mode 100644
index 000000000..f374eaa3d
--- /dev/null
+++ b/pluto-plugins/plugins/exceptions/lib/src/main/res/layout/pluto_excep___item_crash_details_thread_stack_trace_list.xml
@@ -0,0 +1,70 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pluto-plugins/plugins/exceptions/lib/src/main/res/layout/pluto_excep___stub_crash_report.xml b/pluto-plugins/plugins/exceptions/lib/src/main/res/layout/pluto_excep___stub_crash_report.xml
new file mode 100644
index 000000000..a86ca7b78
--- /dev/null
+++ b/pluto-plugins/plugins/exceptions/lib/src/main/res/layout/pluto_excep___stub_crash_report.xml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pluto-plugins/plugins/exceptions/lib/src/main/res/menu/pluto_excep___menu_more_options.xml b/pluto-plugins/plugins/exceptions/lib/src/main/res/menu/pluto_excep___menu_more_options.xml
new file mode 100644
index 000000000..4170cc2cb
--- /dev/null
+++ b/pluto-plugins/plugins/exceptions/lib/src/main/res/menu/pluto_excep___menu_more_options.xml
@@ -0,0 +1,7 @@
+
+
\ No newline at end of file
diff --git a/pluto-plugins/plugins/exceptions/lib/src/main/res/menu/pluto_excep___menu_stack_trace_filter.xml b/pluto-plugins/plugins/exceptions/lib/src/main/res/menu/pluto_excep___menu_stack_trace_filter.xml
new file mode 100644
index 000000000..da55ae642
--- /dev/null
+++ b/pluto-plugins/plugins/exceptions/lib/src/main/res/menu/pluto_excep___menu_stack_trace_filter.xml
@@ -0,0 +1,18 @@
+
+
\ No newline at end of file
diff --git a/pluto-plugins/plugins/exceptions/lib/src/main/res/navigation/pluto_excep___navigation.xml b/pluto-plugins/plugins/exceptions/lib/src/main/res/navigation/pluto_excep___navigation.xml
new file mode 100644
index 000000000..cf37bf3fd
--- /dev/null
+++ b/pluto-plugins/plugins/exceptions/lib/src/main/res/navigation/pluto_excep___navigation.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pluto-plugins/plugins/exceptions/lib/src/main/res/values/strings.xml b/pluto-plugins/plugins/exceptions/lib/src/main/res/values/strings.xml
new file mode 100644
index 000000000..d5a269dfe
--- /dev/null
+++ b/pluto-plugins/plugins/exceptions/lib/src/main/res/values/strings.xml
@@ -0,0 +1,43 @@
+
+
+ Crashes & ANRs
+ Pluto detected a crash in %s
+ Tap here to see crash details.
+ No Crashes or ANRs.
+ Search Exceptions
+ Delete All Crashes
+ No search result found.
+ Exit Crash Details
+ App State
+ App Version
+ Android OS
+ Android API Level
+ Orientation
+ Is Rooted
+ Device Information
+ Model
+ Height
+ Width
+ Density
+ Screen Size
+ Thread State
+ Name
+ State
+ Priority
+ Is Daemon
+ Oops! this looks like our miss.
+ Tap here to report this crash to us, so that you don\'t have to face this crash again.
+ Invalid Crash id
+ Loading Crashes & ANRs
+ ANR Thread Stack Traces
+ Main thread unresponsive for +%d ms
+ ANR detected
+ Look for threads in BLOCKED state.
+ Filter
+ Showing Thread traces for
+ Blocked
+ Waiting
+ Timed_Waiting
+ Runnable
+ All States
+
\ No newline at end of file
diff --git a/pluto-plugins/plugins/layout-inspector/README.md b/pluto-plugins/plugins/layout-inspector/README.md
new file mode 100644
index 000000000..5502b49d9
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/README.md
@@ -0,0 +1,38 @@
+## Integrate Layout Inspector Plugin in your application
+
+[Layout Inspector.webm](https://github.com/user-attachments/assets/ac566253-6787-4515-9cca-0d8d0a820976)
+
+### Add Gradle Dependencies
+Pluto Layout Inspector is distributed through [***mavenCentral***](https://central.sonatype.com/artifact/com.androidpluto.plugins/layout-inspector). To use it, you need to add the following Gradle dependency to your build.gradle file of you android app module.
+
+> Note: add the `no-op` variant to isolate the plugin from release builds.
+```groovy
+dependencies {
+ debugImplementation "com.androidpluto.plugins:layout-inspector:$plutoVersion"
+ releaseImplementation "com.androidpluto.plugins:layout-inspector-no-op:$plutoVersion"
+}
+```
+
+
+### Install plugin to Pluto
+
+Now to start using the plugin, add it to Pluto
+```kotlin
+Pluto.Installer(this)
+ .addPlugin(PlutoLayoutInspectorPlugin())
+ .install()
+```
+
+
+π You are all done!
+
+Now re-build and run your app and open Pluto, you will see the Layout Inspector plugin installed.
+
+
+
+
+### Open Plugin view programmatically
+To open Layout Inspector plugin screen via code, use this
+```kotlin
+Pluto.open(PlutoLayoutInspectorPlugin.ID)
+```
diff --git a/pluto-plugins/plugins/layout-inspector/lib-no-op/.gitignore b/pluto-plugins/plugins/layout-inspector/lib-no-op/.gitignore
new file mode 100644
index 000000000..42afabfd2
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib-no-op/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/pluto-plugins/plugins/layout-inspector/lib-no-op/build.gradle.kts b/pluto-plugins/plugins/layout-inspector/lib-no-op/build.gradle.kts
new file mode 100644
index 000000000..f5ea5f2e4
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib-no-op/build.gradle.kts
@@ -0,0 +1,91 @@
+import com.vanniktech.maven.publish.SonatypeHost
+
+plugins {
+ alias(libs.plugins.android.library)
+ alias(libs.plugins.kotlin.android)
+ alias(libs.plugins.maven.publish)
+}
+
+val version = Versioning.loadVersioningData()
+val verCode = version["code"] as Int
+val verPublish = version["publish"] as String
+val verGitSHA = version["gitSha"] as String
+
+android {
+ namespace = "com.pluto.plugins.layoutinspector"
+
+ compileSdk = libs.versions.compileSdk.get().toInt()
+ buildToolsVersion = libs.versions.buildTools.get()
+
+ buildFeatures {
+ viewBinding = true
+ }
+
+ defaultConfig {
+ minSdk = libs.versions.minSdk.get().toInt()
+ }
+
+ buildTypes {
+ getByName("release") {
+// isDebuggable = true
+ isMinifyEnabled = false
+ isShrinkResources = false
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.toVersion(libs.versions.java.get())
+ targetCompatibility = JavaVersion.toVersion(libs.versions.java.get())
+ }
+
+ kotlinOptions {
+ jvmTarget = libs.versions.java.get()
+ }
+
+ lint {
+ abortOnError = false
+ targetSdk = libs.versions.targetSdk.get().toInt()
+ }
+}
+
+extra["PUBLISH_GROUP_ID"] = "com.androidpluto.plugins"
+extra["PUBLISH_ARTIFACT_ID"] = "layout-inspector-no-op"
+extra["PUBLISH_ARTIFACT_NAME"] = "Android Pluto Layout inspector Plugin"
+extra["PUBLISH_ARTIFACT_DESCRIPTION"] = "Plugin to modify screen layout in Android Pluto"
+
+mavenPublishing {
+ coordinates(
+ groupId = extra["PUBLISH_GROUP_ID"] as String,
+ artifactId = extra["PUBLISH_ARTIFACT_ID"] as String,
+ version = verPublish
+ )
+ pom {
+ name.set(extra["PUBLISH_ARTIFACT_NAME"] as String)
+ description.set(extra["PUBLISH_ARTIFACT_DESCRIPTION"] as String)
+ inceptionYear.set(project.findProperty("pom.inceptionYear") as? String)
+ url.set(project.findProperty("pom.url") as? String)
+ licenses {
+ license {
+ name.set(project.findProperty("pom.license.name") as? String)
+ url.set(project.findProperty("pom.license.url") as? String)
+ }
+ }
+ developers {
+ developer {
+ id.set(project.findProperty("pom.developer.id") as? String)
+ name.set(project.findProperty("pom.developer.name") as? String)
+ email.set(project.findProperty("pom.developer.email") as? String)
+ }
+ }
+ scm {
+ connection.set(project.findProperty("pom.scm.connection") as? String)
+ developerConnection.set(project.findProperty("pom.scm.developerConnection") as? String)
+ url.set(project.findProperty("pom.scm.url") as? String)
+ }
+ }
+ publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL)
+ signAllPublications()
+}
+
+dependencies {
+}
diff --git a/pluto-plugins/plugins/layout-inspector/lib-no-op/src/main/AndroidManifest.xml b/pluto-plugins/plugins/layout-inspector/lib-no-op/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..69fc41290
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib-no-op/src/main/AndroidManifest.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/pluto-plugins/plugins/layout-inspector/lib-no-op/src/main/java/com/pluto/plugins/layoutinspector/PlutoLayoutInspectorPlugin.kt b/pluto-plugins/plugins/layout-inspector/lib-no-op/src/main/java/com/pluto/plugins/layoutinspector/PlutoLayoutInspectorPlugin.kt
new file mode 100644
index 000000000..d6b194575
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib-no-op/src/main/java/com/pluto/plugins/layoutinspector/PlutoLayoutInspectorPlugin.kt
@@ -0,0 +1,8 @@
+package com.pluto.plugins.layoutinspector
+
+@SuppressWarnings("UnusedPrivateMember")
+class PlutoLayoutInspectorPlugin @JvmOverloads constructor(identifier: String = ID) {
+ companion object {
+ const val ID = "layout-inspector"
+ }
+}
diff --git a/pluto-plugins/plugins/layout-inspector/lib/.gitignore b/pluto-plugins/plugins/layout-inspector/lib/.gitignore
new file mode 100644
index 000000000..42afabfd2
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/pluto-plugins/plugins/layout-inspector/lib/build.gradle.kts b/pluto-plugins/plugins/layout-inspector/lib/build.gradle.kts
new file mode 100644
index 000000000..d522e4c4a
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/build.gradle.kts
@@ -0,0 +1,100 @@
+import com.vanniktech.maven.publish.SonatypeHost
+
+plugins {
+ alias(libs.plugins.android.library)
+ alias(libs.plugins.kotlin.android)
+ alias(libs.plugins.maven.publish)
+}
+
+val version = Versioning.loadVersioningData()
+val verCode = version["code"] as Int
+val verPublish = version["publish"] as String
+val verGitSHA = version["gitSha"] as String
+
+android {
+ namespace = "com.pluto.plugins.layoutinspector"
+ resourcePrefix = "pluto_li___"
+
+ compileSdk = libs.versions.compileSdk.get().toInt()
+ buildToolsVersion = libs.versions.buildTools.get()
+
+ buildFeatures {
+ buildConfig = true
+ viewBinding = true
+ }
+
+ defaultConfig {
+ minSdk = libs.versions.minSdk.get().toInt()
+
+ buildConfigField("String", "VERSION_NAME", "\"$verPublish\"")
+ buildConfigField("long", "VERSION_CODE", "$verCode")
+ buildConfigField("String", "GIT_SHA", "\"$verGitSHA\"")
+ }
+
+ buildTypes {
+ getByName("release") {
+// isDebuggable = true
+ isMinifyEnabled = false
+ isShrinkResources = false
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.toVersion(libs.versions.java.get())
+ targetCompatibility = JavaVersion.toVersion(libs.versions.java.get())
+ }
+
+ kotlinOptions {
+ jvmTarget = libs.versions.java.get()
+ }
+
+ lint {
+ abortOnError = false
+ targetSdk = libs.versions.targetSdk.get().toInt()
+ }
+}
+
+extra["PUBLISH_GROUP_ID"] = "com.androidpluto.plugins"
+extra["PUBLISH_ARTIFACT_ID"] = "layout-inspector"
+extra["PUBLISH_ARTIFACT_NAME"] = "Android Pluto Layout inspector Plugin"
+extra["PUBLISH_ARTIFACT_DESCRIPTION"] = "Plugin to modify screen layout in Android Pluto"
+
+mavenPublishing {
+ coordinates(
+ groupId = extra["PUBLISH_GROUP_ID"] as String,
+ artifactId = extra["PUBLISH_ARTIFACT_ID"] as String,
+ version = verPublish
+ )
+ pom {
+ name.set(extra["PUBLISH_ARTIFACT_NAME"] as String)
+ description.set(extra["PUBLISH_ARTIFACT_DESCRIPTION"] as String)
+ inceptionYear.set(project.findProperty("pom.inceptionYear") as? String)
+ url.set(project.findProperty("pom.url") as? String)
+ licenses {
+ license {
+ name.set(project.findProperty("pom.license.name") as? String)
+ url.set(project.findProperty("pom.license.url") as? String)
+ }
+ }
+ developers {
+ developer {
+ id.set(project.findProperty("pom.developer.id") as? String)
+ name.set(project.findProperty("pom.developer.name") as? String)
+ email.set(project.findProperty("pom.developer.email") as? String)
+ }
+ }
+ scm {
+ connection.set(project.findProperty("pom.scm.connection") as? String)
+ developerConnection.set(project.findProperty("pom.scm.developerConnection") as? String)
+ url.set(project.findProperty("pom.scm.url") as? String)
+ }
+ }
+ publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL)
+ signAllPublications()
+}
+
+dependencies {
+ implementation(project(":pluto-plugins:base:lib"))
+
+ implementation(libs.androidx.core)
+}
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/AndroidManifest.xml b/pluto-plugins/plugins/layout-inspector/lib/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..69fc41290
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/AndroidManifest.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/BaseFragment.kt b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/BaseFragment.kt
new file mode 100644
index 000000000..79d25b820
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/BaseFragment.kt
@@ -0,0 +1,5 @@
+package com.pluto.plugins.layoutinspector
+
+import androidx.fragment.app.Fragment
+
+internal class BaseFragment : Fragment(R.layout.pluto_li___fragment_base)
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/PlutoLayoutInspectorPlugin.kt b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/PlutoLayoutInspectorPlugin.kt
new file mode 100644
index 000000000..f8bce1d1d
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/PlutoLayoutInspectorPlugin.kt
@@ -0,0 +1,41 @@
+package com.pluto.plugins.layoutinspector
+
+import androidx.fragment.app.Fragment
+import com.pluto.plugin.DeveloperDetails
+import com.pluto.plugin.Plugin
+import com.pluto.plugin.PluginConfiguration
+import com.pluto.plugins.layoutinspector.internal.ActivityLifecycle
+
+class PlutoLayoutInspectorPlugin() : Plugin(ID) {
+
+ @SuppressWarnings("UnusedPrivateMember")
+ @Deprecated("Use the default constructor PlutoLayoutInspectorPlugin() instead.")
+ constructor(identifier: String) : this()
+
+ override fun getConfig() = PluginConfiguration(
+ name = context.getString(R.string.pluto_li___plugin_name),
+ icon = R.drawable.pluto_li___ic_plugin_logo,
+ version = BuildConfig.VERSION_NAME
+ )
+
+ override fun getView(): Fragment = BaseFragment()
+
+ override fun getDeveloperDetails(): DeveloperDetails {
+ return DeveloperDetails(
+ website = "https://androidpluto.com",
+ vcsLink = "https://github.com/androidPluto/pluto",
+ twitter = "https://twitter.com/android_pluto"
+ )
+ }
+
+ override fun onPluginDataCleared() {
+ }
+
+ override fun onPluginInstalled() {
+ application.registerActivityLifecycleCallbacks(ActivityLifecycle())
+ }
+
+ companion object {
+ const val ID = "layout-inspector"
+ }
+}
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/ViewInfoFragment.kt b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/ViewInfoFragment.kt
new file mode 100644
index 000000000..20d894fc3
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/ViewInfoFragment.kt
@@ -0,0 +1,137 @@
+package com.pluto.plugins.layoutinspector
+
+import android.os.Bundle
+import android.view.View
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.core.os.bundleOf
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.Observer
+import androidx.navigation.fragment.findNavController
+import com.google.android.material.bottomsheet.BottomSheetBehavior
+import com.pluto.plugins.layoutinspector.databinding.PlutoLiFragmentViewInfoBinding
+import com.pluto.plugins.layoutinspector.internal.ActivityLifecycle
+import com.pluto.plugins.layoutinspector.internal.control.ControlCta
+import com.pluto.plugins.layoutinspector.internal.control.ControlsWidget
+import com.pluto.plugins.layoutinspector.internal.hierarchy.ViewHierarchyFragment.Companion.SCROLL_TO_TARGET
+import com.pluto.plugins.layoutinspector.internal.hint.HintFragment
+import com.pluto.plugins.layoutinspector.internal.inspect.InspectViewModel
+import com.pluto.plugins.layoutinspector.internal.inspect.assignTargetTag
+import com.pluto.plugins.layoutinspector.internal.inspect.clearTargetTag
+import com.pluto.utilities.viewBinding
+
+internal class ViewInfoFragment : Fragment(R.layout.pluto_li___fragment_view_info), View.OnClickListener {
+
+ private lateinit var behavior: BottomSheetBehavior
+ private var targetView: View? = null
+ private val binding by viewBinding(PlutoLiFragmentViewInfoBinding::bind)
+ private val inspectViewModel: InspectViewModel by activityViewModels()
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _, insets ->
+ val systemBarsInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars())
+ binding.bsContainer.previewPanelBottomSheet.setPadding(0, 0, 0, systemBarsInsets.bottom)
+ insets
+ }
+ ActivityLifecycle.topActivity?.let { binding.operableView.tryGetFrontView(it) }
+ binding.operableView.setOnClickListener(this)
+ binding.leftControls.initialise(
+ listOf(
+ ControlCta(ID_CLOSE, R.drawable.pluto_li___ic_control_close, getString(R.string.pluto_li___cntrl_title_close)),
+ ControlCta(ID_HINT, R.drawable.pluto_li___ic_control_hint, getString(R.string.pluto_li___cntrl_title_hint)),
+ ControlCta(ID_HIERARCHY, R.drawable.pluto_li___ic_view_hierarchy, getString(R.string.pluto_li___cntrl_title_hierarchy)),
+ ControlCta(ID_MOVE_RIGHT, R.drawable.pluto_li___ic_control_move_right, getString(R.string.pluto_li___cntrl_title_move_to_right))
+ ),
+ onControlCtaListener
+ )
+
+ binding.rightControls.initialise(
+ listOf(
+ ControlCta(ID_MOVE_LEFT, R.drawable.pluto_li___ic_control_move_left, getString(R.string.pluto_li___cntrl_title_move_to_left)),
+ ControlCta(ID_HIERARCHY, R.drawable.pluto_li___ic_view_hierarchy, getString(R.string.pluto_li___cntrl_title_hierarchy)),
+ ControlCta(ID_HINT, R.drawable.pluto_li___ic_control_hint, getString(R.string.pluto_li___cntrl_title_hint)),
+ ControlCta(ID_CLOSE, R.drawable.pluto_li___ic_control_close, getString(R.string.pluto_li___cntrl_title_close))
+ ),
+ onControlCtaListener
+ )
+ binding.leftControls.visibility = View.GONE
+ setupPreviewPanel()
+
+ inspectViewModel.view.removeObserver(inspectRequestObserver)
+ inspectViewModel.view.observe(viewLifecycleOwner, inspectRequestObserver)
+ }
+
+ private fun setupPreviewPanel() {
+ behavior = BottomSheetBehavior.from(binding.bsContainer.previewPanelBottomSheet)
+ behavior.state = BottomSheetBehavior.STATE_HIDDEN
+ }
+
+ override fun onClick(view: View) {
+ clearPreviousSelection()
+ if (binding.operableView.isSelectedEmpty()) {
+ behavior.state = BottomSheetBehavior.STATE_HIDDEN
+ } else {
+ targetView = view
+ targetView?.assignTargetTag()
+ behavior.state = BottomSheetBehavior.STATE_EXPANDED
+ refreshViewDetails(view)
+ }
+ }
+
+ private fun clearPreviousSelection() {
+ targetView?.clearTargetTag()
+ targetView = null
+ }
+
+ private fun refreshViewDetails(view: View) {
+ binding.bsContainer.previewPanel.refresh(
+ view = view,
+ onViewAttrRequested = {
+ findNavController().navigate(R.id.openAttrView)
+ },
+ onViewHierarchyRequested = {
+ val bundle = bundleOf(SCROLL_TO_TARGET to true)
+ findNavController().navigate(R.id.openHierarchyView, bundle)
+ },
+ onCloseRequested = {
+ clearPreviousSelection()
+ binding.operableView.handleClick(view, true)
+ }
+ )
+ }
+
+ private val inspectRequestObserver = Observer {
+ binding.operableView.handleClick(it, false)
+ }
+
+ private val onControlCtaListener = object : ControlsWidget.OnClickListener {
+ override fun onClick(id: String) {
+ when (id) {
+ ID_MOVE_RIGHT -> {
+ binding.leftControls.visibility = View.GONE
+ binding.rightControls.visibility = View.VISIBLE
+ }
+
+ ID_MOVE_LEFT -> {
+ binding.leftControls.visibility = View.VISIBLE
+ binding.rightControls.visibility = View.GONE
+ }
+
+ ID_HIERARCHY -> findNavController().navigate(R.id.openHierarchyView)
+ ID_CLOSE -> requireActivity().finish()
+ ID_HINT -> HintFragment().show(requireActivity().supportFragmentManager, "hint")
+ }
+ }
+ }
+
+ private companion object {
+ const val ID_CLOSE = "close"
+ const val ID_HINT = "hint"
+ const val ID_HIERARCHY = "hierarchy"
+ const val ID_MOVE_LEFT = "moveToLeft"
+ const val ID_MOVE_RIGHT = "moveToRight"
+ }
+}
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/ActivityLifecycle.kt b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/ActivityLifecycle.kt
new file mode 100644
index 000000000..09484db1d
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/ActivityLifecycle.kt
@@ -0,0 +1,32 @@
+package com.pluto.plugins.layoutinspector.internal
+
+import android.app.Activity
+import android.app.Application.ActivityLifecycleCallbacks
+import android.os.Bundle
+import com.pluto.plugin.libinterface.PlutoInterface
+
+internal class ActivityLifecycle : ActivityLifecycleCallbacks {
+
+ companion object {
+ var topActivity: Activity? = null
+ }
+
+ override fun onActivityStarted(activity: Activity) {}
+ override fun onActivityStopped(activity: Activity) {}
+ override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {}
+ override fun onActivityResumed(activity: Activity) {
+ if (activity.javaClass != PlutoInterface.libInfo.pluginActivityClass && activity.javaClass != PlutoInterface.libInfo.selectorActivityClass) {
+ topActivity = activity
+ }
+ }
+
+ override fun onActivityPaused(activity: Activity) {}
+ override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
+ override fun onActivityDestroyed(activity: Activity) {
+ topActivity?.let {
+ if (it == activity) {
+ topActivity = null
+ }
+ }
+ }
+}
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/ParamsPreviewPanel.kt b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/ParamsPreviewPanel.kt
new file mode 100644
index 000000000..4e1ff1bed
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/ParamsPreviewPanel.kt
@@ -0,0 +1,45 @@
+package com.pluto.plugins.layoutinspector.internal
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.constraintlayout.widget.ConstraintLayout
+import com.pluto.plugins.layoutinspector.R
+import com.pluto.plugins.layoutinspector.databinding.PlutoLiParamsPreviewPanelBinding
+import com.pluto.plugins.layoutinspector.internal.inspect.getIdString
+import com.pluto.utilities.extensions.color
+import com.pluto.utilities.setOnDebounceClickListener
+import com.pluto.utilities.spannable.setSpan
+
+internal class ParamsPreviewPanel : ConstraintLayout {
+
+ private val binding = PlutoLiParamsPreviewPanelBinding.inflate(LayoutInflater.from(context), this, true)
+
+ constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle)
+ constructor(context: Context, attrs: AttributeSet) : super(context, attrs, 0)
+ constructor(context: Context) : super(context, null, 0)
+
+ fun refresh(view: View, onViewAttrRequested: () -> Unit, onViewHierarchyRequested: () -> Unit, onCloseRequested: () -> Unit) {
+ binding.viewId.setSpan {
+ view.getIdString()?.let {
+ append(it)
+ } ?: run {
+ append(regular(italic(fontColor("NO_ID", context.color(com.pluto.plugin.R.color.pluto___text_dark_40)))))
+ }
+ }
+ binding.viewType.text = if (view is ViewGroup) "viewGroup" else "view"
+ binding.viewClass.text = view.javaClass.canonicalName
+
+ binding.viewAttrCta.setOnDebounceClickListener {
+ onViewAttrRequested.invoke()
+ }
+ binding.viewHierarchyCta.setOnDebounceClickListener {
+ onViewHierarchyRequested.invoke()
+ }
+ binding.close.setOnDebounceClickListener {
+ onCloseRequested.invoke()
+ }
+ }
+}
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/attributes/ViewAttrFragment.kt b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/attributes/ViewAttrFragment.kt
new file mode 100644
index 000000000..3a9ca17b6
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/attributes/ViewAttrFragment.kt
@@ -0,0 +1,156 @@
+package com.pluto.plugins.layoutinspector.internal.attributes
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.View.VISIBLE
+import android.view.ViewGroup
+import androidx.fragment.app.viewModels
+import androidx.lifecycle.Observer
+import com.google.android.material.bottomsheet.BottomSheetBehavior
+import com.google.android.material.bottomsheet.BottomSheetDialog
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment
+import com.pluto.plugin.share.ContentShareViewModel
+import com.pluto.plugin.share.Shareable
+import com.pluto.plugin.share.lazyContentSharer
+import com.pluto.plugins.layoutinspector.R
+import com.pluto.plugins.layoutinspector.databinding.PlutoLiFragmentViewAttrBinding
+import com.pluto.plugins.layoutinspector.internal.ActivityLifecycle
+import com.pluto.plugins.layoutinspector.internal.attributes.data.MutableAttribute
+import com.pluto.plugins.layoutinspector.internal.attributes.list.AttributeAdapter
+import com.pluto.plugins.layoutinspector.internal.inspect.clearTargetTag
+import com.pluto.plugins.layoutinspector.internal.inspect.findViewByTargetTag
+import com.pluto.plugins.layoutinspector.internal.inspect.getFrontView
+import com.pluto.plugins.layoutinspector.internal.inspect.getIdString
+import com.pluto.utilities.autoClearInitializer
+import com.pluto.utilities.device.Device
+import com.pluto.utilities.extensions.color
+import com.pluto.utilities.list.BaseAdapter
+import com.pluto.utilities.list.DiffAwareAdapter
+import com.pluto.utilities.list.DiffAwareHolder
+import com.pluto.utilities.list.ListItem
+import com.pluto.utilities.setOnDebounceClickListener
+import com.pluto.utilities.spannable.setSpan
+import com.pluto.utilities.viewBinding
+import com.pluto.utilities.views.keyvalue.KeyValuePairEditResult
+import com.pluto.utilities.views.keyvalue.edit.KeyValuePairEditor
+import com.pluto.utilities.views.keyvalue.edit.lazyKeyValuePairEditor
+
+internal class ViewAttrFragment : BottomSheetDialogFragment() {
+
+ private val binding by viewBinding(PlutoLiFragmentViewAttrBinding::bind)
+ private val contentSharer: ContentShareViewModel by lazyContentSharer()
+ private var targetView: View? = null
+ private val attributeAdapter: BaseAdapter by autoClearInitializer {
+ AttributeAdapter(onActionListener)
+ }
+ private val viewModel: ViewAttrViewModel by viewModels()
+ private val keyValuePairEditor: KeyValuePairEditor by lazyKeyValuePairEditor()
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
+ inflater.inflate(R.layout.pluto_li___fragment_view_attr, container, false)
+
+ override fun getTheme(): Int = R.style.PlutoLIBottomSheetDialog
+
+ override fun onStart() {
+ super.onStart()
+ val width = ViewGroup.LayoutParams.MATCH_PARENT
+ val height = ViewGroup.LayoutParams.MATCH_PARENT
+ dialog?.window?.setLayout(width, height)
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ ActivityLifecycle.topActivity?.let {
+ it.getFrontView().findViewByTargetTag()?.let { view ->
+ targetView = view
+ } ?: run {
+ targetView?.clearTargetTag()
+ }
+ }
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ dialog?.setOnShowListener {
+ val dialog = it as BottomSheetDialog
+ val bottomSheet = dialog.findViewById(com.google.android.material.R.id.design_bottom_sheet)
+ bottomSheet?.let {
+ dialog.behavior.peekHeight = Device(requireContext()).screen.heightPx
+ dialog.behavior.state = BottomSheetBehavior.STATE_EXPANDED
+ }
+ }
+ targetView?.let { target ->
+ binding.close.setOnDebounceClickListener {
+ dismiss()
+ }
+ binding.share.setOnDebounceClickListener {
+ viewModel.shareableAttr.value?.let {
+ contentSharer.share(
+ Shareable(
+ title = "Sharing View Attributes", content = it, fileName = "View Attributes generated via Pluto"
+ )
+ )
+ }
+ }
+ binding.title.setSpan {
+ append(semiBold(target.javaClass.simpleName))
+ append("\n")
+ target.getIdString()?.let {
+ append(regular(fontSize(it, SUBTITLE_TEXT_SIZE_IN_SP)))
+ } ?: run {
+ append(
+ regular(
+ fontSize(
+ italic(
+ fontColor(
+ "NO_ID",
+ context.color(com.pluto.plugin.R.color.pluto___text_dark_40)
+ )
+ ),
+ SUBTITLE_TEXT_SIZE_IN_SP
+ )
+ )
+ )
+ }
+ }
+ binding.attrList.apply {
+ adapter = attributeAdapter
+ }
+
+ keyValuePairEditor.result.removeObserver(keyValuePairEditObserver)
+ keyValuePairEditor.result.observe(viewLifecycleOwner, keyValuePairEditObserver)
+
+ viewModel.list.removeObserver(parsedAttrObserver)
+ viewModel.list.observe(viewLifecycleOwner, parsedAttrObserver)
+ viewModel.parse(target)
+ }
+ }
+
+ private val parsedAttrObserver = Observer> {
+ binding.share.visibility = VISIBLE
+ attributeAdapter.list = it
+ }
+
+ private val keyValuePairEditObserver = Observer {
+ targetView?.let { view ->
+ it.value?.let { value ->
+ if (it.metaData is MutableAttribute) {
+ viewModel.updateAttributeValue(view, it.metaData as MutableAttribute, value)
+ }
+ }
+ }
+ }
+
+ private val onActionListener = object : DiffAwareAdapter.OnActionListener {
+ override fun onAction(action: String, data: ListItem, holder: DiffAwareHolder) {
+ if (data is MutableAttribute) {
+ keyValuePairEditor.edit(data.requestEdit())
+ }
+ }
+ }
+
+ private companion object {
+ const val SUBTITLE_TEXT_SIZE_IN_SP = 12
+ }
+}
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/attributes/ViewAttrViewModel.kt b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/attributes/ViewAttrViewModel.kt
new file mode 100644
index 000000000..c835b102e
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/attributes/ViewAttrViewModel.kt
@@ -0,0 +1,76 @@
+package com.pluto.plugins.layoutinspector.internal.attributes
+
+import android.app.Application
+import android.view.View
+import android.view.ViewGroup
+import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.viewModelScope
+import com.pluto.plugins.layoutinspector.internal.attributes.data.Attribute
+import com.pluto.plugins.layoutinspector.internal.attributes.data.MutableAttribute
+import com.pluto.plugins.layoutinspector.internal.attributes.data.parser.AttributeParser
+import com.pluto.plugins.layoutinspector.internal.attributes.list.AttributeTitle
+import com.pluto.plugins.layoutinspector.internal.inspect.getIdString
+import com.pluto.utilities.list.ListItem
+import kotlinx.coroutines.launch
+
+internal class ViewAttrViewModel(application: Application) : AndroidViewModel(application) {
+
+ private val parser = AttributeParser()
+
+ val list: LiveData>
+ get() = _list
+ private val _list = MutableLiveData>()
+
+ val shareableAttr: LiveData
+ get() = _shareableAttr
+ private val _shareableAttr = MutableLiveData()
+
+ fun parse(v: View) {
+ viewModelScope.launch {
+ val attrList = generateAttributes(v)
+ _list.postValue(attrList)
+
+ val shareableAttr = generateAttributeShareable(attrList)
+ _shareableAttr.postValue(shareableAttr)
+ }
+ }
+
+ private fun generateAttributeShareable(attrList: ArrayList): String {
+ val text = StringBuilder()
+ text.append("View Attributes")
+ attrList.forEach { attr ->
+ when (attr) {
+ is AttributeTitle -> text.append("\n\n*** attributes from: ${attr.title}")
+ is Attribute<*> -> text.append("\n\t${attr.title}: ${attr.value}")
+ }
+ }
+ return text.toString()
+ }
+
+ private fun generateAttributes(v: View): ArrayList {
+ val attrList = arrayListOf()
+ v.getIdString()?.let {
+ attrList.add(Attribute("id", it))
+ }
+ val tempAttrList = arrayListOf>(
+ Attribute("view_type", if (v is ViewGroup) "viewGroup" else "view"),
+ Attribute("view_class", v.javaClass.canonicalName)
+ )
+ attrList.addAll(tempAttrList.sortedBy { it.title })
+ parser.parse(v).forEach { attr ->
+ attrList.add(AttributeTitle(attr.parameterizedType))
+ attrList.addAll(attr.attributes)
+ }
+ return attrList
+ }
+
+ fun updateAttributeValue(view: View, it: MutableAttribute, value: String) {
+ viewModelScope.launch {
+ it.handleEdit(view, value)
+ view.requestLayout()
+ view.post { parse(view) }
+ }
+ }
+}
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/attributes/data/Attribute.kt b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/attributes/data/Attribute.kt
new file mode 100644
index 000000000..c861a7bc2
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/attributes/data/Attribute.kt
@@ -0,0 +1,14 @@
+package com.pluto.plugins.layoutinspector.internal.attributes.data
+
+import com.pluto.utilities.list.ListItem
+import com.pluto.utilities.views.keyvalue.KeyValuePairEditMetaData
+
+internal open class Attribute(val title: String, val value: T) :
+ ListItem(),
+ KeyValuePairEditMetaData {
+ open fun displayText(): CharSequence? = if (value is CharSequence) value else value?.toString()
+
+ override fun isSame(other: Any): Boolean {
+ return other is Attribute<*> && other.title == title
+ }
+}
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/attributes/data/MutableAttribute.kt b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/attributes/data/MutableAttribute.kt
new file mode 100644
index 000000000..1ba498f63
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/attributes/data/MutableAttribute.kt
@@ -0,0 +1,9 @@
+package com.pluto.plugins.layoutinspector.internal.attributes.data
+
+import android.view.View
+import com.pluto.utilities.views.keyvalue.KeyValuePairEditRequest
+
+interface MutableAttribute {
+ fun requestEdit(): KeyValuePairEditRequest
+ fun handleEdit(view: View, updatedValue: String)
+}
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/attributes/data/mutability/AttributeAlpha.kt b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/attributes/data/mutability/AttributeAlpha.kt
new file mode 100644
index 000000000..4897be559
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/attributes/data/mutability/AttributeAlpha.kt
@@ -0,0 +1,22 @@
+package com.pluto.plugins.layoutinspector.internal.attributes.data.mutability
+
+import android.util.Log
+import android.view.View
+import com.pluto.plugins.layoutinspector.internal.attributes.data.Attribute
+import com.pluto.plugins.layoutinspector.internal.attributes.data.MutableAttribute
+import com.pluto.utilities.views.keyvalue.KeyValuePairEditRequest
+
+internal class AttributeAlpha(title: String, value: Float) : Attribute(title, value), MutableAttribute {
+
+ override fun requestEdit(): KeyValuePairEditRequest = KeyValuePairEditRequest(
+ key = "$title (0 to 1)", value = value.toString(), hint = "enter value (0.0 - 1.0)", metaData = this
+ )
+
+ override fun handleEdit(view: View, updatedValue: String) {
+ if (updatedValue.toFloat() in 0f..1f) {
+ view.alpha = updatedValue.toFloat()
+ } else {
+ Log.e("layout-inspector", "improper alpha value, should be between 0f to 1f")
+ }
+ }
+}
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/attributes/data/mutability/AttributeColor.kt b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/attributes/data/mutability/AttributeColor.kt
new file mode 100644
index 000000000..21c334d2a
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/attributes/data/mutability/AttributeColor.kt
@@ -0,0 +1,40 @@
+package com.pluto.plugins.layoutinspector.internal.attributes.data.mutability
+
+import android.graphics.Color
+import android.view.View
+import android.widget.TextView
+import com.pluto.plugins.layoutinspector.internal.attributes.data.Attribute
+import com.pluto.plugins.layoutinspector.internal.attributes.data.MutableAttribute
+import com.pluto.utilities.views.keyvalue.KeyValuePairEditRequest
+
+internal abstract class AttributeColor(title: String, value: Int) : Attribute(title, value), MutableAttribute {
+ override fun displayText(): CharSequence? = formatColor(value)
+
+ override fun requestEdit(): KeyValuePairEditRequest = KeyValuePairEditRequest(
+ key = title, value = formatColor(value), hint = "enter value", metaData = this
+ )
+
+ class Text(title: String, value: Int) : AttributeColor(title, value) {
+ override fun handleEdit(view: View, updatedValue: String) {
+ if (view is TextView) {
+ view.setTextColor(Color.parseColor(updatedValue))
+ }
+ }
+ }
+
+ class Hint(title: String, value: Int) : AttributeColor(title, value) {
+ override fun handleEdit(view: View, updatedValue: String) {
+ if (view is TextView) {
+ view.setHintTextColor(Color.parseColor(updatedValue))
+ }
+ }
+ }
+
+ class Background(title: String, value: Int) : AttributeColor(title, value) {
+ override fun handleEdit(view: View, updatedValue: String) {
+ view.setBackgroundColor(Color.parseColor(updatedValue))
+ }
+ }
+
+ private fun formatColor(value: Int): String = "#${Integer.toHexString(value).uppercase()}"
+}
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/attributes/data/mutability/AttributeDimenDP.kt b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/attributes/data/mutability/AttributeDimenDP.kt
new file mode 100644
index 000000000..f4f9ce23b
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/attributes/data/mutability/AttributeDimenDP.kt
@@ -0,0 +1,76 @@
+package com.pluto.plugins.layoutinspector.internal.attributes.data.mutability
+
+import android.view.View
+import android.view.ViewGroup
+import com.pluto.plugins.layoutinspector.internal.attributes.data.Attribute
+import com.pluto.plugins.layoutinspector.internal.attributes.data.MutableAttribute
+import com.pluto.utilities.extensions.dp2px
+import com.pluto.utilities.extensions.px2dp
+import com.pluto.utilities.views.keyvalue.KeyValuePairEditInputType
+import com.pluto.utilities.views.keyvalue.KeyValuePairEditRequest
+
+internal abstract class AttributeDimenDP(title: String, value: Float) : Attribute(title, value), MutableAttribute {
+ override fun displayText(): CharSequence? = "${value.px2dp.toInt()} dp"
+
+ override fun requestEdit(): KeyValuePairEditRequest = KeyValuePairEditRequest(
+ key = "$title (dp)", value = "${value.px2dp.toInt()}", hint = "enter value (in dp)", inputType = KeyValuePairEditInputType.Integer, metaData = this
+ )
+
+ /* padding */
+ class PaddingStart(title: String, value: Float) : AttributeDimenDP(title, value) {
+ override fun handleEdit(view: View, updatedValue: String) {
+ view.setPaddingRelative(
+ updatedValue.toFloat().dp2px.toInt(), view.paddingTop, view.paddingEnd, view.paddingBottom
+ )
+ }
+ }
+
+ class PaddingEnd(title: String, value: Float) : AttributeDimenDP(title, value) {
+ override fun handleEdit(view: View, updatedValue: String) {
+ view.setPaddingRelative(
+ view.paddingStart, view.paddingTop, updatedValue.toFloat().dp2px.toInt(), view.paddingBottom
+ )
+ }
+ }
+
+ class PaddingTop(title: String, value: Float) : AttributeDimenDP(title, value) {
+ override fun handleEdit(view: View, updatedValue: String) {
+ view.setPaddingRelative(
+ view.paddingStart, updatedValue.toFloat().dp2px.toInt(), view.paddingEnd, view.paddingBottom
+ )
+ }
+ }
+
+ class PaddingBottom(title: String, value: Float) : AttributeDimenDP(title, value) {
+ override fun handleEdit(view: View, updatedValue: String) {
+ view.setPaddingRelative(
+ view.paddingStart, view.paddingTop, view.paddingEnd, updatedValue.toFloat().dp2px.toInt(),
+ )
+ }
+ }
+
+ /* margin */
+ class MarginStart(title: String, value: Float) : AttributeDimenDP(title, value) {
+ override fun handleEdit(view: View, updatedValue: String) {
+ (view.layoutParams as ViewGroup.MarginLayoutParams).marginStart = updatedValue.toFloat().dp2px.toInt()
+ }
+ }
+
+ class MarginEnd(title: String, value: Float) : AttributeDimenDP(title, value) {
+ override fun handleEdit(view: View, updatedValue: String) {
+ (view.layoutParams as ViewGroup.MarginLayoutParams).marginEnd = updatedValue.toFloat().dp2px.toInt()
+ }
+ }
+
+ class MarginTop(title: String, value: Float) : AttributeDimenDP(title, value) {
+ override fun handleEdit(view: View, updatedValue: String) {
+ (view.layoutParams as ViewGroup.MarginLayoutParams).topMargin = updatedValue.toFloat().dp2px.toInt()
+ }
+ }
+
+ class MarginBottom(title: String, value: Float) : AttributeDimenDP(title, value) {
+ override fun handleEdit(view: View, updatedValue: String) {
+ (view.layoutParams as ViewGroup.MarginLayoutParams).bottomMargin = updatedValue.toFloat().dp2px.toInt()
+ }
+ }
+}
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/attributes/data/mutability/AttributeDimenSP.kt b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/attributes/data/mutability/AttributeDimenSP.kt
new file mode 100644
index 000000000..e00584248
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/attributes/data/mutability/AttributeDimenSP.kt
@@ -0,0 +1,24 @@
+package com.pluto.plugins.layoutinspector.internal.attributes.data.mutability
+
+import android.view.View
+import android.widget.TextView
+import com.pluto.plugins.layoutinspector.internal.attributes.data.Attribute
+import com.pluto.plugins.layoutinspector.internal.attributes.data.MutableAttribute
+import com.pluto.utilities.extensions.px2sp
+import com.pluto.utilities.views.keyvalue.KeyValuePairEditRequest
+
+internal abstract class AttributeDimenSP(title: String, value: Float) : Attribute(title, value), MutableAttribute {
+ override fun displayText(): CharSequence? = "${value.px2sp.toInt()} sp"
+
+ override fun requestEdit(): KeyValuePairEditRequest = KeyValuePairEditRequest(
+ key = "$title (sp)", value = value.px2sp.toInt().toString(), hint = "enter value (in sp)", metaData = this
+ )
+
+ class TextSize(title: String, value: Float) : AttributeDimenSP(title, value) {
+ override fun handleEdit(view: View, updatedValue: String) {
+ if (view is TextView) {
+ view.textSize = updatedValue.toFloat()
+ }
+ }
+ }
+}
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/attributes/data/mutability/AttributeGravity.kt b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/attributes/data/mutability/AttributeGravity.kt
new file mode 100644
index 000000000..4b6e31505
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/attributes/data/mutability/AttributeGravity.kt
@@ -0,0 +1,105 @@
+package com.pluto.plugins.layoutinspector.internal.attributes.data.mutability
+
+import android.view.Gravity
+import androidx.annotation.GravityInt
+import com.pluto.plugins.layoutinspector.internal.attributes.data.Attribute
+
+internal class AttributeGravity(title: String, value: Int) : Attribute(title, value) {
+ override fun displayText(): CharSequence? = formatGravity(value)
+
+// override fun requestEdit(): KeyValuePairEditRequest = KeyValuePairEditRequest(
+// key = title,
+// value = formatGravity(value),
+// metaData = this,
+// inputType = KeyValuePairEditInputType.Selection,
+// candidateOptions = listOf(
+// LABEL_NO_GRAVITY,
+// LABEL_LEFT,
+// LABEL_TOP,
+// LABEL_RIGHT,
+// LABEL_BOTTOM,
+// LABEL_CENTER,
+// LABEL_START,
+// LABEL_END,
+// LABEL_CENTER_HORIZONTAL,
+// LABEL_CENTER_VERTICAL,
+// LABEL_CLIP_HORIZONTAL,
+// LABEL_CLIP_VERTICAL,
+// LABEL_FILL_HORIZONTAL,
+// LABEL_FILL_VERTICAL,
+// LABEL_FILL,
+// LABEL_NOT_SET
+// )
+// )
+//
+// override fun handleEdit(view: View, updatedValue: String) {
+// var gravity: Int? = null
+// when (updatedValue) {
+// LABEL_NO_GRAVITY -> "NO_GRAVITY"
+// LABEL_LEFT -> "LEFT"
+// LABEL_TOP -> "TOP"
+// LABEL_RIGHT -> "RIGHT"
+// LABEL_BOTTOM -> "BOTTOM"
+// LABEL_CENTER -> "CENTER"
+// LABEL_CENTER_HORIZONTAL -> "CENTER_HORIZONTAL"
+// LABEL_CENTER_VERTICAL -> "CENTER_VERTICAL"
+// LABEL_START -> "START"
+// LABEL_END -> "END"
+// LABEL_CLIP_HORIZONTAL -> "CLIP_HORIZONTAL"
+// LABEL_CLIP_VERTICAL -> "CLIP_VERTICAL"
+// LABEL_FILL -> "FILL"
+// LABEL_FILL_HORIZONTAL -> "FILL_HORIZONTAL"
+// LABEL_FILL_VERTICAL -> "FILL_VERTICAL"
+// else -> "NOT SET"
+// }
+//
+// if(view is TextView) {
+// Log.d("prateek")
+// view.gravity = Gravity.LEFT
+// }
+// if (scaleType != null) {
+// (view as ImageView).scaleType = scaleType
+// } else {
+// Log.e("layout-inspector", "improper scale type value, should be between 0f to 1f")
+// }
+// }
+
+ @SuppressWarnings("ComplexMethod")
+ private fun formatGravity(@GravityInt gravity: Int): String = when (gravity) {
+ Gravity.NO_GRAVITY -> LABEL_NO_GRAVITY
+ Gravity.LEFT -> LABEL_LEFT
+ Gravity.TOP -> LABEL_TOP
+ Gravity.RIGHT -> LABEL_RIGHT
+ Gravity.BOTTOM -> LABEL_BOTTOM
+ Gravity.CENTER -> LABEL_CENTER
+ Gravity.CENTER_HORIZONTAL -> LABEL_CENTER_HORIZONTAL
+ Gravity.CENTER_VERTICAL -> LABEL_CENTER_VERTICAL
+ Gravity.START -> LABEL_START
+ Gravity.END -> LABEL_END
+ Gravity.CLIP_HORIZONTAL -> LABEL_CLIP_HORIZONTAL
+ Gravity.CLIP_VERTICAL -> LABEL_CLIP_VERTICAL
+ Gravity.FILL -> LABEL_FILL
+ Gravity.FILL_HORIZONTAL -> LABEL_FILL_HORIZONTAL
+ Gravity.FILL_VERTICAL -> LABEL_FILL_VERTICAL
+ else -> LABEL_NOT_SET
+ }
+
+ private companion object {
+ const val LABEL_NO_GRAVITY = "NO_GRAVITY"
+ const val LABEL_LEFT = "LEFT"
+ const val LABEL_TOP = "TOP"
+ const val LABEL_RIGHT = "RIGHT"
+ const val LABEL_BOTTOM = "BOTTOM"
+ const val LABEL_CENTER = "CENTER"
+ const val LABEL_CENTER_HORIZONTAL = "CENTER_HORIZONTAL"
+ const val LABEL_CENTER_VERTICAL = "CENTER_VERTICAL"
+ const val LABEL_START = "START"
+ const val LABEL_END = "END"
+ const val LABEL_CLIP_HORIZONTAL = "CLIP_HORIZONTAL"
+ const val LABEL_CLIP_VERTICAL = "CLIP_VERTICAL"
+ const val LABEL_FILL = "FILL"
+ const val LABEL_FILL_HORIZONTAL = "FILL_HORIZONTAL"
+ const val LABEL_FILL_VERTICAL = "FILL_VERTICAL"
+ const val LABEL_NOT_SET = "NOT SET"
+ }
+}
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/attributes/data/mutability/AttributeLayoutParam.kt b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/attributes/data/mutability/AttributeLayoutParam.kt
new file mode 100644
index 000000000..8c8d70317
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/attributes/data/mutability/AttributeLayoutParam.kt
@@ -0,0 +1,74 @@
+package com.pluto.plugins.layoutinspector.internal.attributes.data.mutability
+
+import android.view.View
+import android.view.ViewGroup
+import com.pluto.plugins.layoutinspector.internal.attributes.data.Attribute
+import com.pluto.plugins.layoutinspector.internal.attributes.data.MutableAttribute
+import com.pluto.utilities.extensions.dp2px
+import com.pluto.utilities.extensions.px2dp
+import com.pluto.utilities.views.keyvalue.KeyValuePairEditInputType
+import com.pluto.utilities.views.keyvalue.KeyValuePairEditRequest
+
+internal abstract class AttributeLayoutParam(title: String, value: Data) : Attribute(title, value), MutableAttribute {
+ override fun displayText(): CharSequence? = value.toDisplayText()
+ override fun requestEdit(): KeyValuePairEditRequest = KeyValuePairEditRequest(
+ key = "$title (dp)",
+ value = if (value.actualValue() != LABEL_WRAP_CONTENT && value.actualValue() != LABEL_MATCH_PARENT) {
+ value.actualValue()
+ } else {
+ null
+ },
+ hint = "enter value (in dp)",
+ metaData = this,
+ inputType = KeyValuePairEditInputType.Integer,
+ candidateOptions = listOf(
+ LABEL_WRAP_CONTENT,
+ LABEL_MATCH_PARENT
+ )
+ )
+
+ class Height(title: String, value: Data) : AttributeLayoutParam(title, value) {
+ override fun handleEdit(view: View, updatedValue: String) {
+ view.layoutParams.height = when (updatedValue) {
+ LABEL_WRAP_CONTENT -> ViewGroup.LayoutParams.WRAP_CONTENT
+ LABEL_MATCH_PARENT -> ViewGroup.LayoutParams.MATCH_PARENT
+ else -> updatedValue.toFloat().dp2px.toInt()
+ }
+ }
+ }
+
+ class Width(title: String, value: Data) : AttributeLayoutParam(title, value) {
+ override fun handleEdit(view: View, updatedValue: String) {
+ view.layoutParams.width = when (updatedValue) {
+ LABEL_WRAP_CONTENT -> ViewGroup.LayoutParams.WRAP_CONTENT
+ LABEL_MATCH_PARENT -> ViewGroup.LayoutParams.MATCH_PARENT
+ else -> updatedValue.toFloat().dp2px.toInt()
+ }
+ }
+ }
+
+ private companion object {
+ const val LABEL_WRAP_CONTENT = "wrap_content"
+ const val LABEL_MATCH_PARENT = "match_parent"
+ }
+
+ internal data class Data(
+ val layoutParam: Int,
+ val size: Int
+ ) {
+ fun toDisplayText(): CharSequence {
+ val dp = "${size.toFloat().px2dp.toInt()} dp"
+ return when (layoutParam) {
+ ViewGroup.LayoutParams.WRAP_CONTENT -> "$LABEL_WRAP_CONTENT ($dp)"
+ ViewGroup.LayoutParams.MATCH_PARENT -> "$LABEL_MATCH_PARENT ($dp)"
+ else -> dp
+ }
+ }
+
+ fun actualValue(): String = when (layoutParam) {
+ ViewGroup.LayoutParams.WRAP_CONTENT -> LABEL_WRAP_CONTENT
+ ViewGroup.LayoutParams.MATCH_PARENT -> LABEL_MATCH_PARENT
+ else -> "${size.toFloat().px2dp.toInt()}"
+ }
+ }
+}
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/attributes/data/mutability/AttributeScaleType.kt b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/attributes/data/mutability/AttributeScaleType.kt
new file mode 100644
index 000000000..825fd69e4
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/attributes/data/mutability/AttributeScaleType.kt
@@ -0,0 +1,60 @@
+package com.pluto.plugins.layoutinspector.internal.attributes.data.mutability
+
+import android.util.Log
+import android.view.View
+import android.widget.ImageView
+import com.pluto.plugins.layoutinspector.internal.attributes.data.Attribute
+import com.pluto.plugins.layoutinspector.internal.attributes.data.MutableAttribute
+import com.pluto.utilities.views.keyvalue.KeyValuePairEditInputType
+import com.pluto.utilities.views.keyvalue.KeyValuePairEditRequest
+
+internal class AttributeScaleType(title: String, value: ImageView.ScaleType) : Attribute(title, value), MutableAttribute {
+ override fun displayText(): CharSequence? = value.name
+
+ override fun requestEdit(): KeyValuePairEditRequest = KeyValuePairEditRequest(
+ key = title,
+ value = value.name,
+ metaData = this,
+ inputType = KeyValuePairEditInputType.Selection,
+ candidateOptions = listOf(
+ ImageView.ScaleType.CENTER_INSIDE.name,
+ ImageView.ScaleType.CENTER_CROP.name,
+ ImageView.ScaleType.CENTER.name,
+ ImageView.ScaleType.FIT_CENTER.name,
+ ImageView.ScaleType.FIT_END.name,
+ ImageView.ScaleType.FIT_START.name,
+ ImageView.ScaleType.FIT_XY.name,
+ ImageView.ScaleType.MATRIX.name
+ )
+ )
+
+ override fun handleEdit(view: View, updatedValue: String) {
+ var scaleType: ImageView.ScaleType? = null
+ when (updatedValue) {
+ LABEL_CENTER_INSIDE -> scaleType = ImageView.ScaleType.CENTER_INSIDE
+ LABEL_CENTER_CROP -> scaleType = ImageView.ScaleType.CENTER_CROP
+ LABEL_CENTER -> scaleType = ImageView.ScaleType.CENTER
+ LABEL_FIT_CENTER -> scaleType = ImageView.ScaleType.FIT_CENTER
+ LABEL_FIT_END -> scaleType = ImageView.ScaleType.FIT_END
+ LABEL_FIT_START -> scaleType = ImageView.ScaleType.FIT_START
+ LABEL_FIT_XY -> scaleType = ImageView.ScaleType.FIT_XY
+ LABEL_MATRIX -> scaleType = ImageView.ScaleType.MATRIX
+ }
+ if (scaleType != null) {
+ (view as ImageView).scaleType = scaleType
+ } else {
+ Log.e("layout-inspector", "improper scale type value, should be between 0f to 1f")
+ }
+ }
+
+ private companion object {
+ const val LABEL_CENTER_INSIDE = "CENTER_INSIDE"
+ const val LABEL_CENTER_CROP = "CENTER_CROP"
+ const val LABEL_CENTER = "CENTER"
+ const val LABEL_FIT_CENTER = "FIT_CENTER"
+ const val LABEL_FIT_END = "FIT_END"
+ const val LABEL_FIT_START = "FIT_START"
+ const val LABEL_FIT_XY = "FIT_XY"
+ const val LABEL_MATRIX = "MATRIX"
+ }
+}
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/attributes/data/mutability/AttributeText.kt b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/attributes/data/mutability/AttributeText.kt
new file mode 100644
index 000000000..0be45de36
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/attributes/data/mutability/AttributeText.kt
@@ -0,0 +1,30 @@
+package com.pluto.plugins.layoutinspector.internal.attributes.data.mutability
+
+import android.view.View
+import android.widget.TextView
+import com.pluto.plugins.layoutinspector.internal.attributes.data.Attribute
+import com.pluto.plugins.layoutinspector.internal.attributes.data.MutableAttribute
+import com.pluto.utilities.views.keyvalue.KeyValuePairEditRequest
+
+internal abstract class AttributeText(title: String, value: CharSequence?) : Attribute(title, value), MutableAttribute {
+
+ override fun requestEdit(): KeyValuePairEditRequest = KeyValuePairEditRequest(
+ key = title, value = value?.toString(), hint = "enter value", metaData = this
+ )
+
+ class Text(title: String, value: CharSequence?) : AttributeText(title, value) {
+ override fun handleEdit(view: View, updatedValue: String) {
+ if (view is TextView) {
+ view.text = updatedValue
+ }
+ }
+ }
+
+ class Hint(title: String, value: CharSequence?) : AttributeText(title, value) {
+ override fun handleEdit(view: View, updatedValue: String) {
+ if (view is TextView) {
+ view.hint = updatedValue
+ }
+ }
+ }
+}
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/attributes/data/mutability/AttributeVisibility.kt b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/attributes/data/mutability/AttributeVisibility.kt
new file mode 100644
index 000000000..a9ef0a7f5
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/attributes/data/mutability/AttributeVisibility.kt
@@ -0,0 +1,45 @@
+package com.pluto.plugins.layoutinspector.internal.attributes.data.mutability
+
+import android.view.View
+import com.pluto.plugins.layoutinspector.internal.attributes.data.Attribute
+import com.pluto.plugins.layoutinspector.internal.attributes.data.MutableAttribute
+import com.pluto.utilities.views.keyvalue.KeyValuePairEditInputType
+import com.pluto.utilities.views.keyvalue.KeyValuePairEditRequest
+
+internal class AttributeVisibility(title: String, value: Int) : Attribute(title, value), MutableAttribute {
+ override fun displayText(): CharSequence? = formatVisibility(value)
+
+ override fun requestEdit(): KeyValuePairEditRequest = KeyValuePairEditRequest(
+ key = title,
+ value = formatVisibility(value),
+ metaData = this,
+ inputType = KeyValuePairEditInputType.Selection,
+ candidateOptions = listOf(
+ formatVisibility(View.VISIBLE),
+ formatVisibility(View.INVISIBLE),
+ formatVisibility(View.GONE)
+ )
+ )
+
+ override fun handleEdit(view: View, updatedValue: String) {
+ view.visibility = when (updatedValue) {
+ LABEL_VISIBLE -> View.VISIBLE
+ LABEL_INVISIBLE -> View.INVISIBLE
+ LABEL_GONE -> View.GONE
+ else -> View.VISIBLE
+ }
+ }
+
+ private fun formatVisibility(value: Int): String = when (value) {
+ View.VISIBLE -> LABEL_VISIBLE
+ View.INVISIBLE -> LABEL_INVISIBLE
+ View.GONE -> LABEL_GONE
+ else -> "NOT SET"
+ }
+
+ private companion object {
+ const val LABEL_VISIBLE = "VISIBLE"
+ const val LABEL_INVISIBLE = "INVISIBLE"
+ const val LABEL_GONE = "GONE"
+ }
+}
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/attributes/data/parser/AttributeParser.kt b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/attributes/data/parser/AttributeParser.kt
new file mode 100644
index 000000000..9283daece
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/attributes/data/parser/AttributeParser.kt
@@ -0,0 +1,33 @@
+package com.pluto.plugins.layoutinspector.internal.attributes.data.parser
+
+import android.view.View
+import com.pluto.plugins.layoutinspector.internal.attributes.data.Attribute
+import com.pluto.plugins.layoutinspector.internal.attributes.data.parser.types.ImageViewParser
+import com.pluto.plugins.layoutinspector.internal.attributes.data.parser.types.TextViewParser
+import com.pluto.plugins.layoutinspector.internal.attributes.data.parser.types.ViewGroupParser
+import com.pluto.plugins.layoutinspector.internal.attributes.data.parser.types.ViewParser
+
+internal class AttributeParser {
+ private val parsers = arrayListOf>().apply {
+ add(ImageViewParser())
+ add(TextViewParser())
+ add(ViewGroupParser())
+ add(ViewParser())
+ }
+
+ fun parse(view: View): List {
+ val attributes = arrayListOf()
+ parsers.forEach { parser ->
+ val attributeList = parser.getAttributes(view)
+ if (!attributeList.isNullOrEmpty()) {
+ attributes.add(ParsedAttribute(parser.parameterizedType, attributeList.sortedBy { it.title }))
+ }
+ }
+ return attributes
+ }
+
+ data class ParsedAttribute(
+ val parameterizedType: String,
+ val attributes: List>
+ )
+}
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/attributes/data/parser/IParser.kt b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/attributes/data/parser/IParser.kt
new file mode 100644
index 000000000..c9abe07d9
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/attributes/data/parser/IParser.kt
@@ -0,0 +1,32 @@
+package com.pluto.plugins.layoutinspector.internal.attributes.data.parser
+
+import android.view.View
+import com.pluto.plugins.layoutinspector.internal.attributes.data.Attribute
+import java.lang.reflect.ParameterizedType
+
+internal abstract class IParser {
+
+ private val parameterizedTypeClass = (javaClass.genericSuperclass as ParameterizedType).actualTypeArguments[0] as Class<*>
+ val parameterizedType: String = parameterizedTypeClass.toString().replace("class", "", true).trim()
+
+ private fun isValidType(viewClazz: Class<*>): Boolean {
+ var clazz: Class<*> = viewClazz
+ do {
+ if (parameterizedTypeClass === clazz) {
+ return true
+ }
+ clazz = clazz.superclass
+ } while (clazz != Any::class.java)
+ return false
+ }
+
+ @SuppressWarnings("TooGenericExceptionCaught")
+ fun getAttributes(view: View): List>? = try {
+ if (isValidType(view.javaClass)) getTypeAttributes(view) else null
+ } catch (t: Throwable) {
+ t.printStackTrace()
+ null
+ }
+
+ protected abstract fun getTypeAttributes(view: View): List>
+}
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/attributes/data/parser/types/ImageViewParser.kt b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/attributes/data/parser/types/ImageViewParser.kt
new file mode 100644
index 000000000..dec7e8cb7
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/attributes/data/parser/types/ImageViewParser.kt
@@ -0,0 +1,18 @@
+package com.pluto.plugins.layoutinspector.internal.attributes.data.parser.types
+
+import android.view.View
+import android.widget.ImageView
+import com.pluto.plugins.layoutinspector.internal.attributes.data.Attribute
+import com.pluto.plugins.layoutinspector.internal.attributes.data.mutability.AttributeScaleType
+import com.pluto.plugins.layoutinspector.internal.attributes.data.parser.IParser
+
+internal class ImageViewParser : IParser() {
+
+ override fun getTypeAttributes(view: View): List> {
+ val attributes = arrayListOf>()
+ (view as ImageView).apply {
+ attributes.add(AttributeScaleType("scale_type", scaleType))
+ }
+ return attributes
+ }
+}
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/attributes/data/parser/types/TextViewParser.kt b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/attributes/data/parser/types/TextViewParser.kt
new file mode 100644
index 000000000..e570c4a88
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/attributes/data/parser/types/TextViewParser.kt
@@ -0,0 +1,28 @@
+package com.pluto.plugins.layoutinspector.internal.attributes.data.parser.types
+
+import android.view.View
+import android.widget.TextView
+import com.pluto.plugins.layoutinspector.internal.attributes.data.Attribute
+import com.pluto.plugins.layoutinspector.internal.attributes.data.mutability.AttributeColor
+import com.pluto.plugins.layoutinspector.internal.attributes.data.mutability.AttributeDimenSP
+import com.pluto.plugins.layoutinspector.internal.attributes.data.mutability.AttributeGravity
+import com.pluto.plugins.layoutinspector.internal.attributes.data.mutability.AttributeText
+import com.pluto.plugins.layoutinspector.internal.attributes.data.parser.IParser
+
+internal class TextViewParser : IParser() {
+
+ override fun getTypeAttributes(view: View): List> {
+ val attributes = arrayListOf>()
+ (view as TextView).apply {
+ attributes.add(AttributeText.Text("text", text))
+ attributes.add(AttributeColor.Text("text_color", currentTextColor))
+ attributes.add(AttributeText.Hint("hint", hint))
+ attributes.add(AttributeColor.Hint("text_hint_color", currentHintTextColor))
+ attributes.add(AttributeDimenSP.TextSize("text_size", textSize))
+ attributes.add(AttributeGravity("gravity", gravity))
+ attributes.add(Attribute("line_count", lineCount))
+ attributes.add(Attribute("line_height", lineHeight.toFloat()))
+ }
+ return attributes
+ }
+}
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/attributes/data/parser/types/ViewGroupParser.kt b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/attributes/data/parser/types/ViewGroupParser.kt
new file mode 100644
index 000000000..f40e68430
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/attributes/data/parser/types/ViewGroupParser.kt
@@ -0,0 +1,18 @@
+package com.pluto.plugins.layoutinspector.internal.attributes.data.parser.types
+
+import android.view.View
+import android.view.ViewGroup
+import com.pluto.plugins.layoutinspector.internal.attributes.data.Attribute
+import com.pluto.plugins.layoutinspector.internal.attributes.data.parser.IParser
+
+internal class ViewGroupParser : IParser() {
+
+ override fun getTypeAttributes(view: View): List> {
+ val attributes = arrayListOf>()
+ (view as ViewGroup).apply {
+ attributes.add(Attribute("child_count", childCount))
+ attributes.add(Attribute("will_not_draw", willNotDraw()))
+ }
+ return attributes
+ }
+}
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/attributes/data/parser/types/ViewParser.kt b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/attributes/data/parser/types/ViewParser.kt
new file mode 100644
index 000000000..b1b2904de
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/attributes/data/parser/types/ViewParser.kt
@@ -0,0 +1,52 @@
+package com.pluto.plugins.layoutinspector.internal.attributes.data.parser.types
+
+import android.graphics.drawable.ColorDrawable
+import android.view.View
+import android.view.ViewGroup
+import android.view.ViewGroup.MarginLayoutParams
+import com.pluto.plugins.layoutinspector.internal.attributes.data.Attribute
+import com.pluto.plugins.layoutinspector.internal.attributes.data.mutability.AttributeAlpha
+import com.pluto.plugins.layoutinspector.internal.attributes.data.mutability.AttributeColor
+import com.pluto.plugins.layoutinspector.internal.attributes.data.mutability.AttributeDimenDP
+import com.pluto.plugins.layoutinspector.internal.attributes.data.mutability.AttributeLayoutParam
+import com.pluto.plugins.layoutinspector.internal.attributes.data.mutability.AttributeVisibility
+import com.pluto.plugins.layoutinspector.internal.attributes.data.parser.IParser
+
+internal class ViewParser : IParser() {
+
+ override fun getTypeAttributes(view: View): List> {
+ val attributes = arrayListOf>()
+ val params: ViewGroup.LayoutParams = view.layoutParams
+ attributes.add(Attribute("layoutParams", params.javaClass.name))
+ attributes.add(AttributeLayoutParam.Width("layout_width", AttributeLayoutParam.Data(params.width, view.width)))
+ attributes.add(AttributeLayoutParam.Height("layout_height", AttributeLayoutParam.Data(params.height, view.height)))
+ attributes.add(AttributeVisibility("visibility", view.visibility))
+ attributes.add(AttributeDimenDP.PaddingStart("padding_start", view.paddingStart.toFloat()))
+ attributes.add(AttributeDimenDP.PaddingTop("padding_top", view.paddingTop.toFloat()))
+ attributes.add(AttributeDimenDP.PaddingEnd("padding_end", view.paddingEnd.toFloat()))
+ attributes.add(AttributeDimenDP.PaddingBottom("padding_bottom", view.paddingBottom.toFloat()))
+ if (view.layoutParams != null && view.layoutParams is MarginLayoutParams) {
+ val marginLayoutParams: MarginLayoutParams = view.layoutParams as MarginLayoutParams
+ attributes.add(AttributeDimenDP.MarginStart("margin_start", marginLayoutParams.marginStart.toFloat()))
+ attributes.add(AttributeDimenDP.MarginTop("margin_top", marginLayoutParams.topMargin.toFloat()))
+ attributes.add(AttributeDimenDP.MarginEnd("margin_end", marginLayoutParams.marginEnd.toFloat()))
+ attributes.add(AttributeDimenDP.MarginBottom("margin_bottom", marginLayoutParams.bottomMargin.toFloat()))
+ }
+ attributes.add(Attribute("translationX", view.translationX))
+ attributes.add(Attribute("translationY", view.translationY))
+ attributes.add(
+ when (view.background) {
+ is ColorDrawable -> AttributeColor.Background("background", (view.background as ColorDrawable).color)
+ else -> Attribute("background", view.background)
+ }
+ )
+ attributes.add(AttributeAlpha("alpha", view.alpha))
+ attributes.add(Attribute("tag", view.tag))
+ attributes.add(Attribute("enabled", view.isEnabled))
+ attributes.add(Attribute("clickable", view.isClickable))
+ attributes.add(Attribute("long_clickable", view.isLongClickable))
+ attributes.add(Attribute("focusable", view.isFocusable))
+ attributes.add(Attribute("content_dscrptn", view.contentDescription))
+ return attributes
+ }
+}
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/attributes/list/AttributeAdapter.kt b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/attributes/list/AttributeAdapter.kt
new file mode 100644
index 000000000..a1bd85813
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/attributes/list/AttributeAdapter.kt
@@ -0,0 +1,30 @@
+package com.pluto.plugins.layoutinspector.internal.attributes.list
+
+import android.view.ViewGroup
+import com.pluto.plugins.layoutinspector.internal.attributes.data.Attribute
+import com.pluto.utilities.list.BaseAdapter
+import com.pluto.utilities.list.DiffAwareHolder
+import com.pluto.utilities.list.ListItem
+
+internal class AttributeAdapter(private val listener: OnActionListener) : BaseAdapter() {
+ override fun getItemViewType(item: ListItem): Int? {
+ return when (item) {
+ is Attribute<*> -> ITEM_TYPE_ATTRIBUTE
+ is AttributeTitle -> ITEM_TYPE_TITLE
+ else -> null
+ }
+ }
+
+ override fun onViewHolderCreated(parent: ViewGroup, viewType: Int): DiffAwareHolder? {
+ return when (viewType) {
+ ITEM_TYPE_ATTRIBUTE -> AttributeItemHolder(parent, listener)
+ ITEM_TYPE_TITLE -> AttributeTitleItemHolder(parent, listener)
+ else -> null
+ }
+ }
+
+ companion object {
+ const val ITEM_TYPE_ATTRIBUTE = 1000
+ const val ITEM_TYPE_TITLE = 1001
+ }
+}
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/attributes/list/AttributeItemHolder.kt b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/attributes/list/AttributeItemHolder.kt
new file mode 100644
index 000000000..fdf17b68f
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/attributes/list/AttributeItemHolder.kt
@@ -0,0 +1,37 @@
+package com.pluto.plugins.layoutinspector.internal.attributes.list
+
+import android.view.ViewGroup
+import com.pluto.plugins.layoutinspector.R
+import com.pluto.plugins.layoutinspector.databinding.PlutoLiItemViewAttrBinding
+import com.pluto.plugins.layoutinspector.internal.attributes.data.Attribute
+import com.pluto.plugins.layoutinspector.internal.attributes.data.MutableAttribute
+import com.pluto.utilities.extensions.inflate
+import com.pluto.utilities.list.DiffAwareAdapter
+import com.pluto.utilities.list.DiffAwareHolder
+import com.pluto.utilities.list.ListItem
+import com.pluto.utilities.views.keyvalue.KeyValuePairData
+
+internal class AttributeItemHolder(parent: ViewGroup, actionListener: DiffAwareAdapter.OnActionListener) :
+ DiffAwareHolder(parent.inflate(R.layout.pluto_li___item_view_attr), actionListener) {
+
+ private val binding = PlutoLiItemViewAttrBinding.bind(itemView)
+
+ override fun onBind(item: ListItem) {
+ if (item is Attribute<*>) {
+ binding.content.set(
+ KeyValuePairData(
+ key = item.title,
+ value = item.displayText(),
+ showClickIndicator = item is MutableAttribute,
+ onClick = getAction(item)
+ )
+ )
+ }
+ }
+
+ private fun getAction(item: Attribute<*>): (() -> Unit)? = if (item is MutableAttribute) {
+ { onAction("click") }
+ } else {
+ null
+ }
+}
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/attributes/list/AttributeTitleItemHolder.kt b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/attributes/list/AttributeTitleItemHolder.kt
new file mode 100644
index 000000000..010c648cc
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/attributes/list/AttributeTitleItemHolder.kt
@@ -0,0 +1,25 @@
+package com.pluto.plugins.layoutinspector.internal.attributes.list
+
+import android.view.ViewGroup
+import androidx.annotation.Keep
+import com.pluto.plugins.layoutinspector.R
+import com.pluto.plugins.layoutinspector.databinding.PlutoLiItemViewAttrTitleBinding
+import com.pluto.utilities.extensions.inflate
+import com.pluto.utilities.list.DiffAwareAdapter
+import com.pluto.utilities.list.DiffAwareHolder
+import com.pluto.utilities.list.ListItem
+
+internal class AttributeTitleItemHolder(parent: ViewGroup, actionListener: DiffAwareAdapter.OnActionListener) :
+ DiffAwareHolder(parent.inflate(R.layout.pluto_li___item_view_attr_title), actionListener) {
+
+ private val binding = PlutoLiItemViewAttrTitleBinding.bind(itemView)
+
+ override fun onBind(item: ListItem) {
+ if (item is AttributeTitle) {
+ binding.title.text = item.title
+ }
+ }
+}
+
+@Keep
+internal data class AttributeTitle(val title: String?) : ListItem()
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/control/ControlCta.kt b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/control/ControlCta.kt
new file mode 100644
index 000000000..21a3c3d80
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/control/ControlCta.kt
@@ -0,0 +1,10 @@
+package com.pluto.plugins.layoutinspector.internal.control
+
+import androidx.annotation.DrawableRes
+import com.pluto.utilities.list.ListItem
+
+internal data class ControlCta(
+ val id: String,
+ @DrawableRes val icon: Int,
+ val hint: String
+) : ListItem()
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/control/ControlCtaAdapter.kt b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/control/ControlCtaAdapter.kt
new file mode 100644
index 000000000..30d5c2ef5
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/control/ControlCtaAdapter.kt
@@ -0,0 +1,26 @@
+package com.pluto.plugins.layoutinspector.internal.control
+
+import android.view.ViewGroup
+import com.pluto.utilities.list.BaseAdapter
+import com.pluto.utilities.list.DiffAwareHolder
+import com.pluto.utilities.list.ListItem
+
+internal class ControlCtaAdapter(private val listener: OnActionListener) : BaseAdapter() {
+ override fun getItemViewType(item: ListItem): Int? {
+ return when (item) {
+ is ControlCta -> ITEM_TYPE_CONTROL_CTA
+ else -> null
+ }
+ }
+
+ override fun onViewHolderCreated(parent: ViewGroup, viewType: Int): DiffAwareHolder? {
+ return when (viewType) {
+ ITEM_TYPE_CONTROL_CTA -> ControlCtaItemHolder(parent, listener)
+ else -> null
+ }
+ }
+
+ companion object {
+ const val ITEM_TYPE_CONTROL_CTA = 1000
+ }
+}
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/control/ControlCtaItemHolder.kt b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/control/ControlCtaItemHolder.kt
new file mode 100644
index 000000000..0ab780ab7
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/control/ControlCtaItemHolder.kt
@@ -0,0 +1,30 @@
+package com.pluto.plugins.layoutinspector.internal.control
+
+import android.view.ViewGroup
+import com.pluto.plugins.layoutinspector.R
+import com.pluto.plugins.layoutinspector.databinding.PlutoLiItemControlCtaBinding
+import com.pluto.utilities.extensions.inflate
+import com.pluto.utilities.extensions.toast
+import com.pluto.utilities.list.DiffAwareAdapter
+import com.pluto.utilities.list.DiffAwareHolder
+import com.pluto.utilities.list.ListItem
+import com.pluto.utilities.setOnDebounceClickListener
+
+internal class ControlCtaItemHolder(parent: ViewGroup, actionListener: DiffAwareAdapter.OnActionListener) :
+ DiffAwareHolder(parent.inflate(R.layout.pluto_li___item_control_cta), actionListener) {
+
+ private val binding = PlutoLiItemControlCtaBinding.bind(itemView)
+
+ override fun onBind(item: ListItem) {
+ if (item is ControlCta) {
+ binding.icon.setImageResource(item.icon)
+ binding.root.setOnDebounceClickListener {
+ onAction(item.id)
+ }
+ binding.root.setOnLongClickListener {
+ context.toast(item.hint)
+ true
+ }
+ }
+ }
+}
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/control/ControlsWidget.kt b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/control/ControlsWidget.kt
new file mode 100644
index 000000000..46646a43d
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/control/ControlsWidget.kt
@@ -0,0 +1,51 @@
+package com.pluto.plugins.layoutinspector.internal.control
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.widget.LinearLayout
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.core.content.ContextCompat
+import androidx.recyclerview.widget.DividerItemDecoration
+import com.pluto.plugins.layoutinspector.R
+import com.pluto.plugins.layoutinspector.databinding.PlutoLiControlsWidgetBinding
+import com.pluto.utilities.list.BaseAdapter
+import com.pluto.utilities.list.DiffAwareAdapter
+import com.pluto.utilities.list.DiffAwareHolder
+import com.pluto.utilities.list.ListItem
+
+internal class ControlsWidget : ConstraintLayout {
+
+ private val binding = PlutoLiControlsWidgetBinding.inflate(LayoutInflater.from(context), this, true)
+ private val pluginAdapter: BaseAdapter by lazy { ControlCtaAdapter(onActionListener) }
+ private var mListener: OnClickListener? = null
+
+ constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle)
+ constructor(context: Context, attrs: AttributeSet) : super(context, attrs, 0)
+ constructor(context: Context) : super(context, null, 0)
+
+ fun initialise(ctas: List, listener: OnClickListener? = null) {
+ mListener = listener
+ binding.list.apply {
+ adapter = pluginAdapter
+ addItemDecoration(
+ DividerItemDecoration(context, LinearLayout.HORIZONTAL).apply {
+ setDrawable(ContextCompat.getDrawable(context, R.drawable.pluto_li___item_divider)!!)
+ }
+ )
+ }
+ pluginAdapter.list = ctas
+ }
+
+ private val onActionListener = object : DiffAwareAdapter.OnActionListener {
+ override fun onAction(action: String, data: ListItem, holder: DiffAwareHolder) {
+ if (data is ControlCta) {
+ mListener?.onClick(data.id)
+ }
+ }
+ }
+
+ interface OnClickListener {
+ fun onClick(id: String)
+ }
+}
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/hierarchy/Hierarchy.kt b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/hierarchy/Hierarchy.kt
new file mode 100644
index 000000000..a9f28a57a
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/hierarchy/Hierarchy.kt
@@ -0,0 +1,34 @@
+package com.pluto.plugins.layoutinspector.internal.hierarchy
+
+import android.view.View
+import android.view.ViewGroup
+import androidx.core.view.children
+import com.pluto.plugins.layoutinspector.internal.inspect.verifyTargetTag
+import com.pluto.utilities.list.ListItem
+
+class Hierarchy(
+ val view: View,
+ val layerCount: Int,
+ var isExpanded: Boolean = false
+) : ListItem() {
+ override fun isEqual(other: Any): Boolean {
+ return other is Hierarchy && other.view.javaClass.name == view.javaClass.name && other.isExpanded == isExpanded
+ }
+
+ val isTargetView: Boolean
+ get() = view.verifyTargetTag()
+
+ fun assembleChildren(recursive: Boolean = false): List {
+ val result = arrayListOf()
+ if (view is ViewGroup) {
+ view.children.forEach {
+ val item = Hierarchy(it, layerCount + 1, isExpanded = recursive)
+ result.add(item)
+ if (recursive) {
+ result.addAll(item.assembleChildren(true))
+ }
+ }
+ }
+ return result
+ }
+}
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/hierarchy/NestedView.kt b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/hierarchy/NestedView.kt
new file mode 100644
index 000000000..281191463
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/hierarchy/NestedView.kt
@@ -0,0 +1,58 @@
+package com.pluto.plugins.layoutinspector.internal.hierarchy
+
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Paint
+import android.util.AttributeSet
+import androidx.constraintlayout.widget.ConstraintLayout
+import com.pluto.utilities.extensions.dp2px
+
+class NestedView : ConstraintLayout {
+
+ constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle)
+ constructor(context: Context, attrs: AttributeSet) : super(context, attrs, 0)
+ constructor(context: Context) : super(context, null, 0)
+
+ private var layerCount = 0
+
+ fun setLayerCount(layerCount: Int) {
+ this.layerCount = layerCount
+ setPadding(interval * layerCount + 2f.dp2px.toInt(), paddingTop, paddingRight, paddingBottom)
+ invalidate()
+ }
+
+ private val interval: Int = 10f.dp2px.toInt()
+ private val paint: Paint = object : Paint() {
+ init {
+ color = Color.GRAY
+ style = Style.FILL
+ strokeWidth = 0.5f.dp2px
+ }
+ }
+ private val colorList: List = arrayListOf(
+ Color.BLACK,
+ Color.parseColor("#546E7A"),
+ Color.parseColor("#FF1744"),
+ Color.parseColor("#FFA000"),
+ Color.parseColor("#1DE9B6"),
+ Color.parseColor("#03A9F4"),
+ Color.parseColor("#00E5FF"),
+ Color.parseColor("#388E3C"),
+ Color.parseColor("#76FF03"),
+ Color.parseColor("#EEFF41")
+ )
+
+ init {
+ setWillNotDraw(false)
+ }
+
+ override fun onDraw(canvas: Canvas) {
+ super.onDraw(canvas)
+ for (i in 1..layerCount) {
+ paint.color = colorList[i % colorList.size]
+ paint.strokeWidth = 1f.dp2px
+ canvas.drawLine((i * interval).toFloat(), 0f, (i * interval).toFloat(), measuredHeight.toFloat(), paint)
+ }
+ }
+}
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/hierarchy/ViewHierarchyFragment.kt b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/hierarchy/ViewHierarchyFragment.kt
new file mode 100644
index 000000000..a6aaf29bf
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/hierarchy/ViewHierarchyFragment.kt
@@ -0,0 +1,144 @@
+package com.pluto.plugins.layoutinspector.internal.hierarchy
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.core.view.isVisible
+import androidx.fragment.app.DialogFragment
+import androidx.fragment.app.activityViewModels
+import androidx.fragment.app.viewModels
+import androidx.lifecycle.Observer
+import androidx.navigation.fragment.findNavController
+import com.pluto.plugins.layoutinspector.R
+import com.pluto.plugins.layoutinspector.databinding.PlutoLiFragmentViewHierarchyBinding
+import com.pluto.plugins.layoutinspector.internal.ActivityLifecycle
+import com.pluto.plugins.layoutinspector.internal.hierarchy.list.HierarchyAdapter
+import com.pluto.plugins.layoutinspector.internal.hierarchy.list.HierarchyItemHolder.Companion.ACTION_ATTRIBUTE
+import com.pluto.plugins.layoutinspector.internal.hierarchy.list.HierarchyItemHolder.Companion.ACTION_EXPAND_COLLAPSE
+import com.pluto.plugins.layoutinspector.internal.hierarchy.list.HierarchyItemHolder.Companion.ACTION_INSPECT_VIEW
+import com.pluto.plugins.layoutinspector.internal.inspect.InspectViewModel
+import com.pluto.plugins.layoutinspector.internal.inspect.assignTargetTag
+import com.pluto.plugins.layoutinspector.internal.inspect.getFrontView
+import com.pluto.utilities.autoClearInitializer
+import com.pluto.utilities.extensions.onBackPressed
+import com.pluto.utilities.extensions.toast
+import com.pluto.utilities.list.BaseAdapter
+import com.pluto.utilities.list.DiffAwareAdapter
+import com.pluto.utilities.list.DiffAwareHolder
+import com.pluto.utilities.list.ListItem
+import com.pluto.utilities.setOnDebounceClickListener
+import com.pluto.utilities.viewBinding
+
+internal class ViewHierarchyFragment : DialogFragment() {
+
+ private var rootView: View? = null
+ private val hierarchyAdapter: BaseAdapter by autoClearInitializer {
+ HierarchyAdapter(onActionListener)
+ }
+ private val viewModel: ViewHierarchyViewModel by viewModels()
+ private val inspectViewModel: InspectViewModel by activityViewModels()
+ private val binding by viewBinding(PlutoLiFragmentViewHierarchyBinding::bind)
+ private var hasAlreadyScrolled: Boolean = false
+ private val shouldScrollToInspectedView: Boolean
+ get() = arguments?.getBoolean(SCROLL_TO_TARGET) ?: false
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
+ return inflater.inflate(R.layout.pluto_li___fragment_view_hierarchy, container, false)
+ }
+
+ override fun getTheme(): Int = R.style.PlutoLIFullScreenDialogStyle
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ onBackPressed { findNavController().navigateUp() }
+ binding.close.setOnDebounceClickListener {
+ findNavController().navigateUp()
+ }
+
+ ActivityLifecycle.topActivity?.getFrontView()?.let {
+ rootView = it.findViewById(android.R.id.content)
+ } ?: run {
+ toast("root view not found, go back & try again")
+ }
+
+ rootView?.let { root ->
+ binding.expandCta.setOnDebounceClickListener {
+ viewModel.expandAll(root)
+ }
+ binding.collapseCta.setOnDebounceClickListener {
+ viewModel.collapseAll(root)
+ }
+ binding.list.apply {
+ adapter = hierarchyAdapter
+ }
+
+ viewModel.list.removeObserver(parsedAttrObserver)
+ viewModel.list.observe(viewLifecycleOwner, parsedAttrObserver)
+ viewModel.parseInit(root)
+ }
+ }
+
+ private fun scrollToInspectedView(list: List) {
+ var inspectedViewIndex = list.indexOfFirst { it.isTargetView }
+ if (inspectedViewIndex > -1) {
+ if (inspectedViewIndex < list.size - 1) inspectedViewIndex++
+ binding.list.smoothScrollToPosition(inspectedViewIndex)
+ }
+ }
+
+ private val parsedAttrObserver = Observer> {
+ hierarchyAdapter.list = arrayListOf().apply {
+ addAll(it)
+ }
+
+ if (shouldScrollToInspectedView && !hasAlreadyScrolled) {
+ scrollToInspectedView(it)
+ hasAlreadyScrolled = true
+ }
+ }
+
+ private val onActionListener = object : DiffAwareAdapter.OnActionListener {
+ override fun onAction(action: String, data: ListItem, holder: DiffAwareHolder) {
+ if (data is Hierarchy) {
+ when (action) {
+ ACTION_INSPECT_VIEW -> inspectView(data.view)
+ ACTION_ATTRIBUTE -> showViewAttribute(data.view)
+ ACTION_EXPAND_COLLAPSE -> collapseExpandView(data, holder)
+ }
+ }
+ }
+ }
+
+ private fun inspectView(view: View) {
+ if (view.isVisible) {
+ inspectViewModel.select(view)
+ dismiss()
+ } else {
+ context?.toast("View is not visible")
+ }
+ }
+
+ private fun showViewAttribute(view: View) {
+ if (view.isVisible) {
+ inspectViewModel.select(view)
+ rootView?.let { viewModel.parseInit(it) }
+ findNavController().navigate(R.id.openAttrView)
+ } else {
+ view.assignTargetTag()
+ findNavController().navigate(R.id.openAttrView)
+ }
+ }
+
+ private fun collapseExpandView(data: Hierarchy, holder: DiffAwareHolder) {
+ if (data.isExpanded) {
+ viewModel.removeChildren(data, holder.layoutPosition)
+ } else {
+ viewModel.addChildren(data, holder.layoutPosition)
+ }
+ }
+
+ companion object {
+ const val SCROLL_TO_TARGET = "scroll_to_target"
+ }
+}
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/hierarchy/ViewHierarchyViewModel.kt b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/hierarchy/ViewHierarchyViewModel.kt
new file mode 100644
index 000000000..90c0515b4
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/hierarchy/ViewHierarchyViewModel.kt
@@ -0,0 +1,81 @@
+package com.pluto.plugins.layoutinspector.internal.hierarchy
+
+import android.app.Application
+import android.view.View
+import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.launch
+
+internal class ViewHierarchyViewModel(application: Application) : AndroidViewModel(application) {
+
+ val list: LiveData>
+ get() = _list
+ private val _list = MutableLiveData>()
+
+ fun parseInit(v: View) {
+ expandAll(v)
+ }
+
+ fun expandAll(rootView: View) {
+ viewModelScope.launch {
+ val root = Hierarchy(rootView, 0, isExpanded = true)
+ val children = root.assembleChildren(true)
+ val list = arrayListOf().apply {
+ add(root)
+ addAll(children)
+ }
+ _list.postValue(list)
+ }
+ }
+
+ fun collapseAll(rootView: View) {
+ viewModelScope.launch {
+ val list = arrayListOf(
+ Hierarchy(rootView, 0)
+ )
+ _list.postValue(list)
+ }
+ }
+
+ fun removeChildren(data: Hierarchy, layoutPosition: Int) {
+ viewModelScope.launch {
+ var isNotValidChild = false
+ val newList = (_list.value ?: arrayListOf()).filterIndexed { index, value ->
+ if (index <= layoutPosition) {
+ true
+ } else {
+ if (value.layerCount == data.layerCount) {
+ isNotValidChild = true
+ }
+ isNotValidChild || value.layerCount <= data.layerCount
+ }
+ }
+ val list = arrayListOf().apply {
+ addAll(newList)
+ }
+ list[layoutPosition] = Hierarchy(
+ view = data.view,
+ layerCount = data.layerCount,
+ isExpanded = false
+ )
+ _list.postValue(list)
+ }
+ }
+
+ fun addChildren(data: Hierarchy, layoutPosition: Int) {
+ viewModelScope.launch {
+ val children = data.assembleChildren(false)
+ val list = _list.value ?: arrayListOf()
+
+ list[layoutPosition] = Hierarchy(
+ view = data.view,
+ layerCount = data.layerCount,
+ isExpanded = true
+ )
+ list.addAll(layoutPosition + 1, children)
+ _list.postValue(list)
+ }
+ }
+}
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/hierarchy/list/HierarchyAdapter.kt b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/hierarchy/list/HierarchyAdapter.kt
new file mode 100644
index 000000000..ea5ade630
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/hierarchy/list/HierarchyAdapter.kt
@@ -0,0 +1,27 @@
+package com.pluto.plugins.layoutinspector.internal.hierarchy.list
+
+import android.view.ViewGroup
+import com.pluto.plugins.layoutinspector.internal.hierarchy.Hierarchy
+import com.pluto.utilities.list.BaseAdapter
+import com.pluto.utilities.list.DiffAwareHolder
+import com.pluto.utilities.list.ListItem
+
+internal class HierarchyAdapter(private val listener: OnActionListener) : BaseAdapter() {
+ override fun getItemViewType(item: ListItem): Int? {
+ return when (item) {
+ is Hierarchy -> ITEM_TYPE_HIERARCHY
+ else -> null
+ }
+ }
+
+ override fun onViewHolderCreated(parent: ViewGroup, viewType: Int): DiffAwareHolder? {
+ return when (viewType) {
+ ITEM_TYPE_HIERARCHY -> HierarchyItemHolder(parent, listener)
+ else -> null
+ }
+ }
+
+ companion object {
+ const val ITEM_TYPE_HIERARCHY = 1000
+ }
+}
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/hierarchy/list/HierarchyItemHolder.kt b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/hierarchy/list/HierarchyItemHolder.kt
new file mode 100644
index 000000000..b2bc83e2b
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/hierarchy/list/HierarchyItemHolder.kt
@@ -0,0 +1,90 @@
+package com.pluto.plugins.layoutinspector.internal.hierarchy.list
+
+import android.view.View
+import android.view.View.GONE
+import android.view.View.VISIBLE
+import android.view.ViewGroup
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.core.content.ContextCompat
+import com.pluto.plugins.layoutinspector.R
+import com.pluto.plugins.layoutinspector.databinding.PlutoLiItemViewHierarchyBinding
+import com.pluto.plugins.layoutinspector.internal.hierarchy.Hierarchy
+import com.pluto.plugins.layoutinspector.internal.inspect.getIdString
+import com.pluto.utilities.extensions.color
+import com.pluto.utilities.extensions.dp
+import com.pluto.utilities.extensions.inflate
+import com.pluto.utilities.extensions.showMoreOptions
+import com.pluto.utilities.list.DiffAwareAdapter
+import com.pluto.utilities.list.DiffAwareHolder
+import com.pluto.utilities.list.ListItem
+import com.pluto.utilities.setOnDebounceClickListener
+import com.pluto.utilities.spannable.setSpan
+
+internal class HierarchyItemHolder(parent: ViewGroup, actionListener: DiffAwareAdapter.OnActionListener) :
+ DiffAwareHolder(parent.inflate(R.layout.pluto_li___item_view_hierarchy), actionListener) {
+
+ private val binding = PlutoLiItemViewHierarchyBinding.bind(itemView)
+
+ override fun onBind(item: ListItem) {
+ if (item is Hierarchy) {
+ if (item.isTargetView) {
+ binding.contentWrapper.background = ContextCompat.getDrawable(context, R.drawable.pluto_li___ic_hierarchy_target_view_indicator)
+ } else {
+ binding.contentWrapper.background = null
+ }
+ binding.viewTitle.setSpan {
+ append(item.view.javaClass.simpleName)
+ if (item.view is ViewGroup) {
+ append(regular(fontColor(" (${item.view.childCount})", context.color(com.pluto.plugin.R.color.pluto___text_dark_40))))
+ }
+ }
+ binding.viewSubtitle.setSpan {
+ item.view.getIdString()?.let {
+ append(it)
+ } ?: run {
+ append(regular(italic(fontColor("NO_ID", context.color(com.pluto.plugin.R.color.pluto___text_dark_40)))))
+ }
+ append(" {(${item.view.left},${item.view.top}),(${item.view.right},${item.view.bottom})}")
+ }
+ binding.viewActionCta.setOnDebounceClickListener {
+ showActionOptions(it)
+ }
+ binding.contentWrapper.setOnLongClickListener {
+ showActionOptions(it)
+ true
+ }
+ binding.expandStateIndicator.setImageResource(
+ if (item.isExpanded) {
+ R.drawable.pluto_li___ic_hierarchy_show_less
+ } else {
+ R.drawable.pluto_li___ic_hierarchy_show_more
+ }
+ )
+ binding.expandStateIndicator.visibility = if (item.view is ViewGroup && item.view.childCount > 0) VISIBLE else GONE
+ if (item.view is ViewGroup && item.view.childCount > 0) {
+ binding.contentWrapper.setOnDebounceClickListener { onAction(ACTION_EXPAND_COLLAPSE) }
+ } else {
+ binding.contentWrapper.setOnDebounceClickListener(action = null)
+ }
+ val layoutParams: ConstraintLayout.LayoutParams = binding.viewTitle.layoutParams as ConstraintLayout.LayoutParams
+ layoutParams.marginStart = if (item.view !is ViewGroup) 8f.dp.toInt() else 0
+ binding.viewTitle.layoutParams = layoutParams
+ binding.root.setLayerCount(item.layerCount)
+ }
+ }
+
+ private fun showActionOptions(view: View) {
+ context.showMoreOptions(view, R.menu.pluto_li___menu_hierarchy_options) { item ->
+ when (item.itemId) {
+ R.id.select -> onAction(ACTION_INSPECT_VIEW)
+ R.id.attribute -> onAction(ACTION_ATTRIBUTE)
+ }
+ }
+ }
+
+ companion object {
+ const val ACTION_INSPECT_VIEW = "inspect"
+ const val ACTION_ATTRIBUTE = "attribute"
+ const val ACTION_EXPAND_COLLAPSE = "expand_collapse"
+ }
+}
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/hint/HintAdapter.kt b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/hint/HintAdapter.kt
new file mode 100644
index 000000000..d77b09a57
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/hint/HintAdapter.kt
@@ -0,0 +1,30 @@
+package com.pluto.plugins.layoutinspector.internal.hint
+
+import android.view.ViewGroup
+import com.pluto.utilities.list.BaseAdapter
+import com.pluto.utilities.list.DiffAwareHolder
+import com.pluto.utilities.list.ListItem
+
+internal class HintAdapter : BaseAdapter() {
+ override fun getItemViewType(item: ListItem): Int? {
+ return when (item) {
+ is HintItem -> ITEM_TYPE_HINT
+ else -> null
+ }
+ }
+
+ override fun onViewHolderCreated(parent: ViewGroup, viewType: Int): DiffAwareHolder? {
+ return when (viewType) {
+ ITEM_TYPE_HINT -> HintItemHolder(parent)
+ else -> null
+ }
+ }
+
+ companion object {
+ const val ITEM_TYPE_HINT = 1000
+ }
+}
+
+internal data class HintItem(
+ val text: String
+) : ListItem()
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/hint/HintFragment.kt b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/hint/HintFragment.kt
new file mode 100644
index 000000000..810aa143a
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/hint/HintFragment.kt
@@ -0,0 +1,42 @@
+package com.pluto.plugins.layoutinspector.internal.hint
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.Observer
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment
+import com.pluto.plugins.layoutinspector.R
+import com.pluto.plugins.layoutinspector.databinding.PlutoLiHintFragmentBinding
+import com.pluto.utilities.autoClearInitializer
+import com.pluto.utilities.extensions.dp
+import com.pluto.utilities.list.BaseAdapter
+import com.pluto.utilities.list.CustomItemDecorator
+import com.pluto.utilities.viewBinding
+
+internal class HintFragment : BottomSheetDialogFragment() {
+
+ private val binding by viewBinding(PlutoLiHintFragmentBinding::bind)
+ private val settingsAdapter: BaseAdapter by autoClearInitializer { HintAdapter() }
+ private val viewModel: HintViewModel by activityViewModels()
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
+ inflater.inflate(R.layout.pluto_li___hint_fragment, container, false)
+
+ override fun getTheme(): Int = com.pluto.plugin.R.style.PlutoBottomSheetDialogTheme
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ binding.list.apply {
+ adapter = settingsAdapter
+ addItemDecoration(CustomItemDecorator(context, 16f.dp.toInt()))
+ }
+ viewModel.list.removeObserver(settingsObserver)
+ viewModel.list.observe(viewLifecycleOwner, settingsObserver)
+ }
+
+ private val settingsObserver = Observer> {
+ settingsAdapter.list = it
+ }
+}
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/hint/HintItemHolder.kt b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/hint/HintItemHolder.kt
new file mode 100644
index 000000000..46b6d1436
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/hint/HintItemHolder.kt
@@ -0,0 +1,21 @@
+package com.pluto.plugins.layoutinspector.internal.hint
+
+import android.view.ViewGroup
+import com.pluto.plugins.layoutinspector.R
+import com.pluto.plugins.layoutinspector.databinding.PlutoLiItemHintBinding
+import com.pluto.utilities.extensions.inflate
+import com.pluto.utilities.list.DiffAwareAdapter
+import com.pluto.utilities.list.DiffAwareHolder
+import com.pluto.utilities.list.ListItem
+
+internal class HintItemHolder(parent: ViewGroup, listener: DiffAwareAdapter.OnActionListener? = null) :
+ DiffAwareHolder(parent.inflate(R.layout.pluto_li___item_hint), listener) {
+
+ private val binding = PlutoLiItemHintBinding.bind(itemView)
+
+ override fun onBind(item: ListItem) {
+ if (item is HintItem) {
+ binding.text.text = "${layoutPosition + 1}.\t${item.text}"
+ }
+ }
+}
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/hint/HintViewModel.kt b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/hint/HintViewModel.kt
new file mode 100644
index 000000000..27753014d
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/hint/HintViewModel.kt
@@ -0,0 +1,28 @@
+package com.pluto.plugins.layoutinspector.internal.hint
+
+import android.app.Application
+import android.content.Context
+import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import com.pluto.plugins.layoutinspector.R
+
+internal class HintViewModel(application: Application) : AndroidViewModel(application) {
+
+ val list: LiveData>
+ get() = _list
+ private val _list = MutableLiveData>()
+
+ init {
+ generate(getApplication())
+ }
+
+ private fun generate(context: Context?) {
+ context?.apply {
+ val list = arrayListOf()
+ list.add(HintItem(getString(R.string.pluto_li___hint_select_view)))
+ list.add(HintItem(getString(R.string.pluto_li___hint_move_view)))
+ _list.postValue(list)
+ }
+ }
+}
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/inspect/InspectOverlay.kt b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/inspect/InspectOverlay.kt
new file mode 100644
index 000000000..31e524f30
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/inspect/InspectOverlay.kt
@@ -0,0 +1,255 @@
+package com.pluto.plugins.layoutinspector.internal.inspect
+
+import android.animation.ObjectAnimator
+import android.animation.ValueAnimator
+import android.app.Activity
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Paint
+import android.util.AttributeSet
+import android.util.Log
+import android.view.MotionEvent
+import android.view.View
+import android.view.ViewConfiguration
+import android.view.ViewGroup
+import androidx.core.view.children
+import com.pluto.plugins.layoutinspector.internal.inspect.canvas.CaptureCanvas
+import com.pluto.plugins.layoutinspector.internal.inspect.canvas.DimensionCanvas
+import com.pluto.plugins.layoutinspector.internal.inspect.canvas.GridCanvas
+import com.pluto.utilities.extensions.dp2px
+
+internal class InspectOverlay : View {
+
+ private var gridAnimator: ValueAnimator? = null
+ private var touchSlop: Int = ViewConfiguration.get(context).scaledTouchSlop
+ private var longPressTimeout: Int = ViewConfiguration.getLongPressTimeout()
+
+ private var gridCanvas: GridCanvas = GridCanvas(this)
+ private var dimenCanvas: DimensionCanvas = DimensionCanvas(this)
+ private var captureCanvas: CaptureCanvas = CaptureCanvas(this)
+
+ private var prevCoordinate = CoordinatePair()
+ private var downCoordinate = CoordinatePair()
+
+ private var state: State = State.Idle
+
+ private val inspectedViews = arrayListOf()
+ private var targetInspectedView: InspectedView? = null
+
+ private var clickListener: OnClickListener? = null
+
+ constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle)
+ constructor(context: Context, attrs: AttributeSet) : super(context, attrs, 0)
+ constructor(context: Context) : super(context, null, 0)
+
+ private val boundaryPaint: Paint = object : Paint(ANTI_ALIAS_FLAG) {
+ init {
+ color = Color.YELLOW
+ strokeWidth = 2f.dp2px
+ style = Style.STROKE
+ }
+ }
+ private val longPressCheck = Runnable {
+ state = State.Dragging
+ alpha = 1f
+ gridAnimator = ObjectAnimator.ofFloat(0f, 1f).setDuration(longPressTimeout.toLong())
+ gridAnimator?.addUpdateListener { animation ->
+ alpha = animation.animatedValue as Float
+ invalidate()
+ }
+ gridAnimator?.start()
+ }
+
+ fun tryGetFrontView(targetActivity: Activity) {
+ traverse(targetActivity.getFrontView())
+ }
+
+ override fun onTouchEvent(event: MotionEvent?): Boolean {
+ when (event!!.action) {
+ MotionEvent.ACTION_DOWN -> {
+ handleActionDown(event)
+ return true
+ }
+
+ MotionEvent.ACTION_MOVE -> handleActionMove(event)
+ MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> handleActionCancel(event)
+ }
+ return super.onTouchEvent(event)
+ }
+
+ override fun onDraw(canvas: Canvas) {
+ super.onDraw(canvas)
+ canvas.drawRect(0f, 0f, measuredWidth.toFloat(), measuredHeight.toFloat(), boundaryPaint)
+ when (state) {
+ is State.Dragging -> gridCanvas.draw(canvas)
+ else -> {}
+ }
+ captureCanvas.draw(canvas, targetInspectedView)
+ dimenCanvas.draw(canvas, targetInspectedView)
+ }
+
+ private fun handleActionCancel(event: MotionEvent) {
+ cancelCheckTask()
+ when (state) {
+ is State.Idle -> handleClick(event.x, event.y)
+ is State.Dragging -> resetAll()
+ else -> {}
+ }
+ state = State.Idle
+ invalidate()
+ }
+
+ private fun handleActionMove(event: MotionEvent) {
+ when (state) {
+ is State.Dragging -> targetInspectedView?.let {
+ val dx: Float = event.x - prevCoordinate.x
+ val dy: Float = event.y - prevCoordinate.y
+ it.offset(dx, dy)
+ targetInspectedView?.reset()
+ invalidate()
+ }
+
+ is State.Touching -> {
+ }
+
+ else -> {
+ val dx: Float = event.x - downCoordinate.x
+ val dy: Float = event.y - downCoordinate.y
+ if (dx * dx + dy * dy > touchSlop * touchSlop) {
+ state = State.Touching
+ cancelCheckTask()
+ invalidate()
+ }
+ }
+ }
+ prevCoordinate = CoordinatePair(event.x, event.y)
+ }
+
+ private fun handleActionDown(event: MotionEvent) {
+ prevCoordinate = CoordinatePair(event.x, event.y)
+ downCoordinate = CoordinatePair(event.x, event.y)
+ tryStartCheckTask()
+ }
+
+ override fun onDetachedFromWindow() {
+ super.onDetachedFromWindow()
+ inspectedViews.clear()
+ cancelCheckTask()
+ targetInspectedView = null
+ }
+
+ private fun cancelCheckTask() {
+ removeCallbacks(longPressCheck)
+ gridAnimator?.cancel()
+ gridAnimator = null
+ }
+
+ private fun tryStartCheckTask() {
+ cancelCheckTask()
+ targetInspectedView?.let {
+ postDelayed(longPressCheck, longPressTimeout.toLong())
+ }
+ }
+
+ private fun handleClick(x: Float, y: Float) {
+ getInspectedView(x, y)?.let { processViewInspection(it, true) }
+ }
+
+ fun handleClick(v: View, cancelIfSelected: Boolean): Boolean {
+ return getInspectedView(v)?.let {
+ processViewInspection(it, cancelIfSelected)
+ invalidate()
+ true
+ } ?: run {
+ false
+ }
+ }
+
+ @SuppressWarnings("LoopWithTooManyJumpStatements")
+ fun getInspectedView(x: Float, y: Float): InspectedView? {
+ var target: InspectedView? = null
+ for (i in inspectedViews.indices.reversed()) {
+ val inspectedView = inspectedViews[i]
+ if (inspectedView.rect.contains(x.toInt(), y.toInt())) {
+ if (isParentNotVisible(inspectedView.parent)) {
+ continue
+ }
+ target = inspectedView
+ break
+ }
+ }
+ if (target == null) {
+ Log.w(TAG, "getInspectedView: not find")
+ }
+ return target
+ }
+
+ private fun getInspectedView(v: View): InspectedView? {
+ var target: InspectedView? = null
+ for (i in inspectedViews.indices.reversed()) {
+ val inspectedView = inspectedViews[i]
+ if (inspectedView.view === v) {
+ target = inspectedView
+ break
+ }
+ }
+ if (target == null) {
+ Log.w(TAG, "getInspectedView: not find")
+ }
+ return target
+ }
+
+ private fun isParentNotVisible(parent: InspectedView?): Boolean {
+ if (parent == null) {
+ return false
+ }
+ return if (parent.rect.left >= measuredWidth || parent.rect.top >= measuredHeight) {
+ true
+ } else {
+ isParentNotVisible(parent.parent)
+ }
+ }
+
+ private fun processViewInspection(inspectedView: InspectedView, cancelIfSelected: Boolean) {
+ targetInspectedView = if (targetInspectedView == inspectedView && cancelIfSelected) {
+ null
+ } else {
+ inspectedView
+ }
+ dimenCanvas.inspectedView = targetInspectedView
+ clickListener?.onClick(inspectedView.view)
+ }
+
+ fun isSelectedEmpty(): Boolean = targetInspectedView == null
+
+ override fun setOnClickListener(l: OnClickListener?) {
+ clickListener = l
+ }
+
+ companion object {
+ private const val TAG = "InspectOverlay"
+ }
+
+ private data class CoordinatePair(val x: Float = 0f, val y: Float = 0f)
+
+ private sealed class State {
+ object Idle : State()
+ object Touching : State() // trigger move before dragging
+ object Dragging : State() // since long press
+ }
+
+ private fun traverse(view: View) {
+ if (view.alpha == 0f || view.visibility != VISIBLE) return
+ inspectedViews.add(InspectedView(view))
+ if (view is ViewGroup) {
+ view.children.forEach {
+ traverse(it)
+ }
+ }
+ }
+
+ private fun resetAll() {
+ inspectedViews.forEach { it.reset() }
+ }
+}
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/inspect/InspectViewModel.kt b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/inspect/InspectViewModel.kt
new file mode 100644
index 000000000..d9b468fc4
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/inspect/InspectViewModel.kt
@@ -0,0 +1,18 @@
+package com.pluto.plugins.layoutinspector.internal.inspect
+
+import android.app.Application
+import android.view.View
+import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.LiveData
+import com.pluto.utilities.SingleLiveEvent
+
+internal class InspectViewModel(application: Application) : AndroidViewModel(application) {
+
+ val view: LiveData
+ get() = _view
+ private val _view = SingleLiveEvent()
+
+ fun select(view: View) {
+ _view.postValue(view)
+ }
+}
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/inspect/InspectedView.kt b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/inspect/InspectedView.kt
new file mode 100644
index 000000000..4cb54733b
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/inspect/InspectedView.kt
@@ -0,0 +1,47 @@
+package com.pluto.plugins.layoutinspector.internal.inspect
+
+import android.graphics.Rect
+import android.view.View
+
+internal class InspectedView(val view: View) {
+
+ private val originRect: Rect = Rect()
+ val rect: Rect = Rect()
+ private val location = IntArray(2)
+ val parent: InspectedView?
+ get() {
+ val parentView: Any = view.parent
+ return if (parentView is View) {
+ InspectedView(parentView)
+ } else {
+ null
+ }
+ }
+
+ init {
+ reset()
+ originRect.set(rect.left, rect.top, rect.right, rect.bottom)
+ }
+
+ fun reset() {
+ view.getLocationOnScreen(location)
+ val left = location[0]
+ val right = left + view.width
+ val top = location[1]
+ val bottom = top + view.height
+ rect.set(left, top, right, bottom)
+ }
+
+ fun offset(dx: Float, dy: Float) {
+ view.translationX = view.translationX + dx
+ view.translationY = view.translationY + dy
+ }
+
+ override fun equals(other: Any?): Boolean {
+// if (this === other) return true
+// if (other == null || javaClass != other.javaClass) return false
+ return other is InspectedView && view == other.view
+ }
+
+ override fun hashCode(): Int = view.hashCode()
+}
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/inspect/ViewUtilsKtx.kt b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/inspect/ViewUtilsKtx.kt
new file mode 100644
index 000000000..1664a1ffd
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/inspect/ViewUtilsKtx.kt
@@ -0,0 +1,119 @@
+package com.pluto.plugins.layoutinspector.internal.inspect
+
+import android.annotation.SuppressLint
+import android.app.Activity
+import android.content.ContextWrapper
+import android.content.res.Resources
+import android.os.Build
+import android.view.View
+import android.view.ViewGroup
+import android.view.WindowManager
+import androidx.core.view.children
+import com.pluto.plugins.layoutinspector.R
+import com.pluto.utilities.extensions.color
+import com.pluto.utilities.extensions.getIdInfo
+import com.pluto.utilities.spannable.createSpan
+import java.lang.reflect.Field
+
+@SuppressLint("SoonBlockedPrivateApi", "DiscouragedPrivateApi", "PrivateApi")
+@SuppressWarnings("TooGenericExceptionCaught", "NestedBlockDepth")
+internal fun Activity.getFrontView(): View {
+ try {
+ val windowManagerImplClazz: Class<*> = Class.forName("android.view.WindowManagerImpl")
+ val windowManagerGlobalClazz: Class<*> = Class.forName("android.view.WindowManagerGlobal")
+ val viewRootImplClazz: Class<*> = Class.forName("android.view.ViewRootImpl")
+
+ val globalField: Field = windowManagerImplClazz.getDeclaredField("mGlobal")
+ globalField.isAccessible = true
+
+ if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
+ val viewField = windowManagerGlobalClazz.getDeclaredField("mViews")
+ viewField.isAccessible = true
+ (viewField[globalField[windowManager]] as List).reversed().forEach { view ->
+ getDecorView(view)?.let {
+ return it
+ }
+ }
+ } else {
+ val rootsField: Field = windowManagerGlobalClazz.getDeclaredField("mRoots")
+ rootsField.isAccessible = true
+ val viewRootImplList: List<*> = rootsField[globalField[windowManager]] as List<*>
+ viewRootImplList.reversed().forEach { rootImpl ->
+ val windowAttributesField: Field = viewRootImplClazz.getDeclaredField("mWindowAttributes")
+ windowAttributesField.isAccessible = true
+ val viewField: Field = viewRootImplClazz.getDeclaredField("mView")
+ viewField.isAccessible = true
+ val decorView = viewField[rootImpl] as View
+ val layoutParams = windowAttributesField[rootImpl] as WindowManager.LayoutParams
+ if (layoutParams.title.toString().contains(javaClass.name) || getDecorView(decorView) != null) {
+ return decorView
+ }
+ }
+ }
+ } catch (e: Throwable) {
+ e.printStackTrace()
+ }
+ return window.peekDecorView()
+}
+
+@SuppressWarnings("LoopWithTooManyJumpStatements")
+private fun Activity.getDecorView(decorView: View): View? {
+ var targetView: View? = null
+ var context = decorView.context
+ if (context === this) {
+ targetView = decorView
+ } else {
+ while (context is ContextWrapper && context !is Activity) {
+ val baseContext = context.baseContext ?: break
+ if (baseContext === this) {
+ targetView = decorView
+ break
+ }
+ context = baseContext
+ }
+ }
+ return targetView
+}
+
+internal fun View.getIdString(): CharSequence? = try {
+ getIdInfo()?.let {
+ context?.createSpan {
+ append(semiBold(fontColor(it.packageName, context.color(com.pluto.plugin.R.color.pluto___text_dark_60))))
+ append(semiBold(fontColor(":", context.color(com.pluto.plugin.R.color.pluto___text_dark_60))))
+ append(semiBold(fontColor(it.typeName, context.color(com.pluto.plugin.R.color.pluto___text_dark_60))))
+ append(semiBold(fontColor("/", context.color(com.pluto.plugin.R.color.pluto___text_dark_60))))
+ append(semiBold(fontColor(it.entryName, context.color(com.pluto.plugin.R.color.pluto___text_dark_80))))
+ } ?: run { null }
+ } ?: run { null }
+} catch (e: Resources.NotFoundException) {
+ e.printStackTrace()
+ Integer.toHexString(id)
+}
+
+internal fun View.findViewByTargetTag(): View? {
+ if (verifyTargetTag()) {
+ return this
+ }
+ if (this is ViewGroup) {
+ children.forEach { child ->
+ child.findViewByTargetTag()?.let {
+ return it
+ }
+ }
+ }
+ return null
+}
+
+internal fun View.clearTargetTag() {
+ setTag(TARGET_VIEW_TAG_LABEL, null)
+}
+
+internal fun View.assignTargetTag() {
+ setTag(TARGET_VIEW_TAG_LABEL, Any())
+}
+
+internal fun View.verifyTargetTag(): Boolean {
+ return getTag(TARGET_VIEW_TAG_LABEL) != null
+}
+
+private val TARGET_VIEW_TAG_LABEL = R.id.pluto_li___unique_view_tag
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/inspect/canvas/CaptureCanvas.kt b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/inspect/canvas/CaptureCanvas.kt
new file mode 100644
index 000000000..42b380445
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/inspect/canvas/CaptureCanvas.kt
@@ -0,0 +1,83 @@
+package com.pluto.plugins.layoutinspector.internal.inspect.canvas
+
+import android.graphics.Canvas
+import android.graphics.DashPathEffect
+import android.graphics.Paint
+import android.graphics.Rect
+import android.view.View
+import com.pluto.plugin.settings.SettingsPreferences
+import com.pluto.plugins.layoutinspector.R
+import com.pluto.plugins.layoutinspector.internal.inspect.InspectedView
+import com.pluto.utilities.extensions.color
+import com.pluto.utilities.extensions.dp2px
+
+internal class CaptureCanvas(private val container: View) {
+
+ private val cornerCirclePaint: Paint = object : Paint() {
+ init {
+ isAntiAlias = true
+ strokeWidth = 1f.dp2px
+ }
+ }
+ private val captureBoxPaint: Paint = object : Paint() {
+ init {
+ isAntiAlias = true
+ color = container.context.color(com.pluto.plugin.R.color.pluto___blue)
+ style = Style.STROKE
+ strokeWidth = 1f.dp2px
+ }
+ }
+ private val dashLinePaint: Paint = object : Paint() {
+ init {
+ isAntiAlias = true
+ color = container.context.color(com.pluto.plugin.R.color.pluto___emerald)
+ style = Style.STROKE
+ strokeWidth = 1f
+ pathEffect = DashPathEffect(floatArrayOf(3f.dp2px, 3f.dp2px), 0f)
+ }
+ }
+ private val cornerRadius: Float = 1f.dp2px
+
+ private val measuredHeight: Float
+ get() = container.measuredHeight.toFloat()
+
+ private val measuredWidth: Float
+ get() = container.measuredWidth.toFloat()
+
+ init {
+ container.setLayerType(View.LAYER_TYPE_SOFTWARE, null)
+ }
+
+ fun draw(canvas: Canvas, inspectedView: InspectedView?) {
+ canvas.save()
+ inspectedView?.let { drawSelected(canvas, it) }
+ canvas.restore()
+ }
+
+ private fun drawSelected(canvas: Canvas, inspectedView: InspectedView) {
+ val rect: Rect = inspectedView.rect
+ canvas.drawLine(0f, rect.top.toFloat(), measuredWidth, rect.top.toFloat(), dashLinePaint)
+ canvas.drawLine(0f, rect.bottom.toFloat(), measuredWidth, rect.bottom.toFloat(), dashLinePaint)
+ canvas.drawLine(rect.left.toFloat(), 0f, rect.left.toFloat(), measuredHeight, dashLinePaint)
+ canvas.drawLine(rect.right.toFloat(), 0f, rect.right.toFloat(), measuredHeight, dashLinePaint)
+ canvas.drawRect(rect, captureBoxPaint)
+ cornerCirclePaint.color = container.context.color(com.pluto.plugin.R.color.pluto___transparent)
+ cornerCirclePaint.style = Paint.Style.FILL
+ canvas.drawCircle(rect.left.toFloat(), rect.top.toFloat(), cornerRadius, cornerCirclePaint)
+ canvas.drawCircle(rect.right.toFloat(), rect.top.toFloat(), cornerRadius, cornerCirclePaint)
+ canvas.drawCircle(rect.left.toFloat(), rect.bottom.toFloat(), cornerRadius, cornerCirclePaint)
+ canvas.drawCircle(rect.right.toFloat(), rect.bottom.toFloat(), cornerRadius, cornerCirclePaint)
+ cornerCirclePaint.color = container.context.color(
+ if (SettingsPreferences.isDarkThemeEnabled) {
+ com.pluto.plugin.R.color.pluto___red
+ } else {
+ com.pluto.plugin.R.color.pluto___orange
+ }
+ )
+ cornerCirclePaint.style = Paint.Style.STROKE
+ canvas.drawCircle(rect.left.toFloat(), rect.top.toFloat(), cornerRadius, cornerCirclePaint)
+ canvas.drawCircle(rect.right.toFloat(), rect.top.toFloat(), cornerRadius, cornerCirclePaint)
+ canvas.drawCircle(rect.left.toFloat(), rect.bottom.toFloat(), cornerRadius, cornerCirclePaint)
+ canvas.drawCircle(rect.right.toFloat(), rect.bottom.toFloat(), cornerRadius, cornerCirclePaint)
+ }
+}
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/inspect/canvas/DimensionCanvas.kt b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/inspect/canvas/DimensionCanvas.kt
new file mode 100644
index 000000000..2b5a52e71
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/inspect/canvas/DimensionCanvas.kt
@@ -0,0 +1,101 @@
+package com.pluto.plugins.layoutinspector.internal.inspect.canvas
+
+import android.graphics.Canvas
+import android.graphics.Paint
+import android.graphics.Rect
+import android.graphics.RectF
+import android.view.View
+import androidx.core.content.res.ResourcesCompat
+import com.pluto.plugins.layoutinspector.R
+import com.pluto.plugins.layoutinspector.internal.inspect.InspectedView
+import com.pluto.utilities.extensions.color
+import com.pluto.utilities.extensions.dp2px
+import com.pluto.utilities.extensions.px2dp
+
+internal class DimensionCanvas(private val container: View) {
+
+ private val cornerRadius = 3f.dp2px
+ private val textBgTopPadding = 4f.dp2px
+ private val textBgBottomPadding = 1f.dp2px
+ private val textBgHorizontalPadding = 4f.dp2px
+ private val distanceBtwTextAndBorder = 7f.dp2px
+
+ private val textPaint: Paint = object : Paint() {
+ init {
+ color = container.context.color(com.pluto.plugin.R.color.pluto___red_dark)
+ style = Style.FILL
+ strokeWidth = 1f.dp2px
+ textSize = 12f.dp2px
+ typeface = ResourcesCompat.getFont(container.context, com.pluto.plugin.R.font.muli_semibold)
+ flags = FAKE_BOLD_TEXT_FLAG
+ }
+ }
+ private val textBgPaint: Paint = object : Paint() {
+ init {
+ isAntiAlias = true
+ strokeWidth = 1f.dp2px
+ color = container.context.color(com.pluto.plugin.R.color.pluto___white)
+ style = Style.FILL
+ }
+ }
+
+ var inspectedView: InspectedView? = null
+
+ fun draw(canvas: Canvas, inspectedView: InspectedView?) {
+ canvas.save()
+ inspectedView?.let {
+ this.inspectedView?.let {
+ val rect: Rect = it.rect
+ val widthText = "${rect.width().toFloat().px2dp.toInt()} dp"
+ drawText(canvas, widthText, rect.centerX() - getTextWidth(textPaint, widthText) / 2, rect.top - distanceBtwTextAndBorder + 3f.dp2px)
+ val heightText = "${rect.height().toFloat().px2dp.toInt()} dp"
+ drawText(canvas, heightText, rect.right + distanceBtwTextAndBorder, rect.centerY() + getTextHeight(textPaint, heightText) / 2)
+ } ?: run {
+ container.invalidate()
+ return
+ }
+ }
+ canvas.restore()
+ }
+
+ private fun drawText(canvas: Canvas, text: String, x: Float, y: Float) {
+ var left = x - textBgHorizontalPadding
+ var top: Float = y - getTextHeight(textPaint, text) - textBgTopPadding
+ var right: Float = x + getTextWidth(textPaint, text) + textBgHorizontalPadding
+ var bottom = y + textBgBottomPadding
+ // ensure text in screen bound
+ if (left < 0) {
+ right -= left
+ left = 0f
+ }
+ if (top < 0) {
+ bottom -= top
+ top = 0f
+ }
+ if (bottom > canvas.height) {
+ val diff = top - bottom
+ bottom = canvas.height.toFloat()
+ top = bottom + diff
+ }
+ if (right > canvas.width) {
+ val diff = left - right
+ right = canvas.width.toFloat()
+ left = right + diff
+ }
+ val tmpRectF = RectF().apply {
+ set(left, top, right, bottom)
+ }
+ canvas.drawRoundRect(tmpRectF, cornerRadius, cornerRadius, textBgPaint)
+ canvas.drawText(text, left + textBgHorizontalPadding, bottom - textBgTopPadding, textPaint)
+ }
+
+ private fun getTextHeight(paint: Paint, text: String): Float {
+ val rect = Rect()
+ paint.getTextBounds(text, 0, text.length, rect)
+ return rect.height().toFloat()
+ }
+
+ private fun getTextWidth(paint: Paint, text: String?): Float {
+ return paint.measureText(text)
+ }
+}
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/inspect/canvas/GridCanvas.kt b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/inspect/canvas/GridCanvas.kt
new file mode 100644
index 000000000..71f3f385e
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/java/com/pluto/plugins/layoutinspector/internal/inspect/canvas/GridCanvas.kt
@@ -0,0 +1,40 @@
+package com.pluto.plugins.layoutinspector.internal.inspect.canvas
+
+import android.graphics.Canvas
+import android.graphics.Paint
+import android.view.View
+import com.pluto.plugin.settings.SettingsPreferences
+import com.pluto.plugins.layoutinspector.R
+import com.pluto.utilities.extensions.color
+import com.pluto.utilities.extensions.dp2px
+
+internal class GridCanvas(private val container: View) {
+ private val gridPaint: Paint = object : Paint() {
+ init {
+ color = container.context.color(
+ if (SettingsPreferences.isDarkThemeEnabled) {
+ com.pluto.plugin.R.color.pluto___red_40
+ } else {
+ com.pluto.plugin.R.color.pluto___orange_40
+ }
+ )
+ style = Style.FILL
+ strokeWidth = 1f.dp2px
+ }
+ }
+
+ fun draw(canvas: Canvas) {
+ canvas.save()
+ var startX = 0
+ while (startX < container.measuredWidth) {
+ canvas.drawLine(startX.toFloat().dp2px, 0f, startX.toFloat().dp2px, container.measuredHeight.toFloat(), gridPaint)
+ startX += SettingsPreferences.gridSize
+ }
+ var startY = 0
+ while (startY < container.measuredHeight) {
+ canvas.drawLine(0f, startY.toFloat().dp2px, container.measuredWidth.toFloat(), startY.toFloat().dp2px, gridPaint)
+ startY += SettingsPreferences.gridSize
+ }
+ canvas.restore()
+ }
+}
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/res/drawable/pluto_li___ic_arrow_back.xml b/pluto-plugins/plugins/layout-inspector/lib/src/main/res/drawable/pluto_li___ic_arrow_back.xml
new file mode 100644
index 000000000..bd6a5a9cb
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/res/drawable/pluto_li___ic_arrow_back.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/res/drawable/pluto_li___ic_close.xml b/pluto-plugins/plugins/layout-inspector/lib/src/main/res/drawable/pluto_li___ic_close.xml
new file mode 100644
index 000000000..23ef1c507
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/res/drawable/pluto_li___ic_close.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/res/drawable/pluto_li___ic_control_close.xml b/pluto-plugins/plugins/layout-inspector/lib/src/main/res/drawable/pluto_li___ic_control_close.xml
new file mode 100644
index 000000000..ae90a27e1
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/res/drawable/pluto_li___ic_control_close.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/res/drawable/pluto_li___ic_control_hint.xml b/pluto-plugins/plugins/layout-inspector/lib/src/main/res/drawable/pluto_li___ic_control_hint.xml
new file mode 100644
index 000000000..333d22a71
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/res/drawable/pluto_li___ic_control_hint.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/res/drawable/pluto_li___ic_control_move_left.xml b/pluto-plugins/plugins/layout-inspector/lib/src/main/res/drawable/pluto_li___ic_control_move_left.xml
new file mode 100644
index 000000000..a9750778e
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/res/drawable/pluto_li___ic_control_move_left.xml
@@ -0,0 +1,14 @@
+
+
+
+
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/res/drawable/pluto_li___ic_control_move_right.xml b/pluto-plugins/plugins/layout-inspector/lib/src/main/res/drawable/pluto_li___ic_control_move_right.xml
new file mode 100644
index 000000000..07d93aca2
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/res/drawable/pluto_li___ic_control_move_right.xml
@@ -0,0 +1,14 @@
+
+
+
+
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/res/drawable/pluto_li___ic_hierarchy_attr.xml b/pluto-plugins/plugins/layout-inspector/lib/src/main/res/drawable/pluto_li___ic_hierarchy_attr.xml
new file mode 100644
index 000000000..71c5d2b5c
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/res/drawable/pluto_li___ic_hierarchy_attr.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/res/drawable/pluto_li___ic_hierarchy_collapse.xml b/pluto-plugins/plugins/layout-inspector/lib/src/main/res/drawable/pluto_li___ic_hierarchy_collapse.xml
new file mode 100644
index 000000000..4a9433b8f
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/res/drawable/pluto_li___ic_hierarchy_collapse.xml
@@ -0,0 +1,12 @@
+
+
+
+
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/res/drawable/pluto_li___ic_hierarchy_expand.xml b/pluto-plugins/plugins/layout-inspector/lib/src/main/res/drawable/pluto_li___ic_hierarchy_expand.xml
new file mode 100644
index 000000000..9aa5f4f45
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/res/drawable/pluto_li___ic_hierarchy_expand.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/res/drawable/pluto_li___ic_hierarchy_separator.xml b/pluto-plugins/plugins/layout-inspector/lib/src/main/res/drawable/pluto_li___ic_hierarchy_separator.xml
new file mode 100644
index 000000000..c69a68321
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/res/drawable/pluto_li___ic_hierarchy_separator.xml
@@ -0,0 +1,8 @@
+
+
+
+
\ No newline at end of file
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/res/drawable/pluto_li___ic_hierarchy_show_less.xml b/pluto-plugins/plugins/layout-inspector/lib/src/main/res/drawable/pluto_li___ic_hierarchy_show_less.xml
new file mode 100644
index 000000000..6bdbb481e
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/res/drawable/pluto_li___ic_hierarchy_show_less.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/res/drawable/pluto_li___ic_hierarchy_show_more.xml b/pluto-plugins/plugins/layout-inspector/lib/src/main/res/drawable/pluto_li___ic_hierarchy_show_more.xml
new file mode 100644
index 000000000..5aeb81e1b
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/res/drawable/pluto_li___ic_hierarchy_show_more.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/res/drawable/pluto_li___ic_hierarchy_target_view_indicator.xml b/pluto-plugins/plugins/layout-inspector/lib/src/main/res/drawable/pluto_li___ic_hierarchy_target_view_indicator.xml
new file mode 100644
index 000000000..cf80092f5
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/res/drawable/pluto_li___ic_hierarchy_target_view_indicator.xml
@@ -0,0 +1,8 @@
+
+
+
+
\ No newline at end of file
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/res/drawable/pluto_li___ic_plugin_logo.xml b/pluto-plugins/plugins/layout-inspector/lib/src/main/res/drawable/pluto_li___ic_plugin_logo.xml
new file mode 100644
index 000000000..b0e3adf6e
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/res/drawable/pluto_li___ic_plugin_logo.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/pluto/src/main/res/drawable/pluto___ic_share.xml b/pluto-plugins/plugins/layout-inspector/lib/src/main/res/drawable/pluto_li___ic_share.xml
similarity index 100%
rename from pluto/src/main/res/drawable/pluto___ic_share.xml
rename to pluto-plugins/plugins/layout-inspector/lib/src/main/res/drawable/pluto_li___ic_share.xml
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/res/drawable/pluto_li___ic_view_hierarchy.xml b/pluto-plugins/plugins/layout-inspector/lib/src/main/res/drawable/pluto_li___ic_view_hierarchy.xml
new file mode 100644
index 000000000..ee90a145a
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/res/drawable/pluto_li___ic_view_hierarchy.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/res/drawable/pluto_li___item_divider.xml b/pluto-plugins/plugins/layout-inspector/lib/src/main/res/drawable/pluto_li___item_divider.xml
new file mode 100644
index 000000000..8e82856f1
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/res/drawable/pluto_li___item_divider.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/res/layout/pluto_li___bs_container_preview_panel.xml b/pluto-plugins/plugins/layout-inspector/lib/src/main/res/layout/pluto_li___bs_container_preview_panel.xml
new file mode 100644
index 000000000..9b9af5b67
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/res/layout/pluto_li___bs_container_preview_panel.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/res/layout/pluto_li___controls_widget.xml b/pluto-plugins/plugins/layout-inspector/lib/src/main/res/layout/pluto_li___controls_widget.xml
new file mode 100644
index 000000000..1c42ca22c
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/res/layout/pluto_li___controls_widget.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/res/layout/pluto_li___fragment_base.xml b/pluto-plugins/plugins/layout-inspector/lib/src/main/res/layout/pluto_li___fragment_base.xml
new file mode 100644
index 000000000..a70b98a87
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/res/layout/pluto_li___fragment_base.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/res/layout/pluto_li___fragment_view_attr.xml b/pluto-plugins/plugins/layout-inspector/lib/src/main/res/layout/pluto_li___fragment_view_attr.xml
new file mode 100644
index 000000000..3473ab9fa
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/res/layout/pluto_li___fragment_view_attr.xml
@@ -0,0 +1,92 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/res/layout/pluto_li___fragment_view_hierarchy.xml b/pluto-plugins/plugins/layout-inspector/lib/src/main/res/layout/pluto_li___fragment_view_hierarchy.xml
new file mode 100644
index 000000000..7b04363e8
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/res/layout/pluto_li___fragment_view_hierarchy.xml
@@ -0,0 +1,94 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/res/layout/pluto_li___fragment_view_info.xml b/pluto-plugins/plugins/layout-inspector/lib/src/main/res/layout/pluto_li___fragment_view_info.xml
new file mode 100644
index 000000000..c13e703ef
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/res/layout/pluto_li___fragment_view_info.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/res/layout/pluto_li___hint_fragment.xml b/pluto-plugins/plugins/layout-inspector/lib/src/main/res/layout/pluto_li___hint_fragment.xml
new file mode 100644
index 000000000..39b7c3d03
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/res/layout/pluto_li___hint_fragment.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/res/layout/pluto_li___item_control_cta.xml b/pluto-plugins/plugins/layout-inspector/lib/src/main/res/layout/pluto_li___item_control_cta.xml
new file mode 100644
index 000000000..5956fd8fc
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/res/layout/pluto_li___item_control_cta.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/res/layout/pluto_li___item_hint.xml b/pluto-plugins/plugins/layout-inspector/lib/src/main/res/layout/pluto_li___item_hint.xml
new file mode 100644
index 000000000..f7c1c7bd0
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/res/layout/pluto_li___item_hint.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/res/layout/pluto_li___item_view_attr.xml b/pluto-plugins/plugins/layout-inspector/lib/src/main/res/layout/pluto_li___item_view_attr.xml
new file mode 100644
index 000000000..e001265cf
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/res/layout/pluto_li___item_view_attr.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/res/layout/pluto_li___item_view_attr_title.xml b/pluto-plugins/plugins/layout-inspector/lib/src/main/res/layout/pluto_li___item_view_attr_title.xml
new file mode 100644
index 000000000..268bf9239
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/res/layout/pluto_li___item_view_attr_title.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/res/layout/pluto_li___item_view_hierarchy.xml b/pluto-plugins/plugins/layout-inspector/lib/src/main/res/layout/pluto_li___item_view_hierarchy.xml
new file mode 100644
index 000000000..84a28b18b
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/res/layout/pluto_li___item_view_hierarchy.xml
@@ -0,0 +1,74 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/res/layout/pluto_li___params_preview_panel.xml b/pluto-plugins/plugins/layout-inspector/lib/src/main/res/layout/pluto_li___params_preview_panel.xml
new file mode 100644
index 000000000..a8360deb5
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/res/layout/pluto_li___params_preview_panel.xml
@@ -0,0 +1,112 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/res/menu/pluto_li___menu_hierarchy_options.xml b/pluto-plugins/plugins/layout-inspector/lib/src/main/res/menu/pluto_li___menu_hierarchy_options.xml
new file mode 100644
index 000000000..d3844535a
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/res/menu/pluto_li___menu_hierarchy_options.xml
@@ -0,0 +1,9 @@
+
+
\ No newline at end of file
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/res/navigation/pluto_li___navigation.xml b/pluto-plugins/plugins/layout-inspector/lib/src/main/res/navigation/pluto_li___navigation.xml
new file mode 100644
index 000000000..0bac4c985
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/res/navigation/pluto_li___navigation.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/res/values/ids.xml b/pluto-plugins/plugins/layout-inspector/lib/src/main/res/values/ids.xml
new file mode 100644
index 000000000..3a0cf1ffb
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/res/values/ids.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/res/values/strings.xml b/pluto-plugins/plugins/layout-inspector/lib/src/main/res/values/strings.xml
new file mode 100644
index 000000000..43e8a44c7
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/res/values/strings.xml
@@ -0,0 +1,17 @@
+
+
+ Layout Inspector
+ How to use
+ Click on view to select, click again to cancel selection.
+ Select a view, then long press anywhere to start moving the selected view
+ View Attributes
+ See in View Hierarchy
+ View Hierarchy
+ Close
+ See Hint
+ Open Screen Hierarchy
+ Move Controls to Right
+ Move Controls to Left
+ Inspect View
+ Show Attributes
+
\ No newline at end of file
diff --git a/pluto-plugins/plugins/layout-inspector/lib/src/main/res/values/styles.xml b/pluto-plugins/plugins/layout-inspector/lib/src/main/res/values/styles.xml
new file mode 100644
index 000000000..0b7c0acfc
--- /dev/null
+++ b/pluto-plugins/plugins/layout-inspector/lib/src/main/res/values/styles.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pluto-plugins/plugins/logger/README.md b/pluto-plugins/plugins/logger/README.md
new file mode 100644
index 000000000..da8ac4500
--- /dev/null
+++ b/pluto-plugins/plugins/logger/README.md
@@ -0,0 +1,70 @@
+## Integrate Logger Plugin in your application
+
+[Logger Demo.webm](https://github.com/user-attachments/assets/81b74c84-393c-4cda-a95f-2070d2cb4853)
+
+### Add Gradle Dependencies
+Pluto Logger is distributed through [***mavenCentral***](https://central.sonatype.com/artifact/com.androidpluto.plugins/logger). To use it, you need to add the following Gradle dependency to your build.gradle file of you android app module.
+
+> Note: add the `no-op` variant to isolate the plugin from release builds.
+```groovy
+dependencies {
+ debugImplementation "com.androidpluto.plugins:logger:$plutoVersion"
+ releaseImplementation "com.androidpluto.plugins:logger-no-op:$plutoVersion"
+}
+```
+
+
+### Install plugin to Pluto
+
+Now to start using the plugin, add it to Pluto
+```kotlin
+Pluto.Installer(this)
+ .addPlugin(PlutoLoggerPlugin())
+ .install()
+```
+
+
+### Add Pluto Logs
+
+Pluto allows you to log and persist the user journey through the app, and help debug them without any need to connect to Logcat.
+
+- **with PlutoLog**
+```kotlin
+PlutoLog.event("analytics", eventName, HashMap(attributes))
+PlutoLog.d("debug_log", "button clicked")
+PlutoLog.e("error_log", "api call failed with http_status 400")
+PlutoLog.w("warning_log", "warning log")
+PlutoLog.i("info_log", "api call completed")
+```
+
+- **with Timber**
+```kotlin
+Timber.tag("analytics").event(eventName, HashMap(attributes))
+Timber.tag("debug_log").d("button clicked")
+Timber.tag("error_log").e(NullPointerException("demo"), "api call failed with http_status 400")
+Timber.tag("warning_log").w(NullPointerException("demo"), "warning log")
+Timber.i("api call completed")
+```
+Install Pluto as a Tree for Timber:
+```
+Timber.plant(PlutoTimberTree());
+```
+
+But if you are connected to Logcat, PlutoLogs behave similar to Log class, with an improvement to tag the method and file name also. In Logcat, PlutoLogs will look like the following.
+```
+D/onClick(MainActivity.kt:40) | debug_log: button clicked
+E/onFailure(NetworkManager.kt:17) | error_log: api call falied with http_status 400
+```
+
+
+π You are all done!
+
+Now re-build and run your app and open Pluto, you will see the Logger plugin installed.
+
+
+
+### Open Plugin view programmatically
+To open Logger plugin screen via code, use this
+```kotlin
+Pluto.open(PlutoLoggerPlugin.ID)
+```
diff --git a/pluto-plugins/plugins/logger/lib-no-op/.gitignore b/pluto-plugins/plugins/logger/lib-no-op/.gitignore
new file mode 100644
index 000000000..42afabfd2
--- /dev/null
+++ b/pluto-plugins/plugins/logger/lib-no-op/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/pluto-plugins/plugins/logger/lib-no-op/build.gradle.kts b/pluto-plugins/plugins/logger/lib-no-op/build.gradle.kts
new file mode 100644
index 000000000..b94d835f9
--- /dev/null
+++ b/pluto-plugins/plugins/logger/lib-no-op/build.gradle.kts
@@ -0,0 +1,93 @@
+import com.vanniktech.maven.publish.SonatypeHost
+
+plugins {
+ alias(libs.plugins.android.library)
+ alias(libs.plugins.kotlin.android)
+
+ alias(libs.plugins.maven.publish)
+}
+
+val version = Versioning.loadVersioningData()
+val verCode = version["code"] as Int
+val verPublish = version["publish"] as String
+val verGitSHA = version["gitSha"] as String
+
+android {
+ namespace = "com.pluto.plugins.logger"
+
+ compileSdk = libs.versions.compileSdk.get().toInt()
+ buildToolsVersion = libs.versions.buildTools.get()
+
+ buildFeatures {
+ viewBinding = true
+ }
+
+ defaultConfig {
+ minSdk = libs.versions.minSdk.get().toInt()
+ }
+
+ buildTypes {
+ getByName("release") {
+// isDebuggable = true
+ isMinifyEnabled = false
+ isShrinkResources = false
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.toVersion(libs.versions.java.get())
+ targetCompatibility = JavaVersion.toVersion(libs.versions.java.get())
+ }
+
+ kotlinOptions {
+ jvmTarget = libs.versions.java.get()
+ }
+
+ lint {
+ abortOnError = false
+ targetSdk = libs.versions.targetSdk.get().toInt()
+ }
+}
+
+extra["PUBLISH_GROUP_ID"] = "com.androidpluto.plugins"
+extra["PUBLISH_ARTIFACT_ID"] = "logger-no-op"
+extra["PUBLISH_ARTIFACT_NAME"] = "Android Pluto Logger Plugin"
+extra["PUBLISH_ARTIFACT_DESCRIPTION"] = "Plugin to monitor logs in Android Pluto"
+
+mavenPublishing {
+ coordinates(
+ groupId = extra["PUBLISH_GROUP_ID"] as String,
+ artifactId = extra["PUBLISH_ARTIFACT_ID"] as String,
+ version = verPublish
+ )
+ pom {
+ name.set(extra["PUBLISH_ARTIFACT_NAME"] as String)
+ description.set(extra["PUBLISH_ARTIFACT_DESCRIPTION"] as String)
+ inceptionYear.set(project.findProperty("pom.inceptionYear") as? String)
+ url.set(project.findProperty("pom.url") as? String)
+ licenses {
+ license {
+ name.set(project.findProperty("pom.license.name") as? String)
+ url.set(project.findProperty("pom.license.url") as? String)
+ }
+ }
+ developers {
+ developer {
+ id.set(project.findProperty("pom.developer.id") as? String)
+ name.set(project.findProperty("pom.developer.name") as? String)
+ email.set(project.findProperty("pom.developer.email") as? String)
+ }
+ }
+ scm {
+ connection.set(project.findProperty("pom.scm.connection") as? String)
+ developerConnection.set(project.findProperty("pom.scm.developerConnection") as? String)
+ url.set(project.findProperty("pom.scm.url") as? String)
+ }
+ }
+ publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL)
+ signAllPublications()
+}
+
+dependencies {
+ api(libs.timber)
+}
diff --git a/pluto-plugins/plugins/logger/lib-no-op/src/main/AndroidManifest.xml b/pluto-plugins/plugins/logger/lib-no-op/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..44008a433
--- /dev/null
+++ b/pluto-plugins/plugins/logger/lib-no-op/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/pluto-plugins/plugins/logger/lib-no-op/src/main/java/com/pluto/plugins/logger/PlutoLog.kt b/pluto-plugins/plugins/logger/lib-no-op/src/main/java/com/pluto/plugins/logger/PlutoLog.kt
new file mode 100644
index 000000000..106999052
--- /dev/null
+++ b/pluto-plugins/plugins/logger/lib-no-op/src/main/java/com/pluto/plugins/logger/PlutoLog.kt
@@ -0,0 +1,32 @@
+package com.pluto.plugins.logger
+
+import androidx.annotation.Keep
+
+@Keep
+@SuppressWarnings("UnusedPrivateMember", "EmptyFunctionBlock")
+class PlutoLog private constructor() {
+
+ companion object {
+
+ @JvmStatic
+ fun v(tag: String, message: String, tr: Throwable? = null) {}
+
+ @JvmStatic
+ fun d(tag: String, message: String, tr: Throwable? = null) {}
+
+ @JvmStatic
+ fun i(tag: String, message: String, tr: Throwable? = null) {}
+
+ @JvmStatic
+ fun w(tag: String, message: String, tr: Throwable? = null) {}
+
+ @JvmStatic
+ fun e(tag: String, message: String, tr: Throwable? = null) {}
+
+ @JvmStatic
+ fun wtf(tag: String, message: String, tr: Throwable? = null) {}
+
+ @JvmStatic
+ fun event(tag: String, event: String, attributes: HashMap? = null) {}
+ }
+}
diff --git a/pluto-plugins/plugins/logger/lib-no-op/src/main/java/com/pluto/plugins/logger/PlutoLoggerPlugin.kt b/pluto-plugins/plugins/logger/lib-no-op/src/main/java/com/pluto/plugins/logger/PlutoLoggerPlugin.kt
new file mode 100644
index 000000000..5f47ee964
--- /dev/null
+++ b/pluto-plugins/plugins/logger/lib-no-op/src/main/java/com/pluto/plugins/logger/PlutoLoggerPlugin.kt
@@ -0,0 +1,8 @@
+package com.pluto.plugins.logger
+
+@SuppressWarnings("UnusedPrivateMember")
+class PlutoLoggerPlugin @JvmOverloads constructor(identifier: String = ID) {
+ companion object {
+ const val ID = "logger"
+ }
+}
diff --git a/pluto-plugins/plugins/logger/lib-no-op/src/main/java/com/pluto/plugins/logger/PlutoTimberTree.kt b/pluto-plugins/plugins/logger/lib-no-op/src/main/java/com/pluto/plugins/logger/PlutoTimberTree.kt
new file mode 100644
index 000000000..49957456b
--- /dev/null
+++ b/pluto-plugins/plugins/logger/lib-no-op/src/main/java/com/pluto/plugins/logger/PlutoTimberTree.kt
@@ -0,0 +1,11 @@
+package com.pluto.plugins.logger
+
+import timber.log.Timber
+
+class PlutoTimberTree : Timber.Tree() {
+
+ override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {}
+}
+
+@SuppressWarnings("UnusedPrivateMember", "EmptyFunctionBlock")
+fun Timber.Tree.event(event: String, attr: HashMap? = null) {}
diff --git a/pluto-plugins/plugins/logger/lib/.gitignore b/pluto-plugins/plugins/logger/lib/.gitignore
new file mode 100644
index 000000000..42afabfd2
--- /dev/null
+++ b/pluto-plugins/plugins/logger/lib/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/pluto-plugins/plugins/logger/lib/build.gradle.kts b/pluto-plugins/plugins/logger/lib/build.gradle.kts
new file mode 100644
index 000000000..a1d09d092
--- /dev/null
+++ b/pluto-plugins/plugins/logger/lib/build.gradle.kts
@@ -0,0 +1,107 @@
+import com.vanniktech.maven.publish.SonatypeHost
+
+plugins {
+ alias(libs.plugins.android.library)
+ alias(libs.plugins.kotlin.android)
+ alias(libs.plugins.kotlin.parcelize)
+ alias(libs.plugins.ksp)
+ alias(libs.plugins.maven.publish)
+}
+
+val version = Versioning.loadVersioningData()
+val verCode = version["code"] as Int
+val verPublish = version["publish"] as String
+val verGitSHA = version["gitSha"] as String
+
+android {
+ resourcePrefix = "pluto_logger___"
+ namespace = "com.pluto.plugins.logger"
+
+ compileSdk = libs.versions.compileSdk.get().toInt()
+ buildToolsVersion = libs.versions.buildTools.get()
+
+ buildFeatures {
+ buildConfig = true
+ viewBinding = true
+ }
+
+ defaultConfig {
+ minSdk = libs.versions.minSdk.get().toInt()
+
+ buildConfigField("String", "VERSION_NAME", "\"$verPublish\"")
+ buildConfigField("long", "VERSION_CODE", "$verCode")
+ buildConfigField("String", "GIT_SHA", "\"$verGitSHA\"")
+ }
+
+ buildTypes {
+ getByName("release") {
+// isDebuggable = true
+ isMinifyEnabled = false
+ isShrinkResources = false
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.toVersion(libs.versions.java.get())
+ targetCompatibility = JavaVersion.toVersion(libs.versions.java.get())
+ }
+
+ kotlinOptions {
+ jvmTarget = libs.versions.java.get()
+ }
+
+ lint {
+ abortOnError = false
+ targetSdk = libs.versions.targetSdk.get().toInt()
+ }
+}
+
+extra["PUBLISH_GROUP_ID"] = "com.androidpluto.plugins"
+extra["PUBLISH_ARTIFACT_ID"] = "logger"
+extra["PUBLISH_ARTIFACT_NAME"] = "Android Pluto Logger Plugin"
+extra["PUBLISH_ARTIFACT_DESCRIPTION"] = "Plugin to monitor logs in Android Pluto"
+
+mavenPublishing {
+ coordinates(
+ groupId = extra["PUBLISH_GROUP_ID"] as String,
+ artifactId = extra["PUBLISH_ARTIFACT_ID"] as String,
+ version = verPublish
+ )
+ pom {
+ name.set(extra["PUBLISH_ARTIFACT_NAME"] as String)
+ description.set(extra["PUBLISH_ARTIFACT_DESCRIPTION"] as String)
+ inceptionYear.set(project.findProperty("pom.inceptionYear") as? String)
+ url.set(project.findProperty("pom.url") as? String)
+ licenses {
+ license {
+ name.set(project.findProperty("pom.license.name") as? String)
+ url.set(project.findProperty("pom.license.url") as? String)
+ }
+ }
+ developers {
+ developer {
+ id.set(project.findProperty("pom.developer.id") as? String)
+ name.set(project.findProperty("pom.developer.name") as? String)
+ email.set(project.findProperty("pom.developer.email") as? String)
+ }
+ }
+ scm {
+ connection.set(project.findProperty("pom.scm.connection") as? String)
+ developerConnection.set(project.findProperty("pom.scm.developerConnection") as? String)
+ url.set(project.findProperty("pom.scm.url") as? String)
+ }
+ }
+ publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL)
+ signAllPublications()
+}
+
+dependencies {
+ implementation(project(":pluto-plugins:base:lib"))
+ implementation(libs.androidx.navigation.ui)
+ api(libs.timber)
+ implementation(libs.moshi)
+ ksp(libs.moshi.codegen)
+
+ implementation(libs.room)
+ ksp(libs.room.compiler)
+}
diff --git a/pluto-plugins/plugins/logger/lib/src/main/AndroidManifest.xml b/pluto-plugins/plugins/logger/lib/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..44008a433
--- /dev/null
+++ b/pluto-plugins/plugins/logger/lib/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/pluto-plugins/plugins/logger/lib/src/main/java/com/pluto/plugins/logger/FilterViewModel.kt b/pluto-plugins/plugins/logger/lib/src/main/java/com/pluto/plugins/logger/FilterViewModel.kt
new file mode 100644
index 000000000..b503db8f9
--- /dev/null
+++ b/pluto-plugins/plugins/logger/lib/src/main/java/com/pluto/plugins/logger/FilterViewModel.kt
@@ -0,0 +1,119 @@
+package com.pluto.plugins.logger
+
+import android.app.Application
+import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MediatorLiveData
+import androidx.lifecycle.MutableLiveData
+import com.pluto.plugins.logger.internal.LogTimeStamp
+import com.pluto.plugins.logger.internal.LogType
+import com.pluto.plugins.logger.internal.Session
+import com.pluto.utilities.selector.SelectorOption
+
+internal class FilterViewModel(application: Application) : AndroidViewModel(application) {
+ val selectedFiltersList: LiveData>
+ get() = _selectedFiltersList
+ private val _selectedFiltersList = MutableLiveData>()
+
+ val selectedTimeStamp: LiveData
+ get() = _selectedTimeStamp
+ private val _selectedTimeStamp = MutableLiveData()
+
+ val searchTextLogger: LiveData
+ get() = _searchTextLogger
+ private val _searchTextLogger = MutableLiveData()
+
+ private val preferences = Preferences(application)
+ private val logTypes = listOf(
+ LogType("debug"),
+ LogType("verbose"),
+ LogType("error"),
+ LogType("info"),
+ LogType("event")
+ )
+
+ private val timeStamps = listOf(
+ LogTimeStamp(1),
+ LogTimeStamp(5),
+ LogTimeStamp(10),
+ LogTimeStamp(Integer.MIN_VALUE, true)
+ )
+
+ val isTriggerSearch: LiveData
+ get() = _isTriggerSearch
+ private val _isTriggerSearch = MediatorLiveData()
+ val isFilterApplied: LiveData
+ get() = _isFilterApplied
+ private val _isFilterApplied = MediatorLiveData()
+
+ val isFilterVisible: LiveData
+ get() = _isFilterVisible
+ private val _isFilterVisible = MediatorLiveData()
+
+ init {
+
+ _isTriggerSearch.addSource(_selectedFiltersList) { _isTriggerSearch.postValue(true) }
+ _isTriggerSearch.addSource(_searchTextLogger) { _isTriggerSearch.postValue(true) }
+ _isTriggerSearch.addSource(_selectedTimeStamp) { _isTriggerSearch.postValue(true) }
+ _searchTextLogger.postValue(Session.loggerSearchText)
+ _selectedFiltersList.postValue(preferences.selectedFilterLogType)
+ _selectedTimeStamp.postValue(preferences.selectedFilterTime)
+ _isFilterApplied.addSource(_selectedFiltersList) {
+ if (it.isNotEmpty() || getSelectedTimeStamp().timeStamp != 0) {
+ _isFilterApplied.postValue(true)
+ } else {
+ _isFilterApplied.postValue(false)
+ }
+ }
+ _isFilterApplied.addSource(_selectedTimeStamp) {
+ if (it.timeStamp != 0 || getSelectedFilters().isNotEmpty()) {
+ _isFilterApplied.postValue(true)
+ } else {
+ _isFilterApplied.postValue(false)
+ }
+ }
+ _isFilterVisible.postValue(false)
+ }
+
+ fun getLogTypes(): List {
+ return logTypes
+ }
+
+ fun getTimeStamps(): List {
+ return timeStamps
+ }
+
+ fun getSelectedFilters(): List {
+ return selectedFiltersList.value ?: emptyList()
+ }
+
+ fun getSelectedTimeStamp(): LogTimeStamp {
+ return selectedTimeStamp.value ?: LogTimeStamp(0, false)
+ }
+
+ fun updateSearchText(searchText: String) {
+ _searchTextLogger.postValue(searchText)
+ Session.loggerSearchText = searchText
+ }
+
+ fun setSelectedFiltersLogType(logTypeList: ArrayList) {
+ _selectedFiltersList.postValue(logTypeList)
+ preferences.selectedFilterLogType = logTypeList
+ }
+ fun setSelectedFilterTimeStamp(logTimeStamp: LogTimeStamp) {
+ _selectedTimeStamp.postValue(logTimeStamp)
+ preferences.selectedFilterTime = logTimeStamp
+ }
+ fun getSearchText(): String {
+ return searchTextLogger.value ?: ""
+ }
+
+ fun toggleFilterViewVisibility() {
+ _isFilterVisible.postValue(_isFilterVisible.value?.not())
+ }
+ fun clearFilters() {
+ preferences.clearFilters()
+ _selectedFiltersList.postValue(emptyList())
+ _selectedTimeStamp.postValue(LogTimeStamp(0, false))
+ }
+}
diff --git a/pluto-plugins/plugins/logger/lib/src/main/java/com/pluto/plugins/logger/PlutoLog.kt b/pluto-plugins/plugins/logger/lib/src/main/java/com/pluto/plugins/logger/PlutoLog.kt
new file mode 100644
index 000000000..e6d104e66
--- /dev/null
+++ b/pluto-plugins/plugins/logger/lib/src/main/java/com/pluto/plugins/logger/PlutoLog.kt
@@ -0,0 +1,64 @@
+package com.pluto.plugins.logger
+
+import android.util.Log
+import androidx.annotation.Keep
+import com.pluto.plugins.logger.internal.LogsProcessor.Companion.process
+import com.pluto.plugins.logger.internal.LogsProcessor.Companion.processEvent
+import com.pluto.plugins.logger.internal.LogsProcessor.Companion.stackTraceElement
+
+@Keep
+class PlutoLog private constructor() {
+
+ companion object {
+
+ @JvmStatic
+ @JvmOverloads
+ fun v(tag: String, message: String, tr: Throwable? = null) {
+ process(Log.VERBOSE, tag, message, tr, Thread.currentThread().getStackTraceElement())
+ }
+
+ @JvmStatic
+ @JvmOverloads
+ fun d(tag: String, message: String, tr: Throwable? = null) {
+ process(Log.DEBUG, tag, message, tr, Thread.currentThread().getStackTraceElement())
+ }
+
+ @JvmStatic
+ @JvmOverloads
+ fun i(tag: String, message: String, tr: Throwable? = null) {
+ process(Log.INFO, tag, message, tr, Thread.currentThread().getStackTraceElement())
+ }
+
+ @JvmStatic
+ @JvmOverloads
+ fun w(tag: String, message: String, tr: Throwable? = null) {
+ process(Log.WARN, tag, message, tr, Thread.currentThread().getStackTraceElement())
+ }
+
+ @JvmStatic
+ @JvmOverloads
+ fun e(tag: String, message: String, tr: Throwable? = null) {
+ process(Log.ERROR, tag, message, tr, Thread.currentThread().getStackTraceElement())
+ }
+
+ @JvmStatic
+ @JvmOverloads
+ fun wtf(tag: String, message: String, tr: Throwable? = null) {
+ process(Log.ASSERT, tag, message, tr, Thread.currentThread().getStackTraceElement())
+ }
+
+ @JvmStatic
+ @JvmOverloads
+ fun event(tag: String, event: String, attributes: HashMap? = null) {
+ processEvent(tag, event, attributes, Thread.currentThread().getStackTraceElement())
+ }
+
+ private fun Thread.getStackTraceElement(): StackTraceElement {
+ val index = if (name == "main") MAIN_THREAD_INDEX else DAEMON_THREAD_INDEX
+ return stackTraceElement(index)
+ }
+
+ private const val MAIN_THREAD_INDEX = 6
+ private const val DAEMON_THREAD_INDEX = 5
+ }
+}
diff --git a/pluto-plugins/plugins/logger/lib/src/main/java/com/pluto/plugins/logger/PlutoLoggerPlugin.kt b/pluto-plugins/plugins/logger/lib/src/main/java/com/pluto/plugins/logger/PlutoLoggerPlugin.kt
new file mode 100644
index 000000000..69f8f257d
--- /dev/null
+++ b/pluto-plugins/plugins/logger/lib/src/main/java/com/pluto/plugins/logger/PlutoLoggerPlugin.kt
@@ -0,0 +1,46 @@
+package com.pluto.plugins.logger
+
+import androidx.fragment.app.Fragment
+import com.pluto.plugin.DeveloperDetails
+import com.pluto.plugin.Plugin
+import com.pluto.plugin.PluginConfiguration
+import com.pluto.plugins.logger.internal.LogsFragment
+import com.pluto.plugins.logger.internal.persistence.LogDBHandler
+
+class PlutoLoggerPlugin() : Plugin(ID) {
+
+ @SuppressWarnings("UnusedPrivateMember")
+ @Deprecated("Use the default constructor PlutoLoggerPlugin() instead.")
+ constructor(identifier: String) : this()
+
+ private val settingsPreferences by lazy { Preferences(application) }
+
+ override fun getConfig(): PluginConfiguration = PluginConfiguration(
+ name = context.getString(R.string.pluto_logger___plugin_name),
+ icon = R.drawable.pluto_logger___ic_logger_icon,
+ version = BuildConfig.VERSION_NAME
+ )
+
+ override fun getView(): Fragment = LogsFragment()
+
+ override fun getDeveloperDetails(): DeveloperDetails {
+ return DeveloperDetails(
+ website = "https://androidpluto.com",
+ vcsLink = "https://github.com/androidPluto/pluto",
+ twitter = "https://twitter.com/android_pluto"
+ )
+ }
+
+ override fun onPluginInstalled() {
+ LogDBHandler.initialize(context)
+ }
+
+ override fun onPluginDataCleared() {
+ LogDBHandler.flush()
+ settingsPreferences.clearFilters()
+ }
+
+ companion object {
+ const val ID = "logger"
+ }
+}
diff --git a/pluto-plugins/plugins/logger/lib/src/main/java/com/pluto/plugins/logger/PlutoTimberTree.kt b/pluto-plugins/plugins/logger/lib/src/main/java/com/pluto/plugins/logger/PlutoTimberTree.kt
new file mode 100644
index 000000000..a15628eec
--- /dev/null
+++ b/pluto-plugins/plugins/logger/lib/src/main/java/com/pluto/plugins/logger/PlutoTimberTree.kt
@@ -0,0 +1,77 @@
+package com.pluto.plugins.logger
+
+import com.pluto.plugins.logger.internal.LogsProcessor
+import com.pluto.plugins.logger.internal.LogsProcessor.Companion.LOG_EVENT_PRIORITY
+import com.pluto.plugins.logger.internal.LogsProcessor.Companion.stackTraceElement
+import com.squareup.moshi.JsonAdapter
+import com.squareup.moshi.Moshi
+import com.squareup.moshi.Types
+import java.math.BigDecimal
+import timber.log.Timber
+
+class PlutoTimberTree : Timber.Tree() {
+
+ override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
+ if (priority == LOG_EVENT_PRIORITY) {
+ val eventData = eventExtractor(message)
+ LogsProcessor.processEvent(tag ?: "pluto_timber", eventData.first, eventData.second, Thread.currentThread().getStackTraceElement())
+ } else {
+ LogsProcessor.process(priority, tag ?: "pluto_timber", messageExtractor(message), t, Thread.currentThread().getStackTraceElement())
+ }
+ }
+
+ @SuppressWarnings("NestedBlockDepth")
+ private fun eventExtractor(message: String): Pair?> {
+ val length = message.length
+ var newline = message.indexOf('\t', 0)
+ newline = if (newline != -1) newline else length
+ val end = newline.coerceAtMost(MAX_LOG_LENGTH)
+
+ val event = message.substring(0, end)
+ val attrString = if (end < length) {
+ val moshi = Moshi.Builder().build()
+ val moshiAdapter: JsonAdapter