diff --git a/.circleci/config.yml b/.circleci/config.yml
index 3c2169bbe..17e9ee42f 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -1,28 +1,32 @@
version: 2.1
-executors:
- default-executor:
- docker:
- - image: circleci/android:api-30-ndk
- resource_class: large
-
- environment:
- _JAVA_OPTIONS: "-Xmx1500m -XX:+UnlockExperimentalVMOptions -XX:+UseContainerSupport -XX:ParallelGCThreads=2 -XX:ConcGCThreads=2 -XX:ParallelGCThreads=2 -Djava.util.concurrent.ForkJoinPool.common.parallelism=2"
- TERM: 'dumb'
-
+orbs:
+ android: circleci/android@2.3.0
jobs:
snapshot:
- executor: default-executor
+ environment:
+ TERM: 'dumb'
+ executor:
+ name: android/android-machine
+ tag: 2023.04.1
+ resource-class: large
steps:
- checkout
+ - android/restore-gradle-cache:
+ cache-prefix: v1a
- run:
name: install retry
command: scripts/install-retry.sh
- run:
- name: build and deploy
+ name: build
+ command: |
+ yes | sdkmanager "platforms;android-33" || true
+ /tmp/retry -m 3 ./gradlew :android:assembleRelease --info
+ - run:
+ name: deploy snapshot
command: |
- yes | sdkmanager "platforms;android-27" || true
- /tmp/retry -m 3 ./gradlew :android:assembleRelease
/tmp/retry -m 3 scripts/publish-android-snapshot.sh
+ - android/save-gradle-cache:
+ cache-prefix: v1a
workflows:
version: 2
build-and-deploy:
diff --git a/.github/workflows/android-sample.yml b/.github/workflows/android-sample.yml
index 3b12b5744..2bd77507a 100644
--- a/.github/workflows/android-sample.yml
+++ b/.github/workflows/android-sample.yml
@@ -1,5 +1,5 @@
name: Build Android Sample App
-
+# This action runs on 'git push' and PRs
on: [push, pull_request]
jobs:
@@ -7,14 +7,15 @@ jobs:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3.5.3
- name: set up JDK
- uses: actions/setup-java@v1
+ uses: actions/setup-java@v3.11.0
with:
- java-version: 11
+ distribution: 'temurin'
+ java-version: 17
- name: Compute build cache
run: ./scripts/checksum-android.sh checksum-android.txt
- - uses: actions/cache@v2
+ - uses: actions/cache@v3.3.1
with:
path: |
~/.gradle/caches/modules-*
@@ -30,7 +31,7 @@ jobs:
- name: Build remaining artifacts with Gradle
run: ./gradlew assembleDebug
- name: upload artifact
- uses: actions/upload-artifact@v1
+ uses: actions/upload-artifact@v3.1.2
with:
name: sample-app.apk
path: android/sample/build/outputs/apk/debug/sample-debug.apk
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index fab1b7535..286f826d8 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -1,4 +1,5 @@
name: Docs
+# This action runs on push to 'main'
on:
push:
branches:
@@ -9,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v2.3.1
+ uses: actions/checkout@v3.5.3
with:
persist-credentials: false
@@ -19,10 +20,12 @@ jobs:
yarn build
working-directory: website/
- - name: Deploy
- uses: JamesIves/github-pages-deploy-action@4.1.3
+ - name: Deploy to GitHub Pages
+ uses: JamesIves/github-pages-deploy-action@v4.4.2
with:
branch: gh-pages
folder: website/build
clean: true
+ clean-exclude: |
+ .circleci
commit-message: "[ci skip] Deploying documentation update"
diff --git a/.github/workflows/iOS-Sample.yml b/.github/workflows/iOS-Sample.yml
index 8ff94db75..b530a1fab 100644
--- a/.github/workflows/iOS-Sample.yml
+++ b/.github/workflows/iOS-Sample.yml
@@ -1,4 +1,5 @@
name: Build iOS apps
+# This action runs on 'git push' and PRs to below specified paths
on:
push:
paths:
@@ -21,7 +22,7 @@ jobs:
working-directory: iOS/Sample
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3.5.3
- name: Install dependencies
run: pod install --repo-update
- name: Build Sample app
@@ -36,7 +37,7 @@ jobs:
working-directory: iOS/SampleSwift
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3.5.3
- name: Install dependencies
run: pod install --repo-update
- name: Build SampleSwift app
@@ -51,7 +52,7 @@ jobs:
working-directory: iOS/Tutorial
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3.5.3
- name: Install dependencies
run: pod install --repo-update
- name: Build Tutorial app
diff --git a/.github/workflows/iOS-dependent-pod-lint.yml b/.github/workflows/iOS-dependent-pod-lint.yml
index ee8afac0f..9a6316a3d 100644
--- a/.github/workflows/iOS-dependent-pod-lint.yml
+++ b/.github/workflows/iOS-dependent-pod-lint.yml
@@ -1,4 +1,5 @@
name: Validate Dependent Podspecs
+# This action runs on push and PRs to below specified paths
on:
push:
paths:
@@ -14,7 +15,7 @@ jobs:
run:
working-directory: iOS/Podspecs
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3.5.3
- name: Install Dependences
run: pod repo update
- name: Lint Folly
@@ -26,7 +27,7 @@ jobs:
run:
working-directory: iOS/Podspecs
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3.5.3
- name: Install Dependences
run: pod repo update
- name: Lint Peertalk
diff --git a/.github/workflows/iOS-pod-lint.yml b/.github/workflows/iOS-pod-lint.yml
index 104895799..9ed21c858 100644
--- a/.github/workflows/iOS-pod-lint.yml
+++ b/.github/workflows/iOS-pod-lint.yml
@@ -1,4 +1,5 @@
name: Validate Podspecs
+# This action runs on 'git push' and PRs to below specified paths.
on:
push:
paths:
@@ -19,11 +20,11 @@ jobs:
lint-flipperkit_fbdefines_pod:
runs-on: macos-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3.5.3
- name: Install Dependences
run: pod repo update
- name: Lint FlipperKit/FBDefines
- uses: nick-invision/retry@v2.6.0
+ uses: nick-fields/retry@v2.8.3
with:
timeout_minutes: 60
max_attempts: 3
@@ -32,11 +33,11 @@ jobs:
lint-flipperkit_core_pod:
runs-on: macos-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3.5.3
- name: Install Dependences
run: pod repo update
- name: Lint FlipperKit/Core
- uses: nick-invision/retry@v2.6.0
+ uses: nick-fields/retry@v2.8.3
with:
timeout_minutes: 60
max_attempts: 3
@@ -45,11 +46,11 @@ jobs:
lint-flipperkit_cppbridge_pod:
runs-on: macos-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3.5.3
- name: Install Dependences
run: pod repo update
- name: Lint FlipperKit/CppBridge
- uses: nick-invision/retry@v2.6.0
+ uses: nick-fields/retry@v2.8.3
with:
timeout_minutes: 60
max_attempts: 3
@@ -58,11 +59,11 @@ jobs:
lint-flipperkit_dynamic_convert_pod:
runs-on: macos-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3.5.3
- name: Install Dependences
run: pod repo update
- name: Lint FlipperKit/FBCxxFollyDynamicConvert
- uses: nick-invision/retry@v2.6.0
+ uses: nick-fields/retry@v2.8.3
with:
timeout_minutes: 60
max_attempts: 3
@@ -71,11 +72,11 @@ jobs:
lint-flipperkit_port_forwarding_pod:
runs-on: macos-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3.5.3
- name: Install Dependences
run: pod repo update
- name: Lint FlipperKit/FKPortForwarding
- uses: nick-invision/retry@v2.6.0
+ uses: nick-fields/retry@v2.8.3
with:
timeout_minutes: 60
max_attempts: 3
@@ -84,11 +85,11 @@ jobs:
lint-flipperkit_layout_highlight_pod:
runs-on: macos-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3.5.3
- name: Install Dependences
run: pod repo update
- name: Lint FlipperKit/FlipperKitHighlightOverlay
- uses: nick-invision/retry@v2.6.0
+ uses: nick-fields/retry@v2.8.3
with:
timeout_minutes: 60
max_attempts: 3
@@ -97,11 +98,11 @@ jobs:
lint-flipperkit_layout_text_searchable_pod:
runs-on: macos-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3.5.3
- name: Install Dependences
run: pod repo update
- name: Lint FlipperKit/FlipperKitHighlightOverlay
- uses: nick-invision/retry@v2.6.0
+ uses: nick-fields/retry@v2.8.3
with:
timeout_minutes: 60
max_attempts: 3
@@ -110,11 +111,11 @@ jobs:
lint-flipperkit_layout_helpers_pod:
runs-on: macos-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3.5.3
- name: Install Dependences
run: pod repo update
- name: Lint FlipperKit/FlipperKitLayoutHelpers
- uses: nick-invision/retry@v2.6.0
+ uses: nick-fields/retry@v2.8.3
with:
timeout_minutes: 60
max_attempts: 3
@@ -123,11 +124,11 @@ jobs:
lint-flipperkit_layout_ios_descriptors_pod:
runs-on: macos-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3.5.3
- name: Install Dependences
run: pod repo update
- name: Lint FlipperKit/FlipperKitLayoutIOSDescriptors
- uses: nick-invision/retry@v2.6.0
+ uses: nick-fields/retry@v2.8.3
with:
timeout_minutes: 60
max_attempts: 3
@@ -136,11 +137,11 @@ jobs:
lint-flipperkit_layout_plugin_pod:
runs-on: macos-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3.5.3
- name: Install Dependences
run: pod repo update
- name: Lint FlipperKit/FlipperKitLayoutPlugin
- uses: nick-invision/retry@v2.6.0
+ uses: nick-fields/retry@v2.8.3
with:
timeout_minutes: 60
max_attempts: 3
@@ -149,11 +150,11 @@ jobs:
lint-flipperkit_layout_ck_plugin_pod:
runs-on: macos-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3.5.3
- name: Install Dependences
run: pod repo update
- name: Lint FlipperKit/FlipperKitLayoutComponentKitSupport
- uses: nick-invision/retry@v2.6.0
+ uses: nick-fields/retry@v2.8.3
with:
timeout_minutes: 60
max_attempts: 3
@@ -162,11 +163,11 @@ jobs:
lint-flipperkit_network_pod:
runs-on: macos-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3.5.3
- name: Install Dependences
run: pod repo update
- name: Lint FlipperKit/FlipperKitNetworkPlugin
- uses: nick-invision/retry@v2.6.0
+ uses: nick-fields/retry@v2.8.3
with:
timeout_minutes: 60
max_attempts: 3
@@ -175,11 +176,11 @@ jobs:
lint-flipperkit_skiosnetwork_pod:
runs-on: macos-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3.5.3
- name: Install Dependences
run: pod repo update
- name: Lint FlipperKit/SKIOSNetworkPlugin
- uses: nick-invision/retry@v2.6.0
+ uses: nick-fields/retry@v2.8.3
with:
timeout_minutes: 60
max_attempts: 3
@@ -188,11 +189,11 @@ jobs:
lint-flipperkit_user_default_pod:
runs-on: macos-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3.5.3
- name: Install Dependences
run: pod repo update
- name: Lint FlipperKit/FlipperKitUserDefaultsPlugin
- uses: nick-invision/retry@v2.6.0
+ uses: nick-fields/retry@v2.8.3
with:
timeout_minutes: 60
max_attempts: 3
@@ -201,11 +202,11 @@ jobs:
lint-flipperkit_example_plugin_pod:
runs-on: macos-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3.5.3
- name: Install Dependences
run: pod repo update
- name: Lint FlipperKit/FlipperKitExamplePlugin
- uses: nick-invision/retry@v2.6.0
+ uses: nick-fields/retry@v2.8.3
with:
timeout_minutes: 60
max_attempts: 3
@@ -214,11 +215,11 @@ jobs:
lint-flipperkit_react_plugin_pod:
runs-on: macos-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3.5.3
- name: Install Dependences
run: pod repo update
- name: Lint FlipperKit/FlipperKitReactPlugin
- uses: nick-invision/retry@v2.6.0
+ uses: nick-fields/retry@v2.8.3
with:
timeout_minutes: 60
max_attempts: 3
@@ -227,11 +228,11 @@ jobs:
lint-flipper_pod:
runs-on: macos-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3.5.3
- name: Install Dependences
run: pod repo update
- name: Lint Flipper
- uses: nick-invision/retry@v2.6.0
+ uses: nick-fields/retry@v2.8.3
with:
timeout_minutes: 60
max_attempts: 3
diff --git a/.github/workflows/issues.yml b/.github/workflows/issues.yml
index fb47d142d..b2d14ae0a 100644
--- a/.github/workflows/issues.yml
+++ b/.github/workflows/issues.yml
@@ -1,17 +1,26 @@
name: Label issues
-
# This workflow is triggered on issue comments.
on:
issue_comment:
types: created
+permissions:
+ contents: read
+
jobs:
applyNeedsAttentionLabel:
+ permissions:
+ contents: read # for actions/checkout to fetch code
+ issues: write # for hramos/needs-attention to label issues
name: Apply Needs Attention Label
runs-on: ubuntu-latest
+ if: github.repository == 'facebook/flipper'
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
- name: Apply Needs Attention Label
- uses: hramos/needs-attention@v1
+ uses: hramos/needs-attention@v2.0.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
+ id: needs-attention
+ - name: Result
+ run: echo '${{ steps.needs-attention.outputs.result }}'
diff --git a/.github/workflows/js.yml b/.github/workflows/js.yml
index 7a8e5c82f..d573e1c69 100644
--- a/.github/workflows/js.yml
+++ b/.github/workflows/js.yml
@@ -1,5 +1,5 @@
name: js-flipper
-
+# This action runs on 'git push' and PRs
on: [push, pull_request]
jobs:
@@ -9,12 +9,12 @@ jobs:
working-directory: js/js-flipper
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
- - uses: actions/setup-node@v2
+ - uses: actions/checkout@v3.5.3
+ - uses: actions/setup-node@v3.6.0
with:
- node-version: '16.x'
+ node-version: '18.x'
- name: yarn install (with retry)
- uses: nick-invision/retry@v2.6.0
+ uses: nick-fields/retry@v2.8.3
with:
command: cd js/js-flipper && yarn
timeout_minutes: 30
@@ -30,12 +30,12 @@ jobs:
working-directory: js/react-flipper-example
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
- - uses: actions/setup-node@v2
+ - uses: actions/checkout@v3.5.3
+ - uses: actions/setup-node@v3.6.0
with:
- node-version: '16.x'
+ node-version: '18.x'
- name: js-flipper - yarn install (with retry)
- uses: nick-invision/retry@v2.6.0
+ uses: nick-fields/retry@v2.8.3
with:
command: cd js/js-flipper && yarn
timeout_minutes: 30
@@ -44,7 +44,7 @@ jobs:
run: yarn build
working-directory: js/js-flipper
- name: yarn install (with retry)
- uses: nick-invision/retry@v2.6.0
+ uses: nick-fields/retry@v2.8.3
with:
command: cd js/react-flipper-example && yarn
timeout_minutes: 30
diff --git a/.github/workflows/nodejs-doctor.yml b/.github/workflows/nodejs-doctor.yml
index 7dab0b5b7..0ca49a429 100644
--- a/.github/workflows/nodejs-doctor.yml
+++ b/.github/workflows/nodejs-doctor.yml
@@ -1,20 +1,17 @@
name: Doctor Node CI
-
+# This action runs on 'git push' and PRs
on: [push, pull_request]
jobs:
build:
-
runs-on: 'ubuntu-latest'
-
env:
doctor-directory: ./desktop/doctor
-
steps:
- - uses: actions/checkout@v2
- - uses: actions/setup-node@v1
+ - uses: actions/checkout@v3.5.3
+ - uses: actions/setup-node@v3.6.0
with:
- node-version: '16.x'
+ node-version: '18.x'
- name: install
working-directory: ${{env.doctor-directory}}
run: yarn
diff --git a/.github/workflows/nodejs-pkg.yml b/.github/workflows/nodejs-pkg.yml
index c9e4166e9..22ed081d1 100644
--- a/.github/workflows/nodejs-pkg.yml
+++ b/.github/workflows/nodejs-pkg.yml
@@ -1,20 +1,17 @@
name: PKG Node CI
-
+# This actions runs on 'git push' and PRs
on: [push, pull_request]
jobs:
build:
-
runs-on: ubuntu-latest
-
env:
pkg-directory: ./desktop/pkg
-
steps:
- - uses: actions/checkout@v2
- - uses: actions/setup-node@v1
+ - uses: actions/checkout@v3.5.3
+ - uses: actions/setup-node@v3.6.0
with:
- node-version: '16.x'
+ node-version: '18.x'
- name: install
working-directory: ${{env.pkg-directory}}
run: yarn
diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml
index 9e31450c6..e16f8d179 100644
--- a/.github/workflows/nodejs.yml
+++ b/.github/workflows/nodejs.yml
@@ -1,25 +1,21 @@
name: Desktop Node CI
-
+# This action run on 'git push' and PRs
on: [push, pull_request]
jobs:
build:
-
runs-on: ${{ matrix.os }}
-
env:
desktop-directory: ./desktop
-
strategy:
fail-fast: false
matrix:
- node-version: [14.x]
+ node-version: [18.x]
os: ['ubuntu-latest', 'windows-latest', 'macos-latest']
-
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3.5.3
- name: Use Node.js ${{ matrix.node-version }}
- uses: actions/setup-node@v2
+ uses: actions/setup-node@v3.6.0
with:
node-version: ${{ matrix.node-version }}
- name: Get yarn cache directory path
@@ -64,25 +60,25 @@ jobs:
run: yarn build --win
working-directory: ${{env.desktop-directory}}
- name: upload linux artifact
- uses: actions/upload-artifact@v1
+ uses: actions/upload-artifact@v3.1.2
if: matrix.os == 'ubuntu-latest'
with:
name: Flipper-linux.zip
path: dist/Flipper-linux.zip
- name: upload windows artifact
- uses: actions/upload-artifact@v1
+ uses: actions/upload-artifact@v3.1.2
if: matrix.os == 'windows-latest'
with:
name: Flipper-win.zip
path: dist/Flipper-win.zip
- name: upload mac zip artifact
- uses: actions/upload-artifact@v1
+ uses: actions/upload-artifact@v3.1.2
if: matrix.os == 'macos-latest'
with:
name: Flipper-mac.zip
path: dist/Flipper-mac.zip
- name: upload mac dmg artifact
- uses: actions/upload-artifact@v1
+ uses: actions/upload-artifact@v3.1.2
if: matrix.os == 'macos-latest'
with:
name: Flipper-mac.dmg
diff --git a/.github/workflows/packer.yml b/.github/workflows/packer.yml
index ab4e87bc7..6706b55ef 100644
--- a/.github/workflows/packer.yml
+++ b/.github/workflows/packer.yml
@@ -1,5 +1,5 @@
name: Packer
-
+# This action runs on 'git push' and PRs
on: [push, pull_request]
jobs:
@@ -7,11 +7,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3.5.3
- name: Setup toolchain
- uses: actions-rs/toolchain@v1
- with:
- toolchain: 1.48.0
+ uses: dtolnay/rust-toolchain@stable
+ # The selection of Rust toolchain is made based on the particular '@rev'
+ # of this Action being requested. For example "dtolnay/rust-toolchain@nightly"
+ # pulls in the nightly Rust toolchain, while "dtolnay/rust-toolchain@1.42.0"
+ # pulls in '1.42.0'.
- name: Test
run: cd packer && cargo test
- name: Format
diff --git a/.github/workflows/publish-android.yml b/.github/workflows/publish-android.yml
index 69f59297a..3c8c288b4 100644
--- a/.github/workflows/publish-android.yml
+++ b/.github/workflows/publish-android.yml
@@ -1,5 +1,5 @@
name: Publish Android
-
+# This action runs on 'git push tag v*' and worflow dispatch as specified below
on:
push:
tags:
@@ -10,25 +10,23 @@ on:
description: "Tag to upload artifacts to"
required: false
-
jobs:
build:
-
runs-on: ubuntu-latest
-
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3.5.3
- name: set up JDK
- uses: actions/setup-java@v1
+ uses: actions/setup-java@v3.11.0
with:
- java-version: 11
+ distribution: 'temurin'
+ java-version: 17
- name: Write GPG Sec Ring
run: echo '${{ secrets.GPG_KEY_CONTENTS }}' | base64 -d > /tmp/secring.gpg
- name: Update gradle.properties
- run: echo -e "signing.secretKeyRingFile=/tmp/secring.gpg\nsigning.keyId=${{ secrets.SIGNING_KEY_ID }}\nsigning.password=${{ secrets.SIGNING_PASSWORD }}\nmavenCentralPassword=${{ secrets.SONATYPE_NEXUS_PASSWORD }}\nmavenCentralUsername=${{ secrets.SONATYPE_NEXUS_USERNAME }}" >> gradle.properties
+ run: echo -e "signing.secretKeyRingFile=/tmp/secring.gpg\nsigning.keyId=${{ secrets.SIGNING_KEY_ID }}\nsigning.password=${{ secrets.SIGNING_PASSWORD }}\nmavenCentralPassword=${{ secrets.SONATYPE_NEXUS_PASSWORD }}\nmavenCentralUsername=${{ secrets.SONATYPE_NEXUS_USERNAME }}\nSONATYPE_HOST=DEFAULT\nRELEASE_SIGNING_ENABLED=true\nSONATYPE_AUTOMATIC_RELEASE=true" >> gradle.properties
- name: Compute build cache
run: ./scripts/checksum-android.sh checksum-android.txt
- - uses: actions/cache@v2
+ - uses: actions/cache@v3.3.1
with:
path: |
~/.gradle/caches/modules-*
@@ -38,9 +36,7 @@ jobs:
- name: Build artifacts
run: ./gradlew :sample:assembleDebug :sample:assembleRelease && ./gradlew :android:assembleRelease
- name: Upload Archives
- run: ./gradlew publish -info --no-parallel --no-daemon
- - name: Release and close
- run: ./gradlew closeAndReleaseRepository
+ run: ./gradlew publish -info
- name: Clean secrets
if: always()
run: rm /tmp/secring.gpg
@@ -48,7 +44,7 @@ jobs:
run: mv android/sample/build/outputs/apk/debug/sample-debug.apk SampleApp-android.apk
- name: Attach sample APK to release
if: ${{ github.event.inputs.tag != '' }}
- uses: passy/github-upload-release-artifacts-action@v2.2.2
+ uses: aigoncharov/github-upload-release-artifacts-action@2.2.3
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
diff --git a/.github/workflows/publish-npm.yml b/.github/workflows/publish-npm.yml
index 8847a8582..4a9a6c72c 100644
--- a/.github/workflows/publish-npm.yml
+++ b/.github/workflows/publish-npm.yml
@@ -1,4 +1,5 @@
name: Publish NPM
+# This action runs on 'git push tag v*'
on:
push:
tags:
@@ -13,10 +14,10 @@ jobs:
publish:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
- - uses: actions/setup-node@v1
+ - uses: actions/checkout@v3.5.3
+ - uses: actions/setup-node@v3.6.0
with:
- node-version: '16.x'
+ node-version: '18.x'
- name: Install
run: yarn
- name: Set versions
diff --git a/.github/workflows/publish-pods.yml b/.github/workflows/publish-pods.yml
index 2ef2e2b6e..b0279ac0f 100644
--- a/.github/workflows/publish-pods.yml
+++ b/.github/workflows/publish-pods.yml
@@ -1,4 +1,5 @@
name: Publish Pods
+# This action runs on 'git push tag v*'
on:
push:
tags:
@@ -9,7 +10,7 @@ jobs:
publish_flipper_pod:
runs-on: macos-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3.5.3
- name: Install Dependences
run: pod repo update
@@ -29,7 +30,7 @@ jobs:
needs: publish_flipper_pod
runs-on: macos-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3.5.3
- name: Install Dependences
run: pod repo update
@@ -49,7 +50,7 @@ jobs:
needs: publish_flipperkit_pod
runs-on: macos-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3.5.3
- name: Update Flipper's Podspec
run: ./scripts/update-pod-versions.sh ./ ./Flipper.podspec
@@ -123,7 +124,7 @@ jobs:
git branch
- name: Create PR to Update Podfile.lock
- uses: peter-evans/create-pull-request@v3
+ uses: peter-evans/create-pull-request@v5.0.2
with:
title: "Automated: Update Podfile.lock"
body: |
diff --git a/.github/workflows/react-native-example.yml b/.github/workflows/react-native-example.yml
index 03ab0071f..b56ddabaa 100644
--- a/.github/workflows/react-native-example.yml
+++ b/.github/workflows/react-native-example.yml
@@ -1,5 +1,7 @@
name: Build React Native example
+# This action runs on 'git push' and PRs
on: [push, pull_request]
+
jobs:
build-react-native-example-ios:
runs-on: macos-latest
@@ -7,11 +9,11 @@ jobs:
run:
working-directory: react-native/ReactNativeFlipperExample
steps:
- - uses: actions/checkout@v2
- - uses: actions/setup-node@v2.1.5
+ - uses: actions/checkout@v3.5.3
+ - uses: actions/setup-node@v3.6.0
with:
- node-version: 14.x
- - uses: maxim-lobanov/setup-cocoapods@v1
+ node-version: '18.x'
+ - uses: maxim-lobanov/setup-cocoapods@v1.3.0
with:
# Path to Podfile.lock file to determine Cocoapods version
# n.b. doesn't seem to respect cwd:
@@ -34,17 +36,18 @@ jobs:
run:
working-directory: react-native/ReactNativeFlipperExample
steps:
- - uses: actions/checkout@v2
- - uses: actions/setup-node@v2.1.5
+ - uses: actions/checkout@v3.5.3
+ - uses: actions/setup-node@v3.6.0
with:
- node-version: 14.x
+ node-version: '18.x'
- name: set up JDK
- uses: actions/setup-java@v1
+ uses: actions/setup-java@v3.11.0
with:
- java-version: 11
+ distribution: 'temurin'
+ java-version: '11'
- name: Compute build cache
run: ${GITHUB_WORKSPACE}/scripts/checksum-android.sh checksum-android.txt
- - uses: actions/cache@v2
+ - uses: actions/cache@v3.3.1
with:
path: |
~/.gradle/caches/modules-*
@@ -67,14 +70,16 @@ jobs:
run:
working-directory: react-native/ReactNativeFlipperExample
steps:
- - uses: actions/checkout@v2
- - uses: actions/setup-node@v2.1.5
+ - uses: actions/checkout@v3.5.3
+ - uses: actions/setup-node@v3.6.0
with:
- node-version: 14.x
- - uses: nuget/setup-nuget@v1
+ node-version: '18.x'
+ - name: Set up NuGet.exe
+ - uses: NuGet/setup-nuget@v1.2.0
with:
nuget-version: '5.x'
- - uses: microsoft/setup-msbuild@v1.1
+ - name: Add msbuild to PATH
+ - uses: microsoft/setup-msbuild@v1.3.1
- name: Gather environment info
run: npx envinfo
- name: Install vcpkg packages
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 183421857..3a1ff8029 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -1,4 +1,5 @@
name: Release
+# This action runs on push to 'main' and below specified paths
on:
push:
branches:
@@ -11,12 +12,13 @@ jobs:
runs-on: ubuntu-latest
outputs:
tag: ${{ steps.tag-version-commit.outputs.tag }}
+
steps:
- uses: passy/extract-version-commit@v1.0.0
id: extract-version-commit
with:
version_regex: '^Flipper Release: v([0-9]+\.[0-9]+\.[0-9]+)(?:\n|$)'
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3.5.3
if: ${{ steps.extract-version-commit.outputs.commit != ''}}
with:
ref: ${{ steps.extract-version-commit.outputs.commit }}
@@ -31,12 +33,12 @@ jobs:
version_assertion_command: 'grep -q "\"version\": \"$version\"" desktop/package.json'
- name: Create release
if: ${{ steps.tag-version-commit.outputs.tag != '' }}
- uses: actions/create-release@v1
+ uses: softprops/action-gh-release@v0.1.15
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ steps.tag-version-commit.outputs.tag }}
- release_name: ${{ steps.tag-version-commit.outputs.tag }}
+ name: ${{ steps.tag-version-commit.outputs.tag }}
body: |
See https://github.com/facebook/flipper/blob/main/desktop/static/CHANGELOG.md
for full notes.
@@ -51,30 +53,65 @@ jobs:
desktop-directory: ./desktop
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3.5.3
with:
ref: ${{ needs.release.outputs.tag }}
- - uses: actions/setup-node@v1
+ - uses: actions/setup-node@v3.6.0
with:
- node-version: '16.x'
+ node-version: '18.x'
- name: Install
- uses: nick-invision/retry@v2.6.0
+ uses: nick-fields/retry@v2.8.3
with:
timeout_minutes: 10
max_attempts: 3
command: cd ${{env.desktop-directory}} && yarn
- name: Build
- uses: nick-invision/retry@v2.6.0
+ uses: nick-fields/retry@v2.8.3
with:
timeout_minutes: 30
max_attempts: 3
command: cd ${{env.desktop-directory}} && yarn build --mac --mac-dmg
- name: Upload
- uses: actions/upload-artifact@v1
+ uses: actions/upload-artifact@v3.1.2
with:
name: 'Flipper-mac.dmg'
path: 'dist/Flipper-mac.dmg'
+ build-server-mac:
+ needs:
+ - release
+ runs-on: macos-latest
+ env:
+ desktop-directory: ./desktop
+
+ steps:
+ - uses: actions/checkout@v3.5.3
+ with:
+ ref: ${{ needs.release.outputs.tag }}
+ - uses: actions/setup-node@v3.6.0
+ with:
+ node-version: '18.x'
+ - name: Install
+ uses: nick-invision/retry@v2.0.0
+ with:
+ timeout_minutes: 10
+ max_attempts: 3
+ command: cd ${{env.desktop-directory}} && yarn
+ - name: Build
+ run: cd ${{env.desktop-directory}} && yarn build:flipper-server --mac --dmg
+ - name: List dist artifacts
+ run: ls -l dist/
+ - name: Upload x86-64
+ uses: actions/upload-artifact@v3.1.2
+ with:
+ name: 'Flipper-server-mac-x64.dmg'
+ path: 'dist/Flipper-server-mac-x64.dmg'
+ - name: Upload aarch64
+ uses: actions/upload-artifact@v3.1.2
+ with:
+ name: 'Flipper-server-mac-aarch64.dmg'
+ path: 'dist/Flipper-server-mac-aarch64.dmg'
+
build-linux:
needs:
- release
@@ -83,26 +120,26 @@ jobs:
desktop-directory: ./desktop
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3.5.3
with:
ref: ${{ needs.release.outputs.tag }}
- - uses: actions/setup-node@v1
+ - uses: actions/setup-node@v3.6.0
with:
- node-version: '16.x'
+ node-version: '18.x'
- name: Install
- uses: nick-invision/retry@v2.6.0
+ uses: nick-fields/retry@v2.8.3
with:
timeout_minutes: 10
max_attempts: 3
command: cd ${{env.desktop-directory}} && yarn
- name: Build
- uses: nick-invision/retry@v2.6.0
+ uses: nick-fields/retry@v2.8.3
with:
timeout_minutes: 30
max_attempts: 3
command: cd ${{env.desktop-directory}} && yarn build --linux
- name: Upload Linux
- uses: actions/upload-artifact@v1
+ uses: actions/upload-artifact@v3.1.2
with:
name: 'Flipper-linux.zip'
path: 'dist/Flipper-linux.zip'
@@ -115,28 +152,28 @@ jobs:
desktop-directory: ./desktop
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3.5.3
with:
ref: ${{ needs.release.outputs.tag }}
- - uses: actions/setup-node@v1
+ - uses: actions/setup-node@v3.6.0
with:
- node-version: '16.x'
+ node-version: '18.x'
- name: Install
- uses: nick-invision/retry@v2.6.0
+ uses: nick-fields/retry@v2.8.3
with:
timeout_minutes: 10
max_attempts: 3
shell: pwsh
command: cd ${{env.desktop-directory}}; yarn
- name: Build
- uses: nick-invision/retry@v2.6.0
+ uses: nick-fields/retry@v2.8.3
with:
timeout_minutes: 30
max_attempts: 3
shell: pwsh
command: cd ${{env.desktop-directory}}; yarn build --win
- name: Upload Windows
- uses: actions/upload-artifact@v1
+ uses: actions/upload-artifact@v3.1.2
with:
name: 'Flipper-win.zip'
path: 'dist/Flipper-win.zip'
@@ -149,12 +186,12 @@ jobs:
desktop-directory: ./desktop
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3.5.3
with:
ref: ${{ needs.release.outputs.tag }}
- - uses: actions/setup-node@v1
+ - uses: actions/setup-node@v3.6.0
with:
- node-version: '16.x'
+ node-version: '18.x'
- name: Install
uses: nick-invision/retry@v2.0.0
with:
@@ -166,7 +203,7 @@ jobs:
- name: List dist artifacts
run: ls -l dist/
- name: Upload flipper-server
- uses: actions/upload-artifact@v2
+ uses: actions/upload-artifact@v3.1.2
with:
name: 'flipper-server.tgz'
path: 'dist/flipper-server.tgz'
@@ -176,17 +213,33 @@ jobs:
- build-win
- build-linux
- build-mac
+ - build-server-mac
- build-flipper-server
- release
runs-on: ubuntu-latest
steps:
+ - uses: actions/checkout@v3.5.3
+ with:
+ ref: ${{ needs.release.outputs.tag }}
- name: Download Mac
if: ${{ needs.release.outputs.tag != '' }}
uses: actions/download-artifact@v1
with:
name: 'Flipper-mac.dmg'
path: 'Flipper-mac.dmg'
+ - name: Download Flipper Server x86-64
+ if: ${{ needs.release.outputs.tag != '' }}
+ uses: actions/download-artifact@v1
+ with:
+ name: 'Flipper-server-mac-x64.dmg'
+ path: 'Flipper-server-mac-x64.dmg'
+ - name: Download Flipper Server aarch64
+ if: ${{ needs.release.outputs.tag != '' }}
+ uses: actions/download-artifact@v1
+ with:
+ name: 'Flipper-server-mac-aarch64.dmg'
+ path: 'Flipper-server-mac-aarch64.dmg'
- name: Download Linux
if: ${{ needs.release.outputs.tag != '' }}
uses: actions/download-artifact@v1
@@ -207,12 +260,12 @@ jobs:
path: 'flipper-server.tgz'
- name: GitHub Upload Release Artifacts
if: ${{ needs.release.outputs.tag != '' }}
- uses: passy/github-upload-release-artifacts-action@v2.2.2
+ uses: aigoncharov/github-upload-release-artifacts-action@2.2.3
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
created_tag: ${{ needs.release.outputs.tag }}
- args: Flipper-mac.dmg/Flipper-mac.dmg Flipper-linux.zip/Flipper-linux.zip Flipper-win.zip/Flipper-win.zip flipper-server.tgz/flipper-server.tgz
+ args: Flipper-mac.dmg/Flipper-mac.dmg Flipper-linux.zip/Flipper-linux.zip Flipper-win.zip/Flipper-win.zip flipper-server.tgz/flipper-server.tgz Flipper-server-mac-x64.dmg/Flipper-server-mac-x64.dmg Flipper-server-mac-aarch64.dmg/Flipper-server-mac-aarch64.dmg
- name: Set up npm token
run: echo "//registry.yarnpkg.com/:_authToken=${{ secrets.FLIPPER_NPM_TOKEN }}" >> ~/.npmrc
- name: Publish flipper-server on NPM
@@ -223,7 +276,7 @@ jobs:
yarn publish
- name: Open issue on failure
if: failure()
- uses: JasonEtco/create-an-issue@v2.4.0
+ uses: JasonEtco/create-an-issue@v2.9.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPOSITORY: ${{ github.repository }}
@@ -231,6 +284,7 @@ jobs:
WORKFLOW_NAME: "Publish"
with:
filename: .github/action-failure-template.md
+
dispatch:
needs:
- release
@@ -239,23 +293,20 @@ jobs:
steps:
- name: Publish Workflow Dispatch
if: ${{ needs.release.outputs.tag != '' }}
- uses: benc-uk/workflow-dispatch@v1.1
+ uses: benc-uk/workflow-dispatch@v1.2.2
with:
workflow: Publish Pods
- token: ${{ secrets.PERSONAL_TOKEN }}
ref: ${{ needs.release.outputs.tag }}
- name: Publish NPM
if: ${{ needs.release.outputs.tag != '' }}
- uses: benc-uk/workflow-dispatch@v1.1
+ uses: benc-uk/workflow-dispatch@v1.2.2
with:
workflow: Publish NPM
- token: ${{ secrets.PERSONAL_TOKEN }}
ref: ${{ needs.release.outputs.tag }}
- name: Publish Android
if: ${{ needs.release.outputs.tag != '' }}
- uses: benc-uk/workflow-dispatch@v1.1
+ uses: benc-uk/workflow-dispatch@v1.2.2
with:
workflow: Publish Android
- token: ${{ secrets.PERSONAL_TOKEN }}
ref: ${{ needs.release.outputs.tag }}
inputs: '{"tag": "${{ needs.release.outputs.tag }}"}'
diff --git a/.gitignore b/.gitignore
index 961268360..9a301999a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,10 +8,20 @@ bundle.map
*.bundle.map
.env
desktop-branch-*/
+auth.token
+manifest.json
# conflicts with FB internal infra
.watchmanconfig
+# Don't checkout the yarn/npm lock files that we don't need
+./yarn.lock
+./package-lock.json
+js/yarn.lock
+js/package-lock.json
+react-native/yarn.lock
+react-native/package-lock.json
+
# iOS / Xcode
*.xcworkspace
**/Pods/
@@ -49,3 +59,5 @@ website/src/embedded-pages/docs/plugins/
# Logs
**/*/flipper-server-log.out
+
+*.salive
diff --git a/Flipper.podspec b/Flipper.podspec
index 946e61fe2..2040bbef2 100644
--- a/Flipper.podspec
+++ b/Flipper.podspec
@@ -3,7 +3,7 @@
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.
-flipperkit_version = '0.162.0'
+flipperkit_version = '0.222.0'
Pod::Spec.new do |spec|
spec.name = 'Flipper'
spec.cocoapods_version = '>= 1.10'
diff --git a/FlipperKit.podspec b/FlipperKit.podspec
index 6c3c31960..8ea9f97b5 100644
--- a/FlipperKit.podspec
+++ b/FlipperKit.podspec
@@ -4,8 +4,7 @@
# LICENSE file in the root directory of this source tree.
folly_compiler_flags = '-DDEBUG=1 -DFLIPPER_OSS=1 -DFB_SONARKIT_ENABLED=1 -DFOLLY_HAVE_BACKTRACE=1 -DFOLLY_HAVE_CLOCK_GETTIME=1 -DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -DFOLLY_HAVE_LIBGFLAGS=0 -DFOLLY_HAVE_LIBJEMALLOC=0 -DFOLLY_HAVE_PREADV=0 -DFOLLY_HAVE_PWRITEV=0 -DFOLLY_HAVE_TFO=0 -DFOLLY_USE_SYMBOLIZER=0'
-yogakit_version = '~> 1.18'
-flipperkit_version = '0.162.0'
+flipperkit_version = '0.222.0'
Pod::Spec.new do |spec|
spec.name = 'FlipperKit'
spec.version = flipperkit_version
@@ -17,7 +16,7 @@ Pod::Spec.new do |spec|
spec.source = { :git => 'https://github.com/facebook/flipper.git',
:tag=> "v"+flipperkit_version }
spec.module_name = 'FlipperKit'
- spec.platforms = { :ios => "10.0" }
+ spec.platforms = { :ios => "11.0" }
spec.default_subspecs = "Core"
# This subspec is necessary since FBDefines.h is imported as
@@ -77,7 +76,7 @@ Pod::Spec.new do |spec|
ss.dependency 'FlipperKit/CppBridge'
ss.dependency 'FlipperKit/FKPortForwarding'
ss.dependency 'Flipper', '~>'+flipperkit_version
- ss.dependency 'SocketRocket', '~> 0.6.0'
+ ss.dependency 'SocketRocket', '~> 0.7.0'
ss.compiler_flags = folly_compiler_flags
ss.source_files = 'iOS/FlipperKit/*.{h,m,mm}', 'iOS/FlipperKit/CppBridge/*.{h,mm}'
ss.public_header_files = 'iOS/FlipperKit/**/{FlipperDiagnosticsViewController,FlipperStateUpdateListener,FlipperClient,FlipperPlugin,FlipperConnection,FlipperResponder,SKMacros,FlipperKitCertificateProvider}.h'
@@ -119,8 +118,7 @@ Pod::Spec.new do |spec|
ss.private_header_files = 'iOS/Plugins/FlipperKitPluginUtils/FlipperKitLayoutHelpers/FlipperKitLayoutHelpers/SKObject.h',
'iOS/Plugins/FlipperKitPluginUtils/FlipperKitLayoutHelpers/FlipperKitLayoutHelpers/UIColor+SKSonarValueCoder.h',
'iOS/Plugins/FlipperKitPluginUtils/FlipperKitLayoutHelpers/FlipperKitLayoutHelpers/utils/SKObjectHash.h',
- 'iOS/Plugins/FlipperKitPluginUtils/FlipperKitLayoutHelpers/FlipperKitLayoutHelpers/utils/SKSwizzle.h',
- 'iOS/Plugins/FlipperKitPluginUtils/FlipperKitLayoutHelpers/FlipperKitLayoutHelpers/utils/SKYogaKitHelper.h'
+ 'iOS/Plugins/FlipperKitPluginUtils/FlipperKitLayoutHelpers/FlipperKitLayoutHelpers/utils/SKSwizzle.h'
end
spec.subspec 'FlipperKitLayoutIOSDescriptors' do |ss|
@@ -128,7 +126,6 @@ Pod::Spec.new do |spec|
ss.dependency 'FlipperKit/Core'
ss.dependency 'FlipperKit/FlipperKitHighlightOverlay'
ss.dependency 'FlipperKit/FlipperKitLayoutHelpers'
- ss.dependency 'YogaKit', yogakit_version
ss.compiler_flags = folly_compiler_flags
ss.source_files = 'iOS/Plugins/FlipperKitPluginUtils/FlipperKitLayoutIOSDescriptors/**/*.{h,mm,m}'
end
@@ -140,7 +137,6 @@ Pod::Spec.new do |spec|
ss.dependency 'FlipperKit/FlipperKitHighlightOverlay'
ss.dependency 'FlipperKit/FlipperKitLayoutHelpers'
ss.dependency 'FlipperKit/FlipperKitLayoutIOSDescriptors'
- ss.dependency 'YogaKit', yogakit_version
ss.compiler_flags = folly_compiler_flags
ss.public_header_files = 'iOS/Plugins/FlipperKitLayoutPlugin/FlipperKitLayoutPlugin/FlipperKitLayoutPlugin.h',
'iOS/Plugins/FlipperKitLayoutPlugin/FlipperKitLayoutPlugin/SKDescriptorMapper.h'
@@ -149,11 +145,20 @@ Pod::Spec.new do |spec|
ss.pod_target_xcconfig = { "HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)\"/Headers/Private/FlipperKit/**", "ONLY_ACTIVE_ARCH": "YES" }
end
+ spec.subspec "FlipperKitUIDebuggerPlugin" do |ss|
+ ss.header_dir = "FlipperKitUIDebuggerPlugin"
+ ss.dependency 'FlipperKit/Core'
+ ss.public_header_files = 'iOS/Plugins/FlipperKitUIDebuggerPlugin/FlipperKitUIDebuggerPlugin/FlipperKitUIDebuggerPlugin.h'
+ ss.source_files = 'iOS/Plugins/FlipperKitUIDebuggerPlugin/FlipperKitUIDebuggerPlugin/**/*.{h,cpp,m,mm}'
+ ss.exclude_files = ['iOS/Plugins/FlipperKitUIDebuggerPlugin/fb/*','iOS/Plugins/FlipperKitUIDebuggerPlugin/facebook/*','iOS/Plugins/FlipperKitUIDebuggerPlugin/FlipperKitUIDebuggerPlugin/fb/*' ,'iOS/Plugins/FlipperKitUIDebuggerPlugin/FlipperKitUIDebuggerPlugin/facebook/*']
+ ss.pod_target_xcconfig = { "HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)\"/Headers/Private/FlipperKit/**", "ONLY_ACTIVE_ARCH": "YES" }
+ end
+
spec.subspec "FlipperKitLayoutComponentKitSupport" do |ss|
ss.header_dir = "FlipperKitLayoutComponentKitSupport"
ss.dependency 'FlipperKit/Core'
ss.dependency 'ComponentKit', '0.31'
- ss.dependency 'RenderCore', '0.31' # Pinning it to 0.30, as there won't be any new releases from CK team.
+ ss.dependency 'RenderCore', '0.31'
ss.dependency 'FlipperKit/FlipperKitLayoutPlugin'
ss.dependency 'FlipperKit/FlipperKitLayoutTextSearchable'
ss.dependency 'FlipperKit/FlipperKitHighlightOverlay'
diff --git a/README.md b/README.md
index 77b551455..727eed423 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@
- Flipper (formerly Sonar) is a platform for debugging mobile apps on iOS and Android and, recently, even JS apps in your browser or in Node.js. Visualize, inspect, and control your apps from a simple desktop interface. Use Flipper as is or extend it using the plugin API.
+ Flipper (formerly Sonar) is a platform for debugging mobile apps on iOS and Android and JS apps in your browser or in Node.js. Visualize, inspect, and control your apps from a simple desktop interface. Use Flipper as is or extend it using the plugin API.

@@ -139,6 +139,8 @@ Start up an android emulator and run the following in the project root:
## React Native SDK + Sample app
+> Requires RN 0.69+!
+
```bash
cd react-native/ReactNativeFlipperExample
yarn
diff --git a/android/build.gradle b/android/build.gradle
index 6f789dcbe..763ebccef 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -7,11 +7,10 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
-
-
apply plugin: 'kotlinx-serialization'
android {
+ namespace 'com.facebook.flipper'
compileSdkVersion rootProject.compileSdkVersion
buildToolsVersion rootProject.buildToolsVersion
ndkVersion rootProject.ndkVersion
@@ -32,7 +31,7 @@ android {
externalNativeBuild {
cmake {
arguments '-DANDROID_TOOLCHAIN=clang', '-DANDROID_STL=c++_shared'
- targets 'flipper', 'event_shared', 'event_extra_shared', 'event_core_shared'
+ targets 'flipper'
}
}
}
@@ -49,6 +48,11 @@ android {
}
}
+ compileOptions {
+ targetCompatibility rootProject.javaTargetVersion
+ sourceCompatibility rootProject.javaTargetVersion
+ }
+
buildFeatures {
prefab true
}
@@ -68,6 +72,7 @@ android {
compileOnly deps.proguardAnnotations
implementation deps.kotlinStdLibrary
+ implementation deps.kotlinCoroutinesAndroid
implementation deps.openssl
implementation deps.fbjni
implementation deps.soloader
diff --git a/android/no-op/build.gradle b/android/no-op/build.gradle
index 7301ca418..3caf12b4c 100644
--- a/android/no-op/build.gradle
+++ b/android/no-op/build.gradle
@@ -8,6 +8,7 @@
apply plugin: 'com.android.library'
android {
+ namespace 'com.facebook.flipper.noop'
compileSdkVersion rootProject.compileSdkVersion
buildToolsVersion rootProject.buildToolsVersion
@@ -30,10 +31,3 @@ android {
}
apply plugin: 'com.vanniktech.maven.publish'
-
-task sourcesJar(type: Jar) {
- from android.sourceSets.main.java.srcDirs
- classifier = 'sources'
-}
-
-artifacts.add('archives', sourcesJar)
diff --git a/android/no-op/src/main/AndroidManifest.xml b/android/no-op/src/main/AndroidManifest.xml
index 674594014..260fd956e 100644
--- a/android/no-op/src/main/AndroidManifest.xml
+++ b/android/no-op/src/main/AndroidManifest.xml
@@ -7,5 +7,5 @@
-->
+ package="com.facebook.flipper.noop">
diff --git a/android/plugins/fresco/src/main/AndroidManifest.xml b/android/plugins/fresco/src/main/AndroidManifest.xml
deleted file mode 100644
index 0bafe358f..000000000
--- a/android/plugins/fresco/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
-
diff --git a/android/plugins/fresco/src/main/java/com/facebook/flipper/plugins/fresco/FrescoFlipperDebugPrefHelper.java b/android/plugins/fresco/src/main/java/com/facebook/flipper/plugins/fresco/FrescoFlipperDebugPrefHelper.java
deleted file mode 100644
index bc17585ae..000000000
--- a/android/plugins/fresco/src/main/java/com/facebook/flipper/plugins/fresco/FrescoFlipperDebugPrefHelper.java
+++ /dev/null
@@ -1,21 +0,0 @@
-/*
- * Copyright (c) Meta Platforms, Inc. and affiliates.
- *
- * This source code is licensed under the MIT license found in the
- * LICENSE file in the root directory of this source tree.
- */
-
-package com.facebook.flipper.plugins.fresco;
-
-public interface FrescoFlipperDebugPrefHelper {
-
- interface Listener {
- void onEnabledStatusChanged(boolean enabled);
- }
-
- void setDebugOverlayEnabled(boolean enabled);
-
- boolean isDebugOverlayEnabled();
-
- void setDebugOverlayEnabledListener(Listener l);
-}
diff --git a/android/plugins/fresco/src/main/java/com/facebook/flipper/plugins/fresco/FrescoFlipperPlugin.java b/android/plugins/fresco/src/main/java/com/facebook/flipper/plugins/fresco/FrescoFlipperPlugin.java
deleted file mode 100644
index 0d9c0f31a..000000000
--- a/android/plugins/fresco/src/main/java/com/facebook/flipper/plugins/fresco/FrescoFlipperPlugin.java
+++ /dev/null
@@ -1,651 +0,0 @@
-/*
- * Copyright (c) Meta Platforms, Inc. and affiliates.
- *
- * This source code is licensed under the MIT license found in the
- * LICENSE file in the root directory of this source tree.
- */
-
-package com.facebook.flipper.plugins.fresco;
-
-import android.graphics.Bitmap;
-import android.util.Base64;
-import android.util.Pair;
-import bolts.Continuation;
-import bolts.Task;
-import com.facebook.cache.common.CacheKey;
-import com.facebook.cache.common.SimpleCacheKey;
-import com.facebook.cache.disk.DiskStorage;
-import com.facebook.common.internal.ByteStreams;
-import com.facebook.common.internal.Preconditions;
-import com.facebook.common.internal.Predicate;
-import com.facebook.common.memory.PooledByteBuffer;
-import com.facebook.common.memory.PooledByteBufferInputStream;
-import com.facebook.common.memory.manager.DebugMemoryManager;
-import com.facebook.common.memory.manager.NoOpDebugMemoryManager;
-import com.facebook.common.references.CloseableReference;
-import com.facebook.common.references.SharedReference;
-import com.facebook.drawee.backends.pipeline.Fresco;
-import com.facebook.drawee.backends.pipeline.info.ImageLoadStatus;
-import com.facebook.drawee.backends.pipeline.info.ImageOriginUtils;
-import com.facebook.drawee.backends.pipeline.info.ImagePerfData;
-import com.facebook.drawee.backends.pipeline.info.ImagePerfDataListener;
-import com.facebook.flipper.core.FlipperArray;
-import com.facebook.flipper.core.FlipperConnection;
-import com.facebook.flipper.core.FlipperObject;
-import com.facebook.flipper.core.FlipperReceiver;
-import com.facebook.flipper.core.FlipperResponder;
-import com.facebook.flipper.perflogger.FlipperPerfLogger;
-import com.facebook.flipper.perflogger.NoOpFlipperPerfLogger;
-import com.facebook.flipper.plugins.common.BufferingFlipperPlugin;
-import com.facebook.flipper.plugins.fresco.objecthelper.FlipperObjectHelper;
-import com.facebook.imagepipeline.bitmaps.PlatformBitmapFactory;
-import com.facebook.imagepipeline.cache.CountingMemoryCacheInspector;
-import com.facebook.imagepipeline.cache.CountingMemoryCacheInspector.DumpInfoEntry;
-import com.facebook.imagepipeline.core.ImagePipelineFactory;
-import com.facebook.imagepipeline.debug.CloseableReferenceLeakTracker;
-import com.facebook.imagepipeline.debug.DebugImageTracker;
-import com.facebook.imagepipeline.debug.FlipperImageTracker;
-import com.facebook.imagepipeline.image.CloseableBitmap;
-import com.facebook.imagepipeline.image.CloseableImage;
-import com.facebook.imagepipeline.image.EncodedImage;
-import com.facebook.imageutils.BitmapUtil;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.PrintWriter;
-import java.io.StringWriter;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Set;
-import java.util.concurrent.atomic.AtomicBoolean;
-import javax.annotation.Nullable;
-
-/**
- * Allows Sonar to display the contents of Fresco's caches. This is useful for developers to debug
- * what images are being held in cache as they navigate through their app.
- */
-public class FrescoFlipperPlugin extends BufferingFlipperPlugin
- implements ImagePerfDataListener, CloseableReferenceLeakTracker.Listener {
-
- private static final String FRESCO_EVENT = "events";
- private static final String FRESCO_DEBUGOVERLAY_EVENT = "debug_overlay_event";
- private static final String FRESCO_CLOSEABLE_REFERENCE_LEAK_EVENT =
- "closeable_reference_leak_event";
-
- private static final int BITMAP_PREVIEW_WIDTH = 150;
- private static final int BITMAP_PREVIEW_HEIGHT = 150;
- private static final int BITMAP_SCALING_THRESHOLD_WIDTH = 200;
- private static final int BITMAP_SCALING_THRESHOLD_HEIGHT = 200;
-
- /** Helper for clearing cache. */
- private static final Predicate ALWAYS_TRUE_PREDICATE =
- new Predicate() {
- @Override
- public boolean apply(CacheKey cacheKey) {
- return true;
- }
- };
-
- private final FlipperImageTracker mFlipperImageTracker;
- private final PlatformBitmapFactory mPlatformBitmapFactory;
- @Nullable private final FlipperObjectHelper mSonarObjectHelper;
- private final DebugMemoryManager mMemoryManager;
- private final FlipperPerfLogger mPerfLogger;
- @Nullable private final FrescoFlipperDebugPrefHelper mDebugPrefHelper;
- private final List mEvents = new ArrayList<>();
-
- public FrescoFlipperPlugin(
- DebugImageTracker imageTracker,
- PlatformBitmapFactory bitmapFactory,
- @Nullable FlipperObjectHelper flipperObjectHelper,
- DebugMemoryManager memoryManager,
- FlipperPerfLogger perfLogger,
- @Nullable FrescoFlipperDebugPrefHelper debugPrefHelper,
- @Nullable CloseableReferenceLeakTracker closeableReferenceLeakTracker) {
- mFlipperImageTracker =
- imageTracker instanceof FlipperImageTracker
- ? (FlipperImageTracker) imageTracker
- : new FlipperImageTracker();
- mPlatformBitmapFactory = bitmapFactory;
- mSonarObjectHelper = flipperObjectHelper;
- mMemoryManager = memoryManager;
- mPerfLogger = perfLogger;
- mDebugPrefHelper = debugPrefHelper;
-
- if (closeableReferenceLeakTracker != null) {
- closeableReferenceLeakTracker.setListener(this);
- }
- }
-
- public FrescoFlipperPlugin() {
- this(
- new FlipperImageTracker(),
- Fresco.getImagePipelineFactory().getPlatformBitmapFactory(),
- null,
- new NoOpDebugMemoryManager(),
- new NoOpFlipperPerfLogger(),
- null,
- null);
- }
-
- public FlipperImageTracker getFlipperImageTracker() {
- return mFlipperImageTracker;
- }
-
- @Override
- public String getId() {
- return "Fresco";
- }
-
- @Override
- public void onConnect(FlipperConnection connection) {
- super.onConnect(connection);
- connection.receive(
- "getAllImageEventsInfo",
- new FlipperReceiver() {
- @Override
- public void onReceive(FlipperObject params, FlipperResponder responder) throws Exception {
- if (!ensureFrescoInitialized()) {
- return;
- }
-
- FlipperArray.Builder arrayBuilder = new FlipperArray.Builder();
- for (FlipperObject obj : mEvents) {
- arrayBuilder.put(obj);
- }
- mEvents.clear();
-
- FlipperObject object =
- new FlipperObject.Builder().put("events", arrayBuilder.build()).build();
- responder.success(object);
- }
- });
-
- connection.receive(
- "listImages",
- new FlipperReceiver() {
- @Override
- public void onReceive(FlipperObject params, FlipperResponder responder) throws Exception {
- if (!ensureFrescoInitialized()) {
- return;
- }
-
- mPerfLogger.startMarker("Sonar.Fresco.listImages");
- final boolean showDiskImages = params.getBoolean("showDiskImages");
- final ImagePipelineFactory imagePipelineFactory = Fresco.getImagePipelineFactory();
-
- final CountingMemoryCacheInspector.DumpInfo bitmapMemoryCache =
- new CountingMemoryCacheInspector<>(
- imagePipelineFactory.getBitmapCountingMemoryCache())
- .dumpCacheContent();
- final CountingMemoryCacheInspector.DumpInfo encodedMemoryCache =
- new CountingMemoryCacheInspector<>(
- imagePipelineFactory.getEncodedCountingMemoryCache())
- .dumpCacheContent();
-
- try {
- responder.success(
- getImageList(bitmapMemoryCache, encodedMemoryCache, showDiskImages));
- mPerfLogger.endMarker("Sonar.Fresco.listImages");
- } finally {
- bitmapMemoryCache.release();
- encodedMemoryCache.release();
- }
- }
- });
-
- connection.receive(
- "getImage",
- new FlipperReceiver() {
- @Override
- public void onReceive(FlipperObject params, final FlipperResponder responder)
- throws Exception {
- if (!ensureFrescoInitialized()) {
- return;
- }
-
- mPerfLogger.startMarker("Sonar.Fresco.getImage");
- final String imageId = params.getString("imageId");
- final CacheKey cacheKey = mFlipperImageTracker.getCacheKey(imageId);
- if (cacheKey == null) {
- respondError(responder, "ImageId " + imageId + " was evicted from cache");
- mPerfLogger.cancelMarker("Sonar.Fresco.getImage");
- return;
- }
-
- final ImagePipelineFactory imagePipelineFactory = Fresco.getImagePipelineFactory();
-
- // try to load from bitmap cache
- @Nullable
- CloseableImage closeableImage =
- imagePipelineFactory.getBitmapCountingMemoryCache().inspect(cacheKey);
- if (closeableImage instanceof CloseableBitmap) {
- @Nullable Bitmap bitmap = ((CloseableBitmap) closeableImage).getUnderlyingBitmap();
- if (bitmap != null) {
- loadFromBitmapCache(bitmap, imageId, cacheKey, responder);
- mPerfLogger.endMarker("Sonar.Fresco.getImage");
- return;
- }
- }
-
- // try to load from encoded cache
- PooledByteBuffer encoded =
- imagePipelineFactory.getEncodedCountingMemoryCache().inspect(cacheKey);
- if (encoded != null) {
- loadFromEncodedCache(encoded, imageId, cacheKey, responder);
- mPerfLogger.endMarker("Sonar.Fresco.getImage");
- return;
- }
-
- // try to load from disk
- loadFromDisk(imageId, cacheKey, responder);
- }
-
- private void loadFromBitmapCache(
- final Bitmap bitmap,
- final String imageId,
- final CacheKey cacheKey,
- final FlipperResponder responder) {
- String encodedBitmap = bitmapToBase64Preview(bitmap, mPlatformBitmapFactory);
- responder.success(
- getImageData(
- imageId,
- mFlipperImageTracker.getUriString(cacheKey),
- bitmap.getWidth(),
- bitmap.getHeight(),
- BitmapUtil.getSizeInBytes(bitmap),
- encodedBitmap));
- }
-
- private void loadFromEncodedCache(
- final PooledByteBuffer encoded,
- final String imageId,
- final CacheKey cacheKey,
- final FlipperResponder responder)
- throws Exception {
- byte[] encodedArray = ByteStreams.toByteArray(new PooledByteBufferInputStream(encoded));
- Pair dimensions = BitmapUtil.decodeDimensions(encodedArray);
- if (dimensions == null) {
- respondError(responder, "can not get dimensions withId=" + imageId);
- return;
- }
-
- responder.success(
- getImageData(
- imageId,
- mFlipperImageTracker.getUriString(cacheKey),
- dimensions.first,
- dimensions.second,
- encodedArray.length,
- dataFromEncodedArray(encodedArray)));
- }
-
- private void loadFromDisk(
- final String imageId, final CacheKey cacheKey, final FlipperResponder responder) {
- Task t =
- Fresco.getImagePipelineFactory()
- .getMainBufferedDiskCache()
- .get(cacheKey, new AtomicBoolean(false));
-
- t.continueWith(
- new Continuation() {
- public Void then(Task task) throws Exception {
- if (task.isCancelled() || task.isFaulted()) {
- respondError(responder, "no bitmap withId=" + imageId);
- mPerfLogger.cancelMarker("Sonar.Fresco.getImage");
- return null;
- }
- Preconditions.checkNotNull(task);
- final EncodedImage image = task.getResult();
- try {
- InputStream stream = Preconditions.checkNotNull(image.getInputStream());
- byte[] encodedArray = ByteStreams.toByteArray(stream);
-
- responder.success(
- getImageData(
- imageId,
- Preconditions.checkNotNull(
- mFlipperImageTracker.getLocalPath(cacheKey)),
- image.getWidth(),
- image.getHeight(),
- encodedArray.length,
- dataFromEncodedArray(encodedArray)));
- } finally {
- EncodedImage.closeSafely(image);
- }
- mPerfLogger.endMarker("Sonar.Fresco.getImage");
- return null;
- }
- });
- }
- });
-
- connection.receive(
- "clear",
- new FlipperReceiver() {
- @Override
- public void onReceive(FlipperObject params, FlipperResponder responder) {
- if (!ensureFrescoInitialized()) {
- return;
- }
-
- mPerfLogger.startMarker("Sonar.Fresco.clear");
- final String type = params.getString("type");
- switch (type) {
- case "memory":
- final ImagePipelineFactory imagePipelineFactory = Fresco.getImagePipelineFactory();
- imagePipelineFactory.getBitmapMemoryCache().removeAll(ALWAYS_TRUE_PREDICATE);
- break;
- case "disk":
- Fresco.getImagePipeline().clearDiskCaches();
- break;
- }
- mPerfLogger.endMarker("Sonar.Fresco.clear");
- }
- });
-
- connection.receive(
- "trimMemory",
- new FlipperReceiver() {
- @Override
- public void onReceive(FlipperObject params, FlipperResponder responder) throws Exception {
- if (!ensureFrescoInitialized()) {
- return;
- }
-
- if (mMemoryManager != null) {
- mMemoryManager.trimMemory(
- DebugMemoryManager.ON_SYSTEM_LOW_MEMORY_WHILE_APP_IN_FOREGROUND);
- }
- }
- });
-
- connection.receive(
- "enableDebugOverlay",
- new FlipperReceiver() {
- @Override
- public void onReceive(FlipperObject params, FlipperResponder responder) throws Exception {
- if (!ensureFrescoInitialized()) {
- return;
- }
-
- final boolean enabled = params.getBoolean("enabled");
- if (mDebugPrefHelper != null) {
- mDebugPrefHelper.setDebugOverlayEnabled(enabled);
- }
- }
- });
-
- if (mDebugPrefHelper != null) {
- mDebugPrefHelper.setDebugOverlayEnabledListener(
- new FrescoFlipperDebugPrefHelper.Listener() {
- @Override
- public void onEnabledStatusChanged(boolean enabled) {
- sendDebugOverlayEnabledEvent(enabled);
- }
- });
- sendDebugOverlayEnabledEvent(mDebugPrefHelper.isDebugOverlayEnabled());
- }
- }
-
- private static String dataFromEncodedArray(byte[] encodedArray) {
- return "data:image/jpeg;base64," + Base64.encodeToString(encodedArray, Base64.DEFAULT);
- }
-
- private FlipperObject getImageList(
- final CountingMemoryCacheInspector.DumpInfo bitmapMemoryCache,
- final CountingMemoryCacheInspector.DumpInfo encodedMemoryCache,
- final boolean showDiskImages)
- throws IOException {
- FlipperArray.Builder levelsBuilder =
- new FlipperArray.Builder()
- // bitmap
- .put(getUsedStats("On screen bitmaps", bitmapMemoryCache))
- .put(getCachedStats("Bitmap memory cache", bitmapMemoryCache))
- // encoded
- .put(getUsedStats("Used encoded images", encodedMemoryCache))
- .put(getCachedStats("Cached encoded images", encodedMemoryCache));
- if (showDiskImages) {
- levelsBuilder.put(
- getDiskStats(
- "Disk images",
- Fresco.getImagePipelineFactory().getMainFileCache().getDumpInfo().entries));
- }
-
- return new FlipperObject.Builder().put("levels", levelsBuilder.build()).build();
- }
-
- private FlipperObject getUsedStats(
- final String cacheType, final CountingMemoryCacheInspector.DumpInfo memoryCache) {
- return new FlipperObject.Builder()
- .put("cacheType", cacheType)
- .put("sizeBytes", memoryCache.size - memoryCache.lruSize)
- .put("imageIds", buildImageIdList(memoryCache.sharedEntries))
- .build();
- }
-
- private FlipperObject getCachedStats(
- final String cacheType, final CountingMemoryCacheInspector.DumpInfo memoryCache) {
- return new FlipperObject.Builder()
- .put("cacheType", cacheType)
- .put("clearKey", "memory")
- .put("sizeBytes", memoryCache.size)
- .put("maxSizeBytes", memoryCache.maxSize)
- .put("imageIds", buildImageIdList(memoryCache.lruEntries))
- .build();
- }
-
- private FlipperObject getDiskStats(
- final String cacheType, List diskEntries) {
- return new FlipperObject.Builder()
- .put("cacheType", cacheType)
- .put("clearKey", "disk")
- .put("sizeBytes", Fresco.getImagePipelineFactory().getMainFileCache().getSize())
- .put("imageIds", buildImageIdListDisk(diskEntries))
- .build();
- }
-
- private static FlipperObject getImageData(
- String imageID, String uriString, int width, int height, int sizeBytes, String data) {
- return new FlipperObject.Builder()
- .put("imageId", imageID)
- .put("uri", uriString)
- .put("width", width)
- .put("height", height)
- .put("sizeBytes", sizeBytes)
- .put("data", data)
- .build();
- }
-
- private boolean ensureFrescoInitialized() {
- mPerfLogger.startMarker("Sonar.Fresco.ensureFrescoInitialized");
- try {
- Fresco.getImagePipelineFactory();
- return true;
- } catch (NullPointerException e) {
- return false;
- } finally {
- mPerfLogger.endMarker("Sonar.Fresco.ensureFrescoInitialized");
- }
- }
-
- private FlipperArray buildImageIdList(List> images) {
- FlipperArray.Builder builder = new FlipperArray.Builder();
- for (DumpInfoEntry entry : images) {
- final FlipperImageTracker.ImageDebugData imageDebugData =
- mFlipperImageTracker.getImageDebugData(entry.key);
-
- if (imageDebugData == null) {
- builder.put(mFlipperImageTracker.trackImage(entry.key).getUniqueId());
- } else {
- builder.put(imageDebugData.getUniqueId());
- }
- }
- return builder.build();
- }
-
- private FlipperArray buildImageIdListDisk(List diskEntries) {
- FlipperArray.Builder builder = new FlipperArray.Builder();
- for (DiskStorage.DiskDumpInfoEntry entry : diskEntries) {
- final CacheKey entryCacheKey = new SimpleCacheKey(entry.id, true);
- final FlipperImageTracker.ImageDebugData imageDebugData =
- mFlipperImageTracker.getImageDebugData(entryCacheKey);
-
- if (imageDebugData == null) {
- builder.put(mFlipperImageTracker.trackImage(entry.path, entryCacheKey).getUniqueId());
- } else {
- builder.put(imageDebugData.getUniqueId());
- }
- }
- return builder.build();
- }
-
- private String bitmapToBase64Preview(Bitmap bitmap, PlatformBitmapFactory bitmapFactory) {
- if (bitmap.getWidth() < BITMAP_SCALING_THRESHOLD_WIDTH
- && bitmap.getHeight() < BITMAP_SCALING_THRESHOLD_HEIGHT) {
- return bitmapToBase64WithoutScaling(bitmap);
- }
- mPerfLogger.startMarker("Sonar.Fresco.bitmap2base64-resize");
-
- // TODO (t19034797): properly load images
- CloseableReference scaledBitmapReference = null;
- try {
- float previewAspectRatio = BITMAP_PREVIEW_WIDTH / BITMAP_PREVIEW_HEIGHT;
- float imageAspectRatio = bitmap.getWidth() / bitmap.getHeight();
-
- int scaledWidth;
- int scaledHeight;
- if (previewAspectRatio > imageAspectRatio) {
- scaledWidth = bitmap.getWidth() * BITMAP_PREVIEW_HEIGHT / bitmap.getHeight();
- scaledHeight = BITMAP_PREVIEW_HEIGHT;
- } else {
- scaledWidth = BITMAP_PREVIEW_WIDTH;
- scaledHeight = bitmap.getHeight() * BITMAP_PREVIEW_WIDTH / bitmap.getWidth();
- }
- scaledBitmapReference =
- bitmapFactory.createScaledBitmap(bitmap, scaledWidth, scaledHeight, false);
- return bitmapToBase64WithoutScaling(scaledBitmapReference.get());
- } finally {
- CloseableReference.closeSafely(scaledBitmapReference);
- mPerfLogger.endMarker("Sonar.Fresco.bitmap2base64-resize");
- }
- }
-
- private String bitmapToBase64WithoutScaling(Bitmap bitmap) {
- mPerfLogger.startMarker("Sonar.Fresco.bitmap2base64-orig");
- ByteArrayOutputStream byteArrayOutputStream = null;
- try {
- byteArrayOutputStream = new ByteArrayOutputStream();
- bitmap.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream);
-
- return "data:image/png;base64,"
- + Base64.encodeToString(byteArrayOutputStream.toByteArray(), Base64.DEFAULT);
- } finally {
- if (byteArrayOutputStream != null) {
- try {
- byteArrayOutputStream.close();
- } catch (IOException e) {
- // ignore
- }
- }
- mPerfLogger.endMarker("Sonar.Fresco.bitmap2base64-orig");
- }
- }
-
- public void onImageLoadStatusUpdated(
- ImagePerfData imagePerfData, @ImageLoadStatus int imageLoadStatus) {
- if (imageLoadStatus != ImageLoadStatus.SUCCESS) {
- return;
- }
-
- String requestId = imagePerfData.getRequestId();
- if (requestId == null) {
- return;
- }
-
- FlipperImageTracker.ImageDebugData data =
- mFlipperImageTracker.getDebugDataForRequestId(requestId);
- if (data == null) {
- return;
- }
-
- FlipperArray.Builder imageIdsBuilder = new FlipperArray.Builder();
- Set cks = data.getCacheKeys();
- if (cks != null) {
- for (CacheKey ck : cks) {
- FlipperImageTracker.ImageDebugData d = mFlipperImageTracker.getImageDebugData(ck);
- if (d != null) {
- imageIdsBuilder.put(d.getUniqueId());
- }
- }
- } else {
- imageIdsBuilder.put(data.getUniqueId());
- }
-
- FlipperArray attribution;
- Object callerContext = imagePerfData.getCallerContext();
- if (callerContext == null) {
- attribution = new FlipperArray.Builder().put("unknown").build();
- } else if (mSonarObjectHelper == null) {
- attribution = new FlipperArray.Builder().put(callerContext.toString()).build();
- } else {
- attribution = mSonarObjectHelper.fromCallerContext(callerContext);
- }
-
- FlipperObject.Builder response =
- new FlipperObject.Builder()
- .put("imageIds", imageIdsBuilder.build())
- .put("attribution", attribution)
- .put("startTime", imagePerfData.getControllerSubmitTimeMs())
- .put("endTime", imagePerfData.getControllerFinalImageSetTimeMs())
- .put("source", ImageOriginUtils.toString(imagePerfData.getImageOrigin()));
-
- if (!imagePerfData.isPrefetch()) {
- response.put(
- "viewport",
- new FlipperObject.Builder()
- // TODO (t31947746): scan times
- .put("width", imagePerfData.getOnScreenWidthPx())
- .put("height", imagePerfData.getOnScreenHeightPx())
- .build());
- }
- FlipperObject responseObject = response.build();
- mEvents.add(responseObject);
- send(FRESCO_EVENT, responseObject);
- }
-
- public void onImageVisibilityUpdated(ImagePerfData imagePerfData, int visibilityState) {
- // ignored
- }
-
- public void sendDebugOverlayEnabledEvent(final boolean enabled) {
- final FlipperObject.Builder builder = new FlipperObject.Builder().put("enabled", enabled);
- send(FRESCO_DEBUGOVERLAY_EVENT, builder.build());
- }
-
- private static void respondError(FlipperResponder responder, String errorReason) {
- responder.error(new FlipperObject.Builder().put("reason", errorReason).build());
- }
-
- @Override
- public void onCloseableReferenceLeak(
- SharedReference reference, @Nullable Throwable stacktrace) {
- Object object = reference.get();
- Preconditions.checkNotNull(object);
- final FlipperObject.Builder builder =
- new FlipperObject.Builder()
- .put("identityHashCode", System.identityHashCode(reference))
- .put("className", object.getClass().getName());
- if (stacktrace != null) {
- builder.put("stacktrace", getStackTraceString(stacktrace));
- }
- send(FRESCO_CLOSEABLE_REFERENCE_LEAK_EVENT, builder.build());
- }
-
- public static String getStackTraceString(Throwable tr) {
- StringWriter sw = new StringWriter();
- PrintWriter pw = new PrintWriter(sw);
- tr.printStackTrace(pw);
- return sw.toString();
- }
-}
diff --git a/android/plugins/fresco/src/main/java/com/facebook/flipper/plugins/fresco/FrescoFlipperRequestListener.java b/android/plugins/fresco/src/main/java/com/facebook/flipper/plugins/fresco/FrescoFlipperRequestListener.java
deleted file mode 100644
index cba60f81a..000000000
--- a/android/plugins/fresco/src/main/java/com/facebook/flipper/plugins/fresco/FrescoFlipperRequestListener.java
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * Copyright (c) Meta Platforms, Inc. and affiliates.
- *
- * This source code is licensed under the MIT license found in the
- * LICENSE file in the root directory of this source tree.
- */
-
-package com.facebook.flipper.plugins.fresco;
-
-import com.facebook.imagepipeline.debug.DebugImageTracker;
-import com.facebook.imagepipeline.listener.BaseRequestListener;
-import com.facebook.imagepipeline.request.ImageRequest;
-
-/** Fresco image {@link RequestListener} that logs events for Sonar. */
-public class FrescoFlipperRequestListener extends BaseRequestListener {
-
- private final DebugImageTracker mDebugImageTracker;
-
- public FrescoFlipperRequestListener(DebugImageTracker debugImageTracker) {
- mDebugImageTracker = debugImageTracker;
- }
-
- @Override
- public void onRequestStart(
- ImageRequest request, Object callerContext, String requestId, boolean isPrefetch) {
- mDebugImageTracker.trackImageRequest(request, requestId);
- }
-}
diff --git a/android/plugins/fresco/src/main/java/com/facebook/flipper/plugins/fresco/objecthelper/FlipperObjectHelper.java b/android/plugins/fresco/src/main/java/com/facebook/flipper/plugins/fresco/objecthelper/FlipperObjectHelper.java
deleted file mode 100644
index 722193d3b..000000000
--- a/android/plugins/fresco/src/main/java/com/facebook/flipper/plugins/fresco/objecthelper/FlipperObjectHelper.java
+++ /dev/null
@@ -1,216 +0,0 @@
-/*
- * Copyright (c) Meta Platforms, Inc. and affiliates.
- *
- * This source code is licensed under the MIT license found in the
- * LICENSE file in the root directory of this source tree.
- */
-
-package com.facebook.flipper.plugins.fresco.objecthelper;
-
-import static com.facebook.flipper.plugins.inspector.InspectorValue.Type.Color;
-
-import android.text.TextUtils;
-import com.facebook.drawee.backends.pipeline.info.ImageOriginUtils;
-import com.facebook.drawee.backends.pipeline.info.ImagePerfData;
-import com.facebook.drawee.generic.RoundingParams;
-import com.facebook.flipper.core.FlipperArray;
-import com.facebook.flipper.core.FlipperObject;
-import com.facebook.flipper.plugins.inspector.InspectorValue;
-import com.facebook.imagepipeline.common.ImageDecodeOptions;
-import com.facebook.imagepipeline.common.ResizeOptions;
-import com.facebook.imagepipeline.common.RotationOptions;
-import com.facebook.imagepipeline.debug.FlipperImageTracker;
-import com.facebook.imagepipeline.image.ImageInfo;
-import com.facebook.imagepipeline.image.QualityInfo;
-import com.facebook.imagepipeline.request.ImageRequest;
-import java.util.Map;
-import javax.annotation.Nullable;
-
-/** Serialization helper to create {@link FlipperObject}s. */
-public abstract class FlipperObjectHelper {
-
- public FlipperObject keyValuePair(String key, @Nullable String value) {
- return new FlipperObject.Builder().put(key, value).build();
- }
-
- @Nullable
- public FlipperObject toFlipperObject(@Nullable Map stringMap) {
- if (stringMap == null) {
- return null;
- }
- FlipperObject.Builder optionsJson = new FlipperObject.Builder();
- for (Map.Entry entry : stringMap.entrySet()) {
- optionsJson.put(entry.getKey(), entry.getValue());
- }
- return optionsJson.build();
- }
-
- @Nullable
- public FlipperObject toFlipperObject(@Nullable ImageRequest imageRequest) {
- if (imageRequest == null) {
- return null;
- }
- FlipperObject.Builder optionsJson = new FlipperObject.Builder();
- return addImageRequestProperties(optionsJson, imageRequest).build();
- }
-
- @Nullable
- public FlipperObject toFlipperObject(
- @Nullable FlipperImageTracker.ImageDebugData imageDebugData) {
- if (imageDebugData == null) {
- return null;
- }
- FlipperObject.Builder optionsJson = new FlipperObject.Builder();
- optionsJson.put("imageId", imageDebugData.getUniqueId());
- optionsJson.put("imageRequest", toFlipperObject(imageDebugData.getImageRequest()));
- optionsJson.put(
- "requestId",
- imageDebugData.getRequestIds() != null
- ? TextUtils.join(", ", imageDebugData.getRequestIds())
- : "");
- optionsJson.put("imagePerfData", toFlipperObject(imageDebugData.getImagePerfData()));
- return optionsJson.build();
- }
-
- @Nullable
- public FlipperObject toFlipperObject(@Nullable ImageDecodeOptions options) {
- if (options == null) {
- return null;
- }
- FlipperObject.Builder optionsJson = new FlipperObject.Builder();
- optionsJson.put("minDecodeIntervalMs", options.minDecodeIntervalMs);
- optionsJson.put("decodePreviewFrame", options.decodePreviewFrame);
- optionsJson.put("useLastFrameForPreview", options.useLastFrameForPreview);
- optionsJson.put("decodeAllFrames", options.decodeAllFrames);
- optionsJson.put("forceStaticImage", options.forceStaticImage);
- optionsJson.put("bitmapConfig", options.bitmapConfig.name());
- optionsJson.put(
- "customImageDecoder",
- options.customImageDecoder == null ? "" : options.customImageDecoder.toString());
- return optionsJson.build();
- }
-
- @Nullable
- public FlipperObject toFlipperObject(@Nullable ResizeOptions resizeOptions) {
- if (resizeOptions == null) {
- return null;
- }
- FlipperObject.Builder optionsJson = new FlipperObject.Builder();
- optionsJson.put("width", resizeOptions.width);
- optionsJson.put("height", resizeOptions.height);
- optionsJson.put("maxBitmapSize", resizeOptions.maxBitmapSize);
- optionsJson.put("roundUpFraction", resizeOptions.roundUpFraction);
- return optionsJson.build();
- }
-
- @Nullable
- public FlipperObject toFlipperObject(@Nullable RotationOptions rotationOptions) {
- if (rotationOptions == null) {
- return null;
- }
- FlipperObject.Builder optionsJson = new FlipperObject.Builder();
- optionsJson.put("rotationEnabled", rotationOptions.rotationEnabled());
- optionsJson.put("canDeferUntilRendered", rotationOptions.canDeferUntilRendered());
- optionsJson.put("useImageMetadata", rotationOptions.useImageMetadata());
- if (!rotationOptions.useImageMetadata()) {
- optionsJson.put("forcedAngle", rotationOptions.getForcedAngle());
- }
- return optionsJson.build();
- }
-
- @Nullable
- public FlipperObject toFlipperObject(@Nullable RoundingParams roundingParams) {
- if (roundingParams == null) {
- return null;
- }
- FlipperObject.Builder optionsJson = new FlipperObject.Builder();
- optionsJson.put("borderWidth", roundingParams.getBorderWidth());
- optionsJson.put("cornersRadii", toSonarArray(roundingParams.getCornersRadii()));
- optionsJson.put("padding", roundingParams.getPadding());
- optionsJson.put("roundAsCircle", roundingParams.getRoundAsCircle());
- optionsJson.put("roundingMethod", roundingParams.getRoundingMethod());
- optionsJson.put(
- "borderColor", InspectorValue.immutable(Color, roundingParams.getBorderColor()));
- optionsJson.put(
- "overlayColor", InspectorValue.immutable(Color, roundingParams.getOverlayColor()));
- return optionsJson.build();
- }
-
- @Nullable
- public FlipperObject toFlipperObject(@Nullable ImagePerfData imagePerfData) {
- if (imagePerfData == null) {
- return null;
- }
- FlipperObject.Builder objectJson = new FlipperObject.Builder();
- objectJson.put("requestId", imagePerfData.getRequestId());
- objectJson.put("controllerSubmitTimeMs", imagePerfData.getControllerSubmitTimeMs());
- objectJson.put("controllerFinalTimeMs", imagePerfData.getControllerFinalImageSetTimeMs());
- objectJson.put("imageRequestStartTimeMs", imagePerfData.getImageRequestStartTimeMs());
- objectJson.put("imageRequestEndTimeMs", imagePerfData.getImageRequestEndTimeMs());
- objectJson.put("imageOrigin", ImageOriginUtils.toString(imagePerfData.getImageOrigin()));
- objectJson.put("isPrefetch", imagePerfData.isPrefetch());
- objectJson.put("callerContext", imagePerfData.getCallerContext());
- objectJson.put("imageRequest", toFlipperObject(imagePerfData.getImageRequest()));
- objectJson.put("imageInfo", toFlipperObject(imagePerfData.getImageInfo()));
- return objectJson.build();
- }
-
- @Nullable
- public FlipperObject toFlipperObject(ImageInfo imageInfo) {
- if (imageInfo == null) {
- return null;
- }
- FlipperObject.Builder objectJson = new FlipperObject.Builder();
- objectJson.put("imageWidth", imageInfo.getWidth());
- objectJson.put("imageHeight", imageInfo.getHeight());
- objectJson.put("qualityInfo", toFlipperObject(imageInfo.getQualityInfo()));
- return objectJson.build();
- }
-
- @Nullable
- public FlipperObject toFlipperObject(QualityInfo qualityInfo) {
- if (qualityInfo == null) {
- return null;
- }
- FlipperObject.Builder objectJson = new FlipperObject.Builder();
- objectJson.put("quality", qualityInfo.getQuality());
- objectJson.put("isGoodEnoughQuality", qualityInfo.isOfGoodEnoughQuality());
- objectJson.put("isFullQuality", qualityInfo.isOfFullQuality());
- return objectJson.build();
- }
-
- public FlipperObject.Builder addImageRequestProperties(
- FlipperObject.Builder builder, @Nullable ImageRequest request) {
- if (request == null) {
- return builder;
- }
- builder
- .put("sourceUri", request.getSourceUri())
- .put("preferredWidth", request.getPreferredWidth())
- .put("preferredHeight", request.getPreferredHeight())
- .put("cacheChoice", request.getCacheChoice())
- .put("diskCacheEnabled", request.isDiskCacheEnabled())
- .put("localThumbnailPreviewsEnabled", request.getLocalThumbnailPreviewsEnabled())
- .put("lowestPermittedRequestLevel", request.getLowestPermittedRequestLevel())
- .put("priority", request.getPriority().name())
- .put("progressiveRenderingEnabled", request.getProgressiveRenderingEnabled())
- .put("postprocessor", String.valueOf(request.getPostprocessor()))
- .put("requestListener", String.valueOf(request.getRequestListener()))
- .put("imageDecodeOptions", toFlipperObject(request.getImageDecodeOptions()))
- .put("bytesRange", request.getBytesRange())
- .put("resizeOptions", toFlipperObject(request.getResizeOptions()))
- .put("rotationOptions", toFlipperObject(request.getRotationOptions()));
- return builder;
- }
-
- private FlipperArray toSonarArray(float[] floats) {
- final FlipperArray.Builder builder = new FlipperArray.Builder();
- for (float f : floats) {
- builder.put(f);
- }
- return builder.build();
- }
-
- @Nullable
- public abstract FlipperArray fromCallerContext(@Nullable Object callerContext);
-}
diff --git a/android/plugins/fresco/build.gradle b/android/plugins/jetpack-compose/build.gradle
similarity index 58%
rename from android/plugins/fresco/build.gradle
rename to android/plugins/jetpack-compose/build.gradle
index 737ae77b1..a4e323712 100644
--- a/android/plugins/fresco/build.gradle
+++ b/android/plugins/jetpack-compose/build.gradle
@@ -6,8 +6,10 @@
*/
apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
android {
+ namespace 'com.facebook.flipper.plugins.jetpackcompose'
compileSdkVersion rootProject.compileSdkVersion
buildToolsVersion rootProject.buildToolsVersion
@@ -16,19 +18,18 @@ android {
targetSdkVersion rootProject.targetSdkVersion
}
+ compileOptions {
+ targetCompatibility rootProject.javaTargetVersion
+ sourceCompatibility rootProject.javaTargetVersion
+ }
+
dependencies {
implementation project(':android')
- implementation deps.fresco
- implementation deps.frescoFlipper
- compileOnly deps.jsr305
-
- api deps.boltsTasks
-
- // Exclude the actual stetho dep as we only want some of the fresco APIs here
- implementation(deps.frescoStetho) {
- exclude group: 'com.facebook.stetho'
- }
+ implementation 'androidx.compose.ui:ui:1.4.3'
+ implementation 'androidx.compose.ui:ui-tooling:1.4.3'
+ implementation 'org.jetbrains.kotlin:kotlin-reflect:1.9.0'
}
}
+
apply plugin: 'com.vanniktech.maven.publish'
diff --git a/android/plugins/fresco/gradle.properties b/android/plugins/jetpack-compose/gradle.properties
similarity index 55%
rename from android/plugins/fresco/gradle.properties
rename to android/plugins/jetpack-compose/gradle.properties
index 47f4e428c..9de462875 100644
--- a/android/plugins/fresco/gradle.properties
+++ b/android/plugins/jetpack-compose/gradle.properties
@@ -5,8 +5,8 @@
# file in the root directory of this source tree.
#
-POM_NAME=Flipper Fresco Plugin
-POM_DESCRIPTION=Images plugin for Flipper
-POM_ARTIFACT_ID=flipper-fresco-plugin
+POM_NAME=Flipper Jetpack Compose UIDebugger Plugin
+POM_DESCRIPTION=Jetpack Compose Plugin for the Flipper UIDebugger
+POM_ARTIFACT_ID=flipper-jetpack-compose-plugin
POM_PACKAGING=aar
diff --git a/android/plugins/jetpack-compose/src/main/java/com/facebook/flipper/plugins/jetpackcompose/UIDebuggerComposeSupport.kt b/android/plugins/jetpack-compose/src/main/java/com/facebook/flipper/plugins/jetpackcompose/UIDebuggerComposeSupport.kt
new file mode 100644
index 000000000..55746b67d
--- /dev/null
+++ b/android/plugins/jetpack-compose/src/main/java/com/facebook/flipper/plugins/jetpackcompose/UIDebuggerComposeSupport.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+package com.facebook.flipper.plugins.jetpackcompose
+
+import androidx.compose.ui.platform.ComposeView
+import com.facebook.flipper.plugins.jetpackcompose.descriptors.*
+import com.facebook.flipper.plugins.jetpackcompose.model.*
+import com.facebook.flipper.plugins.uidebugger.core.UIDContext
+import com.facebook.flipper.plugins.uidebugger.descriptors.DescriptorRegister
+
+const val JetpackComposeTag = "JetpackCompose"
+
+object UIDebuggerComposeSupport {
+
+ fun enable(context: UIDContext) {
+ addDescriptors(context.descriptorRegister)
+ }
+
+ private fun addDescriptors(register: DescriptorRegister) {
+ register.register(ComposeView::class.java, ComposeViewDescriptor)
+ register.register(ComposeNode::class.java, ComposeNodeDescriptor)
+ register.register(ComposeInnerViewNode::class.java, ComposeInnerViewDescriptor)
+ }
+}
diff --git a/android/plugins/jetpack-compose/src/main/java/com/facebook/flipper/plugins/jetpackcompose/descriptors/ComposeInnerViewDescriptor.kt b/android/plugins/jetpack-compose/src/main/java/com/facebook/flipper/plugins/jetpackcompose/descriptors/ComposeInnerViewDescriptor.kt
new file mode 100644
index 000000000..8e430d89a
--- /dev/null
+++ b/android/plugins/jetpack-compose/src/main/java/com/facebook/flipper/plugins/jetpackcompose/descriptors/ComposeInnerViewDescriptor.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+package com.facebook.flipper.plugins.jetpackcompose.descriptors
+
+import android.graphics.Bitmap
+import android.view.ViewGroup
+import com.facebook.flipper.plugins.jetpackcompose.model.ComposeInnerViewNode
+import com.facebook.flipper.plugins.uidebugger.descriptors.Id
+import com.facebook.flipper.plugins.uidebugger.descriptors.NodeDescriptor
+import com.facebook.flipper.plugins.uidebugger.descriptors.ViewDescriptor
+import com.facebook.flipper.plugins.uidebugger.descriptors.ViewGroupDescriptor
+import com.facebook.flipper.plugins.uidebugger.model.Bounds
+import com.facebook.flipper.plugins.uidebugger.model.InspectableObject
+import com.facebook.flipper.plugins.uidebugger.model.MetadataId
+import com.facebook.flipper.plugins.uidebugger.util.MaybeDeferred
+import java.lang.System
+
+object ComposeInnerViewDescriptor : NodeDescriptor {
+
+ override fun getId(node: ComposeInnerViewNode): Id = System.identityHashCode(node.view)
+
+ override fun getBounds(node: ComposeInnerViewNode): Bounds {
+ return node.bounds
+ }
+
+ override fun getName(node: ComposeInnerViewNode): String {
+ if (node.view is ViewGroup) {
+ return ViewGroupDescriptor.getName(node.view)
+ }
+ return ViewDescriptor.getName(node.view)
+ }
+
+ override fun getQualifiedName(node: ComposeInnerViewNode): String {
+ if (node.view is ViewGroup) {
+ return ViewGroupDescriptor.getQualifiedName(node.view)
+ }
+ return ViewDescriptor.getQualifiedName(node.view)
+ }
+
+ override fun getChildren(node: ComposeInnerViewNode): List {
+ if (node.view is ViewGroup) {
+ return ViewGroupDescriptor.getChildren(node.view)
+ }
+ return ViewDescriptor.getChildren(node.view)
+ }
+
+ override fun getSnapshot(node: ComposeInnerViewNode, bitmap: Bitmap?): Bitmap? {
+ if (node.view is ViewGroup) {
+ return ViewGroupDescriptor.getSnapshot(node.view, bitmap)
+ }
+ return ViewDescriptor.getSnapshot(node.view, bitmap)
+ }
+
+ override fun getActiveChild(node: ComposeInnerViewNode): Any? {
+ if (node.view is ViewGroup) {
+ return ViewGroupDescriptor.getActiveChild(node.view)
+ }
+ return ViewDescriptor.getActiveChild(node.view)
+ }
+
+ override fun getAttributes(
+ node: ComposeInnerViewNode
+ ): MaybeDeferred> {
+ if (node.view is ViewGroup) {
+ return ViewGroupDescriptor.getAttributes(node.view)
+ }
+ return ViewDescriptor.getAttributes(node.view)
+ }
+
+ override fun getTags(node: ComposeInnerViewNode): Set {
+ if (node.view is ViewGroup) {
+ return ViewGroupDescriptor.getTags(node.view)
+ }
+ return ViewDescriptor.getTags(node.view)
+ }
+}
diff --git a/android/plugins/jetpack-compose/src/main/java/com/facebook/flipper/plugins/jetpackcompose/descriptors/ComposeNodeDescriptor.kt b/android/plugins/jetpack-compose/src/main/java/com/facebook/flipper/plugins/jetpackcompose/descriptors/ComposeNodeDescriptor.kt
new file mode 100644
index 000000000..22b5e0046
--- /dev/null
+++ b/android/plugins/jetpack-compose/src/main/java/com/facebook/flipper/plugins/jetpackcompose/descriptors/ComposeNodeDescriptor.kt
@@ -0,0 +1,140 @@
+/*
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+package com.facebook.flipper.plugins.jetpackcompose.descriptors
+
+import android.graphics.Bitmap
+import com.facebook.flipper.plugins.jetpackcompose.model.ComposeNode
+import com.facebook.flipper.plugins.uidebugger.descriptors.BaseTags
+import com.facebook.flipper.plugins.uidebugger.descriptors.Id
+import com.facebook.flipper.plugins.uidebugger.descriptors.MetadataRegister
+import com.facebook.flipper.plugins.uidebugger.descriptors.NodeDescriptor
+import com.facebook.flipper.plugins.uidebugger.model.Bounds
+import com.facebook.flipper.plugins.uidebugger.model.Coordinate
+import com.facebook.flipper.plugins.uidebugger.model.Inspectable
+import com.facebook.flipper.plugins.uidebugger.model.InspectableObject
+import com.facebook.flipper.plugins.uidebugger.model.InspectableValue
+import com.facebook.flipper.plugins.uidebugger.model.MetadataId
+import com.facebook.flipper.plugins.uidebugger.util.Immediate
+import com.facebook.flipper.plugins.uidebugger.util.MaybeDeferred
+
+object ComposeNodeDescriptor : NodeDescriptor {
+
+ private const val NAMESPACE = "ComposeNode"
+
+ private var SectionId =
+ MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, NAMESPACE)
+ private val IdAttributeId =
+ MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "id")
+ private val KeyAttributeId =
+ MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "key")
+ private val NameAttributeId =
+ MetadataRegister.register(MetadataRegister.TYPE_IDENTITY, NAMESPACE, "name")
+ private val FilenameAttributeId =
+ MetadataRegister.register(MetadataRegister.TYPE_IDENTITY, NAMESPACE, "filename")
+ private val PackageHashAttributeId =
+ MetadataRegister.register(MetadataRegister.TYPE_IDENTITY, NAMESPACE, "packageHash")
+ private val LineNumberAttributeId =
+ MetadataRegister.register(MetadataRegister.TYPE_IDENTITY, NAMESPACE, "lineNumber")
+ private val OffsetAttributeId =
+ MetadataRegister.register(MetadataRegister.TYPE_IDENTITY, NAMESPACE, "offset")
+ private val LengthAttributeId =
+ MetadataRegister.register(MetadataRegister.TYPE_IDENTITY, NAMESPACE, "length")
+ private val BoxAttributeId =
+ MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "box")
+ private val BoundsAttributeId =
+ MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "bounds")
+ private val Bounds0AttributeId =
+ MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "(x0, y0)")
+ private val Bounds1AttributeId =
+ MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "(x1, y1)")
+ private val Bounds2AttributeId =
+ MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "(x2, y2)")
+ private val Bounds3AttributeId =
+ MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "(x3, y3)")
+ private val ViewIdAttributeId =
+ MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "viewId")
+ private val MergedSemanticsAttributeId =
+ MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "mergedSemantics")
+ private val UnmergedSemanticsAttributeId =
+ MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "unmergedSemantics")
+
+ override fun getName(node: ComposeNode): String = node.inspectorNode.name
+
+ override fun getChildren(node: ComposeNode): List {
+ return node.children
+ }
+
+ override fun getAttributes(node: ComposeNode): MaybeDeferred> {
+
+ val builder = mutableMapOf()
+ val props = mutableMapOf()
+
+ props[IdAttributeId] = InspectableValue.Number(node.inspectorNode.id)
+ props[ViewIdAttributeId] = InspectableValue.Number(node.inspectorNode.viewId)
+ props[KeyAttributeId] = InspectableValue.Number(node.inspectorNode.key)
+ props[NameAttributeId] = InspectableValue.Text(node.inspectorNode.name)
+ props[FilenameAttributeId] = InspectableValue.Text(node.inspectorNode.fileName)
+ props[PackageHashAttributeId] = InspectableValue.Number(node.inspectorNode.packageHash)
+ props[LineNumberAttributeId] = InspectableValue.Number(node.inspectorNode.lineNumber)
+ props[OffsetAttributeId] = InspectableValue.Number(node.inspectorNode.offset)
+ props[LengthAttributeId] = InspectableValue.Number(node.inspectorNode.length)
+
+ props[BoxAttributeId] =
+ InspectableValue.Bounds(
+ Bounds(
+ node.inspectorNode.left,
+ node.inspectorNode.top,
+ node.inspectorNode.width,
+ node.inspectorNode.height))
+
+ node.inspectorNode.bounds?.let { bounds ->
+ val quadBounds = mutableMapOf()
+ quadBounds[Bounds0AttributeId] = InspectableValue.Coordinate(Coordinate(bounds.x0, bounds.y0))
+ quadBounds[Bounds1AttributeId] = InspectableValue.Coordinate(Coordinate(bounds.x1, bounds.y1))
+ quadBounds[Bounds2AttributeId] = InspectableValue.Coordinate(Coordinate(bounds.x2, bounds.y2))
+ quadBounds[Bounds3AttributeId] = InspectableValue.Coordinate(Coordinate(bounds.x3, bounds.y3))
+ props[BoundsAttributeId] = InspectableObject(quadBounds.toMap())
+ }
+
+ val mergedSemantics = mutableMapOf()
+ node.inspectorNode.mergedSemantics.forEach {
+ val keyAttributeId =
+ MetadataRegister.register(
+ MetadataRegister.TYPE_ATTRIBUTE, node.inspectorNode.name, it.name)
+ mergedSemantics[keyAttributeId] = InspectableValue.Text(it.value.toString())
+ }
+ props[MergedSemanticsAttributeId] = InspectableObject(mergedSemantics.toMap())
+
+ val unmergedSemantics = mutableMapOf()
+ node.inspectorNode.unmergedSemantics.forEach {
+ val keyAttributeId =
+ MetadataRegister.register(
+ MetadataRegister.TYPE_ATTRIBUTE, node.inspectorNode.name, it.name)
+ mergedSemantics[keyAttributeId] = InspectableValue.Text(it.value.toString())
+ }
+ props[UnmergedSemanticsAttributeId] = InspectableObject(unmergedSemantics.toMap())
+
+ builder[SectionId] = InspectableObject(props.toMap())
+
+ return Immediate(builder)
+ }
+
+ override fun getBounds(node: ComposeNode): Bounds {
+ return node.bounds
+ }
+
+ override fun getQualifiedName(node: ComposeNode): String = node.inspectorNode.name
+
+ override fun getSnapshot(node: ComposeNode, bitmap: Bitmap?): Bitmap? = null
+
+ override fun getActiveChild(node: ComposeNode): Any? = null
+
+ override fun getTags(node: ComposeNode): Set = setOf(BaseTags.Android, "Compose")
+
+ override fun getId(node: ComposeNode): Id = node.inspectorNode.id.toInt()
+}
diff --git a/android/plugins/jetpack-compose/src/main/java/com/facebook/flipper/plugins/jetpackcompose/descriptors/ComposeViewDescriptor.kt b/android/plugins/jetpack-compose/src/main/java/com/facebook/flipper/plugins/jetpackcompose/descriptors/ComposeViewDescriptor.kt
new file mode 100644
index 000000000..0a4d367aa
--- /dev/null
+++ b/android/plugins/jetpack-compose/src/main/java/com/facebook/flipper/plugins/jetpackcompose/descriptors/ComposeViewDescriptor.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+package com.facebook.flipper.plugins.jetpackcompose.descriptors
+
+import android.os.Build
+import android.view.View
+import androidx.compose.ui.platform.ComposeView
+import com.facebook.flipper.plugins.jetpackcompose.model.ComposeNode
+import com.facebook.flipper.plugins.uidebugger.descriptors.ChainedDescriptor
+import facebook.internal.androidx.compose.ui.inspection.inspector.InspectorNode
+import facebook.internal.androidx.compose.ui.inspection.inspector.LayoutInspectorTree
+
+object ComposeViewDescriptor : ChainedDescriptor() {
+ override fun onGetName(node: ComposeView): String = node.javaClass.simpleName
+
+ private fun transform(view: View, nodes: List): List {
+ val positionOnScreen = IntArray(2)
+ view.getLocationOnScreen(positionOnScreen)
+
+ val xOffset = positionOnScreen[0]
+ val yOffset = positionOnScreen[1]
+
+ return nodes.map { node -> ComposeNode(view, node, xOffset, yOffset) }
+ }
+
+ override fun onGetChildren(node: ComposeView): List {
+ val children = mutableListOf()
+ val count = node.childCount - 1
+ for (i in 0..count) {
+ val child: View = node.getChildAt(i)
+ children.add(child)
+
+ if (child.javaClass.simpleName.contains("AndroidComposeView") &&
+ (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)) {
+ val layoutInspector = LayoutInspectorTree()
+ layoutInspector.hideSystemNodes = false
+ return transform(child, layoutInspector.convert(child))
+ }
+ }
+
+ return children
+ }
+}
diff --git a/android/plugins/jetpack-compose/src/main/java/com/facebook/flipper/plugins/jetpackcompose/model/ComposeInnerViewNode.kt b/android/plugins/jetpack-compose/src/main/java/com/facebook/flipper/plugins/jetpackcompose/model/ComposeInnerViewNode.kt
new file mode 100644
index 000000000..b9874f890
--- /dev/null
+++ b/android/plugins/jetpack-compose/src/main/java/com/facebook/flipper/plugins/jetpackcompose/model/ComposeInnerViewNode.kt
@@ -0,0 +1,17 @@
+/*
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+package com.facebook.flipper.plugins.jetpackcompose.model
+
+import android.view.View
+import com.facebook.flipper.plugins.uidebugger.model.Bounds
+
+class ComposeInnerViewNode(
+ val view: View,
+) {
+ val bounds: Bounds = Bounds(0, 0, view.width, view.height)
+}
diff --git a/android/plugins/jetpack-compose/src/main/java/com/facebook/flipper/plugins/jetpackcompose/model/ComposeNode.kt b/android/plugins/jetpack-compose/src/main/java/com/facebook/flipper/plugins/jetpackcompose/model/ComposeNode.kt
new file mode 100644
index 000000000..b3252a2e8
--- /dev/null
+++ b/android/plugins/jetpack-compose/src/main/java/com/facebook/flipper/plugins/jetpackcompose/model/ComposeNode.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+package com.facebook.flipper.plugins.jetpackcompose.model
+
+import android.os.Build
+import android.view.View
+import android.view.ViewGroup
+import androidx.annotation.RequiresApi
+import com.facebook.flipper.plugins.uidebugger.model.*
+import facebook.internal.androidx.compose.ui.inspection.inspector.InspectorNode
+
+class ComposeNode(
+ private val parentComposeView: View,
+ val inspectorNode: InspectorNode,
+ xOffset: Int,
+ yOffset: Int
+) {
+ val bounds: Bounds =
+ Bounds(
+ inspectorNode.left - xOffset,
+ inspectorNode.top - yOffset,
+ inspectorNode.width,
+ inspectorNode.height)
+
+ val children: List = collectChildren()
+
+ private fun collectChildren(): List {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ val viewId = inspectorNode.viewId
+ if (viewId != 0L) {
+ val view = parentComposeView.findViewByDrawingId(viewId)
+ if (view != null) {
+ return listOf(ComposeInnerViewNode(view))
+ }
+ }
+ }
+
+ return inspectorNode.children.map { child ->
+ ComposeNode(parentComposeView, child, inspectorNode.left, inspectorNode.top)
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.Q)
+ private fun View.findViewByDrawingId(drawingId: Long): View? {
+ if (this.uniqueDrawingId == drawingId) {
+ return this
+ }
+ if (this is ViewGroup) {
+ for (i in 0 until this.childCount) {
+ val child = this.getChildAt(i)
+ val foundView = child.findViewByDrawingId(drawingId)
+ if (foundView != null) {
+ return foundView
+ }
+ }
+ }
+ return null
+ }
+}
diff --git a/android/plugins/jetpack-compose/src/main/java/facebook/internal/androidx/compose/ui/inspection/README.facebook b/android/plugins/jetpack-compose/src/main/java/facebook/internal/androidx/compose/ui/inspection/README.facebook
new file mode 100644
index 000000000..2c093fe1d
--- /dev/null
+++ b/android/plugins/jetpack-compose/src/main/java/facebook/internal/androidx/compose/ui/inspection/README.facebook
@@ -0,0 +1,5 @@
+This is a check-in of https://android.googlesource.com/platform/frameworks/support/+/refs/heads/androidx-main/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection
+
+The classes are currently not exported but we rely on them for debug information.
+
+Tree: 59746be8ea17d5753471bd285b3fbc9cf8ea7c31
diff --git a/android/plugins/jetpack-compose/src/main/java/facebook/internal/androidx/compose/ui/inspection/inspector/InlineClassConverter.kt b/android/plugins/jetpack-compose/src/main/java/facebook/internal/androidx/compose/ui/inspection/inspector/InlineClassConverter.kt
new file mode 100644
index 000000000..6c2152379
--- /dev/null
+++ b/android/plugins/jetpack-compose/src/main/java/facebook/internal/androidx/compose/ui/inspection/inspector/InlineClassConverter.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package facebook.internal.androidx.compose.ui.inspection.inspector
+
+/**
+ * Converter for casting a parameter represented by its primitive value to its inline class type.
+ *
+ * For example: an androidx.compose.ui.graphics.Color instance is often represented by a long
+ */
+internal class InlineClassConverter {
+ // Map from inline type name to inline class and conversion lambda
+ private val typeMap = mutableMapOf Any>()
+ // Return value used in functions
+ private val notInlineType: (Any) -> Any = { it }
+
+ /** Clear any cached data. */
+ fun clear() {
+ typeMap.clear()
+ }
+
+ /**
+ * Cast the specified [value] to a value of type [inlineClassName] if possible.
+ *
+ * @param inlineClassName the fully qualified name of the inline class.
+ * @param value the value to convert to an instance of [inlineClassName].
+ */
+ fun castParameterValue(inlineClassName: String?, value: Any?): Any? =
+ if (value != null && inlineClassName != null) typeMapperFor(inlineClassName)(value) else value
+
+ private fun typeMapperFor(typeName: String): (Any) -> (Any) =
+ typeMap.getOrPut(typeName) { loadTypeMapper(typeName.replace('.', '/')) }
+
+ private fun loadTypeMapper(className: String): (Any) -> Any {
+ val javaClass = loadClassOrNull(className) ?: return notInlineType
+ val create = javaClass.declaredConstructors.singleOrNull() ?: return notInlineType
+ create.isAccessible = true
+ return { value -> create.newInstance(value) }
+ }
+
+ private fun loadClassOrNull(className: String): Class<*>? =
+ try {
+ javaClass.classLoader!!.loadClass(className)
+ } catch (ex: Exception) {
+ null
+ }
+}
diff --git a/android/plugins/jetpack-compose/src/main/java/facebook/internal/androidx/compose/ui/inspection/inspector/InspectorNode.kt b/android/plugins/jetpack-compose/src/main/java/facebook/internal/androidx/compose/ui/inspection/inspector/InspectorNode.kt
new file mode 100644
index 000000000..63eb4e4e6
--- /dev/null
+++ b/android/plugins/jetpack-compose/src/main/java/facebook/internal/androidx/compose/ui/inspection/inspector/InspectorNode.kt
@@ -0,0 +1,237 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package facebook.internal.androidx.compose.ui.inspection.inspector
+
+import androidx.compose.ui.layout.LayoutInfo
+import androidx.compose.ui.unit.IntRect
+
+internal const val UNDEFINED_ID = 0L
+
+internal val emptyBox = IntRect(0, 0, 0, 0)
+internal val outsideBox = IntRect(Int.MAX_VALUE, Int.MIN_VALUE, Int.MAX_VALUE, Int.MIN_VALUE)
+
+/** Node representing a Composable for the Layout Inspector. */
+class InspectorNode
+internal constructor(
+ /** The associated render node id or 0. */
+ val id: Long,
+
+ /** The associated key for tracking recomposition counts. */
+ val key: Int,
+
+ /**
+ * The id of the associated anchor for tracking recomposition counts.
+ *
+ * An Anchor is a mechanism in the compose runtime that can identify a Group in the SlotTable
+ * that is invariant to SlotTable updates. See [androidx.compose.runtime.Anchor] for more
+ * information.
+ */
+ val anchorId: Int,
+
+ /** The name of the Composable. */
+ val name: String,
+
+ /** The fileName where the Composable was called. */
+ val fileName: String,
+
+ /**
+ * A hash of the package name to help disambiguate duplicate [fileName] values.
+ *
+ * This hash is calculated by,
+ *
+ * `packageName.fold(0) { hash, current -> hash * 31 + current.toInt() }?.absoluteValue`
+ *
+ * where the package name is the dotted name of the package. This can be used to disambiguate
+ * which file is referenced by [fileName]. This number is -1 if there was no package hash
+ * information generated such as when the file does not contain a package declaration.
+ */
+ val packageHash: Int,
+
+ /** The line number where the Composable was called. */
+ val lineNumber: Int,
+
+ /** The UTF-16 offset in the file where the Composable was called */
+ val offset: Int,
+
+ /** The number of UTF-16 code point comprise the Composable call */
+ val length: Int,
+
+ /** The bounding box of the Composable. */
+ internal val box: IntRect,
+
+ /** The 4 corners of the polygon after transformations of the original rectangle. */
+ val bounds: QuadBounds? = null,
+
+ /** True if the code for the Composable was inlined */
+ val inlined: Boolean = false,
+
+ /** The parameters of this Composable. */
+ val parameters: List,
+
+ /** The id of a android View embedded under this node. */
+ val viewId: Long,
+
+ /** The merged semantics information of this Composable. */
+ val mergedSemantics: List,
+
+ /** The un-merged semantics information of this Composable. */
+ val unmergedSemantics: List,
+
+ /** The children nodes of this Composable. */
+ val children: List
+) {
+ /** Left side of the Composable in pixels. */
+ val left: Int
+ get() = box.left
+
+ /** Top of the Composable in pixels. */
+ val top: Int
+ get() = box.top
+
+ /** Width of the Composable in pixels. */
+ val width: Int
+ get() = box.width
+
+ /** Width of the Composable in pixels. */
+ val height: Int
+ get() = box.height
+
+ fun parametersByKind(kind: ParameterKind): List =
+ when (kind) {
+ ParameterKind.Normal -> parameters
+ ParameterKind.MergedSemantics -> mergedSemantics
+ ParameterKind.UnmergedSemantics -> unmergedSemantics
+ }
+}
+
+data class QuadBounds(
+ val x0: Int,
+ val y0: Int,
+ val x1: Int,
+ val y1: Int,
+ val x2: Int,
+ val y2: Int,
+ val x3: Int,
+ val y3: Int,
+) {
+ val xMin: Int
+ get() = sequenceOf(x0, x1, x2, x3).minOrNull()!!
+
+ val xMax: Int
+ get() = sequenceOf(x0, x1, x2, x3).maxOrNull()!!
+
+ val yMin: Int
+ get() = sequenceOf(y0, y1, y2, y3).minOrNull()!!
+
+ val yMax: Int
+ get() = sequenceOf(y0, y1, y2, y3).maxOrNull()!!
+
+ val outerBox: IntRect
+ get() = IntRect(xMin, yMin, xMax, yMax)
+}
+
+/** Parameter definition with a raw value reference. */
+class RawParameter(val name: String, val value: Any?)
+
+/** Mutable version of [InspectorNode]. */
+internal class MutableInspectorNode {
+ var id = UNDEFINED_ID
+ var key = 0
+ var anchorId = 0
+ val layoutNodes = mutableListOf()
+ val mergedSemantics = mutableListOf()
+ val unmergedSemantics = mutableListOf()
+ var name = ""
+ var fileName = ""
+ var packageHash = -1
+ var lineNumber = 0
+ var offset = 0
+ var length = 0
+ var box: IntRect = emptyBox
+ var bounds: QuadBounds? = null
+ var inlined = false
+ val parameters = mutableListOf()
+ var viewId = UNDEFINED_ID
+ val children = mutableListOf()
+ var outerBox: IntRect = outsideBox
+
+ fun reset() {
+ markUnwanted()
+ id = UNDEFINED_ID
+ key = 0
+ anchorId = 0
+ viewId = UNDEFINED_ID
+ layoutNodes.clear()
+ mergedSemantics.clear()
+ unmergedSemantics.clear()
+ box = emptyBox
+ bounds = null
+ inlined = false
+ outerBox = outsideBox
+ children.clear()
+ }
+
+ fun markUnwanted() {
+ name = ""
+ fileName = ""
+ packageHash = -1
+ lineNumber = 0
+ offset = 0
+ length = 0
+ parameters.clear()
+ }
+
+ fun shallowCopy(node: InspectorNode): MutableInspectorNode = apply {
+ id = node.id
+ key = node.key
+ anchorId = node.anchorId
+ mergedSemantics.addAll(node.mergedSemantics)
+ unmergedSemantics.addAll(node.unmergedSemantics)
+ name = node.name
+ fileName = node.fileName
+ packageHash = node.packageHash
+ lineNumber = node.lineNumber
+ offset = node.offset
+ length = node.length
+ box = node.box
+ bounds = node.bounds
+ inlined = node.inlined
+ parameters.addAll(node.parameters)
+ viewId = node.viewId
+ children.addAll(node.children)
+ }
+
+ fun build(withSemantics: Boolean = true): InspectorNode =
+ InspectorNode(
+ id,
+ key,
+ anchorId,
+ name,
+ fileName,
+ packageHash,
+ lineNumber,
+ offset,
+ length,
+ box,
+ bounds,
+ inlined,
+ parameters.toList(),
+ viewId,
+ if (withSemantics) mergedSemantics.toList() else emptyList(),
+ if (withSemantics) unmergedSemantics.toList() else emptyList(),
+ children.toList())
+}
diff --git a/android/plugins/jetpack-compose/src/main/java/facebook/internal/androidx/compose/ui/inspection/inspector/LayoutInspectorTree.kt b/android/plugins/jetpack-compose/src/main/java/facebook/internal/androidx/compose/ui/inspection/inspector/LayoutInspectorTree.kt
new file mode 100644
index 000000000..867857fa8
--- /dev/null
+++ b/android/plugins/jetpack-compose/src/main/java/facebook/internal/androidx/compose/ui/inspection/inspector/LayoutInspectorTree.kt
@@ -0,0 +1,826 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package facebook.internal.androidx.compose.ui.inspection.inspector
+
+import android.view.View
+import androidx.annotation.VisibleForTesting
+import androidx.compose.runtime.tooling.CompositionData
+import androidx.compose.runtime.tooling.CompositionGroup
+import androidx.compose.ui.InternalComposeUiApi
+import androidx.compose.ui.R
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.layout.GraphicLayerInfo
+import androidx.compose.ui.layout.LayoutInfo
+import androidx.compose.ui.node.InteroperableComposeUiNode
+import androidx.compose.ui.node.Ref
+import androidx.compose.ui.node.RootForTest
+import androidx.compose.ui.platform.ViewRootForInspector
+import androidx.compose.ui.semantics.getAllSemanticsNodes
+import androidx.compose.ui.tooling.data.ContextCache
+import androidx.compose.ui.tooling.data.ParameterInformation
+import androidx.compose.ui.tooling.data.SourceContext
+import androidx.compose.ui.tooling.data.SourceLocation
+import androidx.compose.ui.tooling.data.UiToolingDataApi
+import androidx.compose.ui.tooling.data.findParameters
+import androidx.compose.ui.tooling.data.mapTree
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntRect
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.toSize
+import facebook.internal.androidx.compose.ui.inspection.util.AnchorMap
+import facebook.internal.androidx.compose.ui.inspection.util.NO_ANCHOR_ID
+import java.util.ArrayDeque
+import java.util.Collections
+import java.util.IdentityHashMap
+import kotlin.math.max
+import kotlin.math.min
+import kotlin.math.roundToInt
+
+/**
+ * The [InspectorNode.id] will be populated with:
+ * - the layerId from a LayoutNode if this exists
+ * - an id generated from an Anchor instance from the SlotTree if this exists
+ * - a generated id if none of the above ids are available
+ *
+ * The interval -10000..-2 is reserved for the generated ids.
+ */
+@VisibleForTesting const val RESERVED_FOR_GENERATED_IDS = -10000L
+const val PLACEHOLDER_ID = Long.MAX_VALUE
+
+private val emptySize = IntSize(0, 0)
+
+private val unwantedCalls =
+ setOf(
+ "CompositionLocalProvider",
+ "Content",
+ "Inspectable",
+ "ProvideAndroidCompositionLocals",
+ "ProvideCommonCompositionLocals",
+ )
+
+/** Generator of a tree for the Layout Inspector. */
+@OptIn(UiToolingDataApi::class)
+class LayoutInspectorTree {
+ @Suppress("MemberVisibilityCanBePrivate") var hideSystemNodes = true
+ var includeNodesOutsizeOfWindow = true
+ var includeAllParameters = true
+ private var foundNode: InspectorNode? = null
+ private var windowSize = emptySize
+ private val inlineClassConverter = InlineClassConverter()
+ private val parameterFactory = ParameterFactory(inlineClassConverter)
+ private val cache = ArrayDeque()
+ private var generatedId = -1L
+ private val subCompositions = SubCompositionRoots()
+ /** Map from [LayoutInfo] to the nearest [InspectorNode] that contains it */
+ private val claimedNodes = IdentityHashMap()
+ /** Map from parent tree to child trees that are about to be stitched together */
+ private val treeMap = IdentityHashMap>()
+ /** Map from owner node to child trees that are about to be stitched to this owner */
+ private val ownerMap = IdentityHashMap>()
+ /** Map from semantics id to a list of merged semantics information */
+ private val semanticsMap = mutableMapOf>()
+ /* Map of seemantics id to a list of unmerged semantics information */
+ private val unmergedSemanticsMap = mutableMapOf>()
+ /** Set of tree nodes that were stitched into another tree */
+ private val stitched = Collections.newSetFromMap(IdentityHashMap())
+ private val contextCache = ContextCache()
+ private val anchorMap = AnchorMap()
+
+ /** Converts the [CompositionData] set held by [view] into a list of root nodes. */
+ fun convert(view: View): List {
+ windowSize = IntSize(view.width, view.height)
+ parameterFactory.density = Density(view.context)
+ @Suppress("UNCHECKED_CAST")
+ val tables =
+ view.getTag(R.id.inspection_slot_table_set) as? Set ?: return emptyList()
+ clear()
+ collectSemantics(view)
+ val result = convert(tables, view)
+ clear()
+ return result
+ }
+
+ fun findParameters(view: View, anchorId: Int): InspectorNode? {
+ windowSize = IntSize(view.width, view.height)
+ parameterFactory.density = Density(view.context)
+ val identity = anchorMap[anchorId] ?: return null
+
+ @Suppress("UNCHECKED_CAST")
+ val tables = view.getTag(R.id.inspection_slot_table_set) as? Set ?: return null
+ val node = newNode().apply { this.anchorId = anchorId }
+ val group = tables.firstNotNullOfOrNull { it.find(identity) } ?: return null
+ group.findParameters(contextCache).forEach {
+ val castedValue = castValue(it)
+ node.parameters.add(RawParameter(it.name, castedValue))
+ }
+ return buildAndRelease(node)
+ }
+
+ /**
+ * Add the roots to sub compositions that may have been collected from a different SlotTree.
+ *
+ * See [SubCompositionRoots] for details.
+ */
+ fun addSubCompositionRoots(view: View, nodes: List): List =
+ subCompositions.addRoot(view, nodes)
+
+ /**
+ * Extract the merged semantics for this semantics owner such that they can be added to compose
+ * nodes during the conversion of Group nodes.
+ */
+ private fun collectSemantics(view: View) {
+ val root = view as? RootForTest ?: return
+ val nodes = root.semanticsOwner.getAllSemanticsNodes(mergingEnabled = true)
+ val unmergedNodes = root.semanticsOwner.getAllSemanticsNodes(mergingEnabled = false)
+ nodes.forEach { node ->
+ semanticsMap[node.id] = node.config.map { RawParameter(it.key.name, it.value) }
+ }
+ unmergedNodes.forEach { node ->
+ unmergedSemanticsMap[node.id] = node.config.map { RawParameter(it.key.name, it.value) }
+ }
+ }
+
+ /** Converts the [RawParameter]s of the [node] into displayable parameters. */
+ fun convertParameters(
+ rootId: Long,
+ node: InspectorNode,
+ kind: ParameterKind,
+ maxRecursions: Int,
+ maxInitialIterableSize: Int
+ ): List {
+ val parameters = node.parametersByKind(kind)
+ return parameters.mapIndexed { index, parameter ->
+ parameterFactory.create(
+ rootId,
+ node.id,
+ node.anchorId,
+ parameter.name,
+ parameter.value,
+ kind,
+ index,
+ maxRecursions,
+ maxInitialIterableSize)
+ }
+ }
+
+ /**
+ * Converts a part of the [RawParameter] identified by [reference] into a displayable parameter.
+ * If the parameter is some sort of a collection then [startIndex] and [maxElements] describes the
+ * scope of the data returned.
+ */
+ fun expandParameter(
+ rootId: Long,
+ node: InspectorNode,
+ reference: NodeParameterReference,
+ startIndex: Int,
+ maxElements: Int,
+ maxRecursions: Int,
+ maxInitialIterableSize: Int
+ ): NodeParameter? {
+ val parameters = node.parametersByKind(reference.kind)
+ if (reference.parameterIndex !in parameters.indices) {
+ return null
+ }
+ val parameter = parameters[reference.parameterIndex]
+ return parameterFactory.expand(
+ rootId,
+ node.id,
+ node.anchorId,
+ parameter.name,
+ parameter.value,
+ reference,
+ startIndex,
+ maxElements,
+ maxRecursions,
+ maxInitialIterableSize)
+ }
+
+ /** Reset any state accumulated between windows. */
+ @Suppress("unused")
+ fun resetAccumulativeState() {
+ subCompositions.resetAccumulativeState()
+ parameterFactory.clearReferenceCache()
+ // Reset the generated id. Nodes are assigned an id if there isn't a layout node id present.
+ generatedId = -1L
+ }
+
+ private fun clear() {
+ cache.clear()
+ inlineClassConverter.clear()
+ claimedNodes.clear()
+ treeMap.clear()
+ ownerMap.clear()
+ semanticsMap.clear()
+ unmergedSemanticsMap.clear()
+ stitched.clear()
+ subCompositions.clear()
+ foundNode = null
+ }
+
+ private fun convert(tables: Set, view: View): List {
+ val trees = tables.mapNotNull { convert(view, it) }
+ return when (trees.size) {
+ 0 -> listOf()
+ 1 -> addTree(mutableListOf(), trees.single())
+ else -> stitchTreesByLayoutInfo(trees)
+ }
+ }
+
+ /**
+ * Stitch separate trees together using the [LayoutInfo]s found in the [CompositionData]s.
+ *
+ * Some constructs in Compose (e.g. ModalDrawer) will result is multiple [CompositionData]s. This
+ * code will attempt to stitch the resulting [InspectorNode] trees together by looking at the
+ * parent of each [LayoutInfo].
+ *
+ * If this algorithm is successful the result of this function will be a list with a single tree.
+ */
+ private fun stitchTreesByLayoutInfo(trees: List): List {
+ val layoutToTreeMap = IdentityHashMap()
+ trees.forEach { tree -> tree.layoutNodes.forEach { layoutToTreeMap[it] = tree } }
+ trees.forEach { tree ->
+ val layout = tree.layoutNodes.lastOrNull()
+ val parentLayout =
+ generateSequence(layout) { it.parentInfo }
+ .firstOrNull {
+ val otherTree = layoutToTreeMap[it]
+ otherTree != null && otherTree != tree
+ }
+ if (parentLayout != null) {
+ val ownerNode = claimedNodes[parentLayout]
+ val ownerTree = layoutToTreeMap[parentLayout]
+ if (ownerNode != null && ownerTree != null) {
+ ownerMap.getOrPut(ownerNode) { mutableListOf() }.add(tree)
+ treeMap.getOrPut(ownerTree) { mutableListOf() }.add(tree)
+ }
+ }
+ }
+ var parentTree = findDeepParentTree()
+ while (parentTree != null) {
+ addSubTrees(parentTree)
+ treeMap.remove(parentTree)
+ parentTree = findDeepParentTree()
+ }
+ val result = mutableListOf()
+ trees.asSequence().filter { !stitched.contains(it) }.forEach { addTree(result, it) }
+ return result
+ }
+
+ /**
+ * Return a parent tree where the children trees (to be stitched under the parent) are not a
+ * parent themselves. Do this to avoid rebuilding the same tree more than once.
+ */
+ private fun findDeepParentTree(): MutableInspectorNode? =
+ treeMap.entries
+ .asSequence()
+ .filter { (_, children) -> children.none { treeMap.containsKey(it) } }
+ .firstOrNull()
+ ?.key
+
+ private fun addSubTrees(tree: MutableInspectorNode) {
+ for ((index, child) in tree.children.withIndex()) {
+ tree.children[index] = addSubTrees(child) ?: child
+ }
+ }
+
+ /**
+ * Rebuild [node] with any possible sub trees added (stitched in). Return the rebuild node, or
+ * null if no changes were found in this node or its children. Lazily allocate the new node to
+ * avoid unnecessary allocations.
+ */
+ private fun addSubTrees(node: InspectorNode): InspectorNode? {
+ var newNode: MutableInspectorNode? = null
+ for ((index, child) in node.children.withIndex()) {
+ val newChild = addSubTrees(child)
+ if (newChild != null) {
+ val newCopy = newNode ?: newNode(node)
+ newCopy.children[index] = newChild
+ newNode = newCopy
+ }
+ }
+ val trees = ownerMap[node]
+ if (trees == null && newNode == null) {
+ return null
+ }
+ val newCopy = newNode ?: newNode(node)
+ if (trees != null) {
+ trees.forEach { addTree(newCopy.children, it) }
+ stitched.addAll(trees)
+ }
+ return buildAndRelease(newCopy)
+ }
+
+ /**
+ * Add [tree] to the end of the [out] list. The root nodes of [tree] may be a fake node that hold
+ * a list of [LayoutInfo].
+ */
+ private fun addTree(
+ out: MutableList,
+ tree: MutableInspectorNode
+ ): List {
+ tree.children.forEach {
+ if (it.name.isNotEmpty()) {
+ out.add(it)
+ } else {
+ out.addAll(it.children)
+ }
+ }
+ return out
+ }
+
+ private fun convert(view: View, table: CompositionData): MutableInspectorNode? {
+ val fakeParent = newNode()
+ val group = table.mapTree(::convert, contextCache) ?: return null
+ addToParent(fakeParent, listOf(group), buildFakeChildNodes = true)
+ return if (belongsToView(fakeParent.layoutNodes, view)) fakeParent else null
+ }
+
+ private fun convert(
+ group: CompositionGroup,
+ context: SourceContext,
+ children: List
+ ): MutableInspectorNode {
+ val parent = parse(group, context, children)
+ subCompositions.captureNode(parent, context)
+ addToParent(parent, children)
+ return parent
+ }
+
+ /**
+ * Adds the nodes in [input] to the children of [parentNode]. Nodes without a reference to a
+ * wanted Composable are skipped unless [buildFakeChildNodes]. A single skipped render id and
+ * layoutNode will be added to [parentNode].
+ */
+ private fun addToParent(
+ parentNode: MutableInspectorNode,
+ input: List,
+ buildFakeChildNodes: Boolean = false
+ ) {
+ // If we're adding an unwanted node from the `input` to the parent node and it has a
+ // View ID, then assign it to the parent view so that we don't lose the context that we
+ // found a View as a descendant of the parent node. Most likely, there were one or more
+ // unwanted intermediate nodes between the node that actually owns the Android View
+ // and the desired node that the View should be associated with in the inspector. If
+ // there's more than one input node with a View ID, we skip this step since it's
+ // unclear how these views would be related.
+ input
+ .singleOrNull { it.viewId != UNDEFINED_ID }
+ ?.takeIf { node ->
+ // Take if the node has been marked as unwanted
+ node.id == UNDEFINED_ID
+ }
+ ?.let { nodeWithView -> parentNode.viewId = nodeWithView.viewId }
+
+ var id: Long? = null
+ input.forEach { node ->
+ if (node.name.isEmpty() && !(buildFakeChildNodes && node.layoutNodes.isNotEmpty())) {
+ parentNode.children.addAll(node.children)
+ if (node.id > UNDEFINED_ID) {
+ // If multiple siblings with a render ids are dropped:
+ // Ignore them all. And delegate the drawing to a parent in the inspector.
+ id = if (id == null) node.id else UNDEFINED_ID
+ }
+ } else {
+ node.id = if (node.id != UNDEFINED_ID) node.id else --generatedId
+ val withSemantics = node.packageHash !in systemPackages
+ val resultNode = node.build(withSemantics)
+ // TODO: replace getOrPut with putIfAbsent which requires API level 24
+ node.layoutNodes.forEach { claimedNodes.getOrPut(it) { resultNode } }
+ parentNode.children.add(resultNode)
+ if (withSemantics) {
+ node.mergedSemantics.clear()
+ node.unmergedSemantics.clear()
+ }
+ }
+ if (node.bounds != null && parentNode.box == node.box) {
+ parentNode.bounds = node.bounds
+ }
+ parentNode.layoutNodes.addAll(node.layoutNodes)
+ parentNode.mergedSemantics.addAll(node.mergedSemantics)
+ parentNode.unmergedSemantics.addAll(node.unmergedSemantics)
+ release(node)
+ }
+ val nodeId = id
+ parentNode.id = if (parentNode.id <= UNDEFINED_ID && nodeId != null) nodeId else parentNode.id
+ }
+
+ @OptIn(InternalComposeUiApi::class)
+ private fun parse(
+ group: CompositionGroup,
+ context: SourceContext,
+ children: List
+ ): MutableInspectorNode {
+ val node = newNode()
+ node.name = context.name ?: ""
+ node.key = group.key as? Int ?: 0
+ node.inlined = context.isInline
+
+ // If this node is associated with an android View, set the node's viewId to point to
+ // the hosted view. We use the parent's uniqueDrawingId since the interopView returned here
+ // will be the view itself, but we want to use the `AndroidViewHolder` that hosts the view
+ // instead of the view directly.
+ (group.node as? InteroperableComposeUiNode?)?.getInteropView()?.let { interopView ->
+ (interopView.parent as? View)?.uniqueDrawingId?.let { viewId -> node.viewId = viewId }
+ }
+
+ val layoutInfo = group.node as? LayoutInfo
+ if (layoutInfo != null) {
+ return parseLayoutInfo(layoutInfo, context, node)
+ }
+ if (unwantedOutsideWindow(node, children)) {
+ return markUnwanted(group, context, node)
+ }
+ node.box = context.bounds.emptyCheck()
+ if (unwantedName(node.name) || (node.box == emptyBox && !subCompositions.capturing)) {
+ return markUnwanted(group, context, node)
+ }
+ parseCallLocation(node, context.location)
+ if (isHiddenSystemNode(node)) {
+ return markUnwanted(group, context, node)
+ }
+ node.anchorId = anchorMap[group.identity]
+ node.id = syntheticId(node.anchorId)
+ if (includeAllParameters) {
+ addParameters(context, node)
+ }
+ return node
+ }
+
+ private fun IntRect.emptyCheck(): IntRect = if (left >= right && top >= bottom) emptyBox else this
+
+ private fun IntRect.inWindow(): Boolean =
+ !(left > windowSize.width || right < 0 || top > windowSize.height || bottom < 0)
+
+ private fun IntRect.union(other: IntRect): IntRect {
+ if (this == outsideBox) return other else if (other == outsideBox) return this
+
+ return IntRect(
+ left = min(left, other.left),
+ top = min(top, other.top),
+ bottom = max(bottom, other.bottom),
+ right = max(right, other.right))
+ }
+
+ private fun parseLayoutInfo(
+ layoutInfo: LayoutInfo,
+ context: SourceContext,
+ node: MutableInspectorNode
+ ): MutableInspectorNode {
+ val box = context.bounds
+ val size = box.size.toSize()
+ val coordinates = layoutInfo.coordinates
+ val topLeft = toIntOffset(coordinates.localToWindow(Offset.Zero))
+ val topRight = toIntOffset(coordinates.localToWindow(Offset(size.width, 0f)))
+ val bottomRight = toIntOffset(coordinates.localToWindow(Offset(size.width, size.height)))
+ val bottomLeft = toIntOffset(coordinates.localToWindow(Offset(0f, size.height)))
+ var bounds: QuadBounds? = null
+
+ if (topLeft.x != box.left ||
+ topLeft.y != box.top ||
+ topRight.x != box.right ||
+ topRight.y != box.top ||
+ bottomRight.x != box.right ||
+ bottomRight.y != box.bottom ||
+ bottomLeft.x != box.left ||
+ bottomLeft.y != box.bottom) {
+ bounds =
+ QuadBounds(
+ topLeft.x,
+ topLeft.y,
+ topRight.x,
+ topRight.y,
+ bottomRight.x,
+ bottomRight.y,
+ bottomLeft.x,
+ bottomLeft.y,
+ )
+ }
+ if (!includeNodesOutsizeOfWindow) {
+ // Ignore this node if the bounds are completely outside the window
+ node.outerBox = bounds?.outerBox ?: box
+ if (!node.outerBox.inWindow()) {
+ return node
+ }
+ }
+
+ node.box = box.emptyCheck()
+ node.bounds = bounds
+ node.layoutNodes.add(layoutInfo)
+ val modifierInfo = layoutInfo.getModifierInfo()
+
+ val unmergedSemantics = unmergedSemanticsMap[layoutInfo.semanticsId]
+ if (unmergedSemantics != null) {
+ node.unmergedSemantics.addAll(unmergedSemantics)
+ }
+
+ val mergedSemantics = semanticsMap[layoutInfo.semanticsId]
+ if (mergedSemantics != null) {
+ node.mergedSemantics.addAll(mergedSemantics)
+ }
+
+ node.id =
+ modifierInfo
+ .asSequence()
+ .map { it.extra }
+ .filterIsInstance()
+ .map { it.layerId }
+ .firstOrNull() ?: UNDEFINED_ID
+
+ return node
+ }
+
+ private fun syntheticId(anchorId: Int): Long {
+ if (anchorId == NO_ANCHOR_ID) {
+ return UNDEFINED_ID
+ }
+ // The anchorId is an Int
+ return anchorId.toLong() - Int.MAX_VALUE.toLong() + RESERVED_FOR_GENERATED_IDS
+ }
+
+ private fun belongsToView(layoutNodes: List, view: View): Boolean =
+ layoutNodes
+ .asSequence()
+ .flatMap { node ->
+ node
+ .getModifierInfo()
+ .asSequence()
+ .map { it.extra }
+ .filterIsInstance()
+ .map { it.ownerViewId }
+ }
+ .contains(view.uniqueDrawingId)
+
+ private fun addParameters(context: SourceContext, node: MutableInspectorNode) {
+ context.parameters.forEach {
+ val castedValue = castValue(it)
+ node.parameters.add(RawParameter(it.name, castedValue))
+ }
+ }
+
+ private fun castValue(parameter: ParameterInformation): Any? {
+ val value = parameter.value ?: return null
+ if (parameter.inlineClass == null || !isPrimitive(value.javaClass)) return value
+ return inlineClassConverter.castParameterValue(parameter.inlineClass, value)
+ }
+
+ private fun isPrimitive(cls: Class<*>): Boolean = cls.kotlin.javaPrimitiveType != null
+
+ private fun toIntOffset(offset: Offset): IntOffset =
+ IntOffset(offset.x.roundToInt(), offset.y.roundToInt())
+
+ private fun markUnwanted(
+ group: CompositionGroup,
+ context: SourceContext,
+ node: MutableInspectorNode
+ ): MutableInspectorNode =
+ when (node.name) {
+ "rememberCompositionContext" -> subCompositions.rememberCompositionContext(node, context)
+ "remember" -> subCompositions.remember(node, group)
+ else -> node.apply { markUnwanted() }
+ }
+
+ private fun parseCallLocation(node: MutableInspectorNode, location: SourceLocation?) {
+ val fileName = location?.sourceFile ?: return
+ node.fileName = fileName
+ node.packageHash = location.packageHash
+ node.lineNumber = location.lineNumber
+ node.offset = location.offset
+ node.length = location.length
+ }
+
+ private fun isHiddenSystemNode(node: MutableInspectorNode): Boolean =
+ node.packageHash in systemPackages && hideSystemNodes
+
+ private fun unwantedName(name: String): Boolean =
+ name.isEmpty() || name.startsWith("remember") || name in unwantedCalls
+
+ private fun unwantedOutsideWindow(
+ node: MutableInspectorNode,
+ children: List
+ ): Boolean {
+ if (includeNodesOutsizeOfWindow) {
+ return false
+ }
+ node.outerBox =
+ if (children.isEmpty()) outsideBox
+ else children.map { g -> g.outerBox }.reduce { acc, box -> box.union(acc) }
+ return !node.outerBox.inWindow()
+ }
+
+ private fun newNode(): MutableInspectorNode =
+ if (cache.isNotEmpty()) cache.pop() else MutableInspectorNode()
+
+ private fun newNode(copyFrom: InspectorNode): MutableInspectorNode =
+ newNode().shallowCopy(copyFrom)
+
+ private fun release(node: MutableInspectorNode) {
+ node.reset()
+ cache.add(node)
+ }
+
+ private fun buildAndRelease(node: MutableInspectorNode): InspectorNode {
+ val result = node.build()
+ release(node)
+ return result
+ }
+
+ /**
+ * Keep track of sub-composition roots.
+ *
+ * Examples:
+ * - Popup, Dialog: When one of these is open an extra Android Window is created with its own
+ * AndroidComposeView. The contents of the Composable is a sub-composition that will be computed
+ * by calling convert.
+ *
+ * The Popup/Dialog composable itself, and a few helping composables (the root) will not be
+ * included in the SlotTree with the contents, instead these composables will be found in the
+ * SlotTree for the main app and they all have empty sizes. The aim is to collect these
+ * sub-composition roots such that they can be added to the [InspectorNode]s of the contents.
+ * - AndroidView: When this is used in a compose app we will see a similar pattern in the SlotTree
+ * except there isn't a sub-composition to stitch in. But we need to collect the view id
+ * separately from the "AndroidView" node itself.
+ */
+ private inner class SubCompositionRoots {
+ /** Set to true when the nodes found should be added to a sub-composition root */
+ var capturing = false
+ private set
+
+ /** The `uniqueDrawingId` of the `AndroidComposeView` that owns the root being captured */
+ private var ownerView = UNDEFINED_ID
+
+ /** The node that represent the root of the sub-composition */
+ private var rootNode: MutableInspectorNode? = null
+
+ /** The depth of the parse tree the [rootNode] was found at */
+ private var rootNodeDepth = 0
+
+ /** Last captured view that is believed to be an embbed View under an AndroidView node */
+ private var androidView = UNDEFINED_ID
+
+ /**
+ * The sub-composition roots found.
+ *
+ * Map from View owner to a pair of [InspectorNode] indicating the actual root, and the node
+ * where the content should be stitched in.
+ */
+ private val found = mutableMapOf()
+
+ /** Call this before converting a SlotTree for an AndroidComposeView */
+ fun clear() {
+ capturing = false
+ ownerView = UNDEFINED_ID
+ rootNode?.markUnwanted()
+ rootNode?.id = UNDEFINED_ID
+ rootNode = null
+ rootNodeDepth = 0
+ }
+
+ /** Call this when starting converting a new set of windows */
+ fun resetAccumulativeState() {
+ found.clear()
+ }
+
+ /**
+ * When a "rememberCompositionContext" is found in the slot tree, it indicates that a
+ * sub-composition was started. We should capture all parent nodes with an empty size as the
+ * "root" of the sub-composition.
+ */
+ fun rememberCompositionContext(
+ node: MutableInspectorNode,
+ context: SourceContext
+ ): MutableInspectorNode {
+ if (capturing) {
+ save()
+ }
+ capturing = true
+ rootNode = node
+ rootNodeDepth = context.depth
+ node.id = PLACEHOLDER_ID
+ return node
+ }
+
+ /**
+ * When "remember" is found in the slot tree and we are currently capturing, the data of the
+ * [group] may contain the owner of the sub-composition.
+ */
+ fun remember(node: MutableInspectorNode, group: CompositionGroup): MutableInspectorNode {
+ node.markUnwanted()
+ if (!capturing) {
+ return node
+ }
+ val root = findSingleRootInGroupData(group) ?: return node
+
+ val view = root.subCompositionView
+ if (view != null) {
+ val composeOwner = if (view.childCount == 1) view.getChildAt(0) else return node
+ ownerView = composeOwner.uniqueDrawingId
+ } else {
+ androidView = root.viewRoot?.uniqueDrawingId ?: UNDEFINED_ID
+ // Store the viewRoot such that we can move the View under the compose node
+ // in Studio. We do not need to capture the Groups found for this case, so
+ // we call "reset" here to stop capturing.
+ clear()
+ }
+ return node
+ }
+
+ private fun findSingleRootInGroupData(group: CompositionGroup): ViewRootForInspector? {
+ group.data.filterIsInstance().singleOrNull()?.let {
+ return it
+ }
+ val refs = group.data.filterIsInstance[>().map { it.value }
+ return refs.filterIsInstance]().singleOrNull()
+ }
+
+ /** Capture the top node of the sub-composition root until a non empty node is found. */
+ fun captureNode(node: MutableInspectorNode, context: SourceContext) {
+ if (!capturing) {
+ return
+ }
+ if (node.box != emptyBox) {
+ save()
+ return
+ }
+ val depth = context.depth
+ if (depth < rootNodeDepth) {
+ rootNode = node
+ rootNodeDepth = depth
+ }
+ }
+
+ fun latestViewId(): Long {
+ val id = androidView
+ androidView = UNDEFINED_ID
+ return id
+ }
+
+ /** If a sub-composition root has been captured, save it now. */
+ private fun save() {
+ val node = rootNode
+ if (node != null && ownerView != UNDEFINED_ID) {
+ found[ownerView] = node.build()
+ }
+ node?.markUnwanted()
+ node?.id = UNDEFINED_ID
+ node?.children?.clear()
+ clear()
+ }
+
+ /**
+ * Add the root of the sub-composition to the found tree.
+ *
+ * If a root is not found for this [owner] or if the stitching fails just return [nodes].
+ */
+ fun addRoot(owner: View, nodes: List): List {
+ val root = found[owner.uniqueDrawingId] ?: return nodes
+ val box = IntRect(0, 0, owner.width, owner.height)
+ val info = StitchInfo(nodes, box)
+ val result = listOf(stitch(root, info))
+ return if (info.added) result else nodes
+ }
+
+ private fun stitch(node: InspectorNode, info: StitchInfo): InspectorNode {
+ val children = node.children.map { stitch(it, info) }
+ val index = children.indexOfFirst { it.id == PLACEHOLDER_ID }
+ val newNode = newNode()
+ newNode.shallowCopy(node)
+ newNode.children.clear()
+ if (index < 0) {
+ newNode.children.addAll(children)
+ } else {
+ newNode.children.addAll(children.subList(0, index))
+ newNode.children.addAll(info.nodes)
+ newNode.children.addAll(children.subList(index + 1, children.size))
+ info.added = true
+ }
+ newNode.box = info.bounds
+ return buildAndRelease(newNode)
+ }
+ }
+
+ private class StitchInfo(
+ /** The nodes found that should be stitched into a sub-composition root. */
+ val nodes: List,
+
+ /** The bounds of the View containing the sub-composition */
+ val bounds: IntRect
+ ) {
+ /** Set this to true when the [nodes] have been added to a sub-composition root */
+ var added: Boolean = false
+ }
+}
diff --git a/android/plugins/jetpack-compose/src/main/java/facebook/internal/androidx/compose/ui/inspection/inspector/NodeParameter.kt b/android/plugins/jetpack-compose/src/main/java/facebook/internal/androidx/compose/ui/inspection/inspector/NodeParameter.kt
new file mode 100644
index 000000000..b2a3ce1a0
--- /dev/null
+++ b/android/plugins/jetpack-compose/src/main/java/facebook/internal/androidx/compose/ui/inspection/inspector/NodeParameter.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package facebook.internal.androidx.compose.ui.inspection.inspector
+
+/** Holds data representing a Composable parameter for the Layout Inspector. */
+class NodeParameter
+internal constructor(
+ /** The name of the parameter. */
+ val name: String,
+
+ /** The type of the parameter. */
+ val type: ParameterType,
+
+ /** The value of the parameter. */
+ val value: Any?
+) {
+ /** Sub elements of the parameter. */
+ val elements = mutableListOf()
+
+ /** Reference to value parameter. */
+ var reference: NodeParameterReference? = null
+
+ /** The index into the composite parent parameter value. */
+ var index = 0
+}
+
+/** The type of a parameter. */
+enum class ParameterType {
+ String,
+ Boolean,
+ Double,
+ Float,
+ Int32,
+ Int64,
+ Color,
+ Resource,
+ DimensionDp,
+ DimensionSp,
+ DimensionEm,
+ Lambda,
+ FunctionReference,
+ Iterable,
+}
diff --git a/android/plugins/jetpack-compose/src/main/java/facebook/internal/androidx/compose/ui/inspection/inspector/NodeParameterReference.kt b/android/plugins/jetpack-compose/src/main/java/facebook/internal/androidx/compose/ui/inspection/inspector/NodeParameterReference.kt
new file mode 100644
index 000000000..1353413f0
--- /dev/null
+++ b/android/plugins/jetpack-compose/src/main/java/facebook/internal/androidx/compose/ui/inspection/inspector/NodeParameterReference.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package facebook.internal.androidx.compose.ui.inspection.inspector
+
+import facebook.internal.androidx.compose.ui.inspection.util.asIntArray
+
+/**
+ * A reference to a parameter to a [NodeParameter]
+ *
+ * @param nodeId is the id of the node the parameter belongs to
+ * @param anchorId is the anchor hash of the node the parameter belongs to
+ * @param kind is this a reference to a normal, merged, or unmerged semantic parameter.
+ * @param parameterIndex index into [InspectorNode.parameters], [InspectorNode.mergedSemantics], or
+ * [InspectorNode.unMergedSemantics]
+ * @param indices are indices into the composite parameter
+ */
+class NodeParameterReference(
+ val nodeId: Long,
+ val anchorId: Int,
+ val kind: ParameterKind,
+ val parameterIndex: Int,
+ val indices: IntArray
+) {
+ constructor(
+ nodeId: Long,
+ anchorId: Int,
+ kind: ParameterKind,
+ parameterIndex: Int,
+ indices: List
+ ) : this(nodeId, anchorId, kind, parameterIndex, indices.asIntArray())
+
+ // For testing:
+ override fun toString(): String {
+ val suffix = if (indices.isNotEmpty()) ", ${indices.joinToString()}" else ""
+ return "[$nodeId, $anchorId, $kind, $parameterIndex$suffix]"
+ }
+}
+
+/** Identifies which kind of parameter the [NodeParameterReference] is a reference to. */
+enum class ParameterKind {
+ Normal,
+ MergedSemantics,
+ UnmergedSemantics
+}
diff --git a/android/plugins/jetpack-compose/src/main/java/facebook/internal/androidx/compose/ui/inspection/inspector/PackageHashes.kt b/android/plugins/jetpack-compose/src/main/java/facebook/internal/androidx/compose/ui/inspection/inspector/PackageHashes.kt
new file mode 100644
index 000000000..57a51d030
--- /dev/null
+++ b/android/plugins/jetpack-compose/src/main/java/facebook/internal/androidx/compose/ui/inspection/inspector/PackageHashes.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+// WARNING: DO NOT EDIT THIS FILE MANUALLY. It's automatically generated by running:
+// frameworks/support/compose/ui/ui-inspection/generate-packages/generate_compose_packages.py -r
+package facebook.internal.androidx.compose.ui.inspection.inspector
+
+import androidx.annotation.VisibleForTesting
+import kotlin.math.absoluteValue
+
+@VisibleForTesting
+fun packageNameHash(packageName: String) =
+ packageName.fold(0) { hash, char -> hash * 31 + char.code }.absoluteValue
+
+val systemPackages =
+ setOf(
+ -1,
+ packageNameHash("androidx.compose.animation"),
+ packageNameHash("androidx.compose.animation.core"),
+ packageNameHash("androidx.compose.animation.graphics.vector"),
+ packageNameHash("androidx.compose.desktop"),
+ packageNameHash("androidx.compose.foundation"),
+ packageNameHash("androidx.compose.foundation.gestures"),
+ packageNameHash("androidx.compose.foundation.gestures.snapping"),
+ packageNameHash("androidx.compose.foundation.interaction"),
+ packageNameHash("androidx.compose.foundation.layout"),
+ packageNameHash("androidx.compose.foundation.lazy"),
+ packageNameHash("androidx.compose.foundation.lazy.grid"),
+ packageNameHash("androidx.compose.foundation.lazy.layout"),
+ packageNameHash("androidx.compose.foundation.lazy.staggeredgrid"),
+ packageNameHash("androidx.compose.foundation.newtext.text"),
+ packageNameHash("androidx.compose.foundation.newtext.text.copypasta"),
+ packageNameHash("androidx.compose.foundation.newtext.text.copypasta.selection"),
+ packageNameHash("androidx.compose.foundation.pager"),
+ packageNameHash("androidx.compose.foundation.relocation"),
+ packageNameHash("androidx.compose.foundation.text"),
+ packageNameHash("androidx.compose.foundation.text.selection"),
+ packageNameHash("androidx.compose.foundation.window"),
+ packageNameHash("androidx.compose.material"),
+ packageNameHash("androidx.compose.material.internal"),
+ packageNameHash("androidx.compose.material.pullrefresh"),
+ packageNameHash("androidx.compose.material.ripple"),
+ packageNameHash("androidx.compose.material3"),
+ packageNameHash("androidx.compose.material3.internal"),
+ packageNameHash("androidx.compose.material3.windowsizeclass"),
+ packageNameHash("androidx.compose.runtime"),
+ packageNameHash("androidx.compose.runtime.livedata"),
+ packageNameHash("androidx.compose.runtime.mock"),
+ packageNameHash("androidx.compose.runtime.reflect"),
+ packageNameHash("androidx.compose.runtime.rxjava2"),
+ packageNameHash("androidx.compose.runtime.rxjava3"),
+ packageNameHash("androidx.compose.runtime.saveable"),
+ packageNameHash("androidx.compose.ui"),
+ packageNameHash("androidx.compose.ui.awt"),
+ packageNameHash("androidx.compose.ui.graphics.benchmark"),
+ packageNameHash("androidx.compose.ui.graphics.vector"),
+ packageNameHash("androidx.compose.ui.layout"),
+ packageNameHash("androidx.compose.ui.platform"),
+ packageNameHash("androidx.compose.ui.text"),
+ packageNameHash("androidx.compose.ui.util"),
+ packageNameHash("androidx.compose.ui.viewinterop"),
+ packageNameHash("androidx.compose.ui.window"),
+ )
diff --git a/android/plugins/jetpack-compose/src/main/java/facebook/internal/androidx/compose/ui/inspection/inspector/ParameterFactory.kt b/android/plugins/jetpack-compose/src/main/java/facebook/internal/androidx/compose/ui/inspection/inspector/ParameterFactory.kt
new file mode 100644
index 000000000..e74e49ed4
--- /dev/null
+++ b/android/plugins/jetpack-compose/src/main/java/facebook/internal/androidx/compose/ui/inspection/inspector/ParameterFactory.kt
@@ -0,0 +1,1001 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package facebook.internal.androidx.compose.ui.inspection.inspector
+
+import android.util.Log
+import android.view.View
+import androidx.compose.runtime.internal.ComposableLambda
+import androidx.compose.ui.AbsoluteAlignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.RectangleShape
+import androidx.compose.ui.graphics.Shadow
+import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.platform.InspectableModifier
+import androidx.compose.ui.platform.InspectableValue
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontListFontFamily
+import androidx.compose.ui.text.font.FontStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.font.ResourceFont
+import androidx.compose.ui.text.intl.Locale
+import androidx.compose.ui.text.style.BaselineShift
+import androidx.compose.ui.text.style.TextDecoration
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.TextUnit
+import androidx.compose.ui.unit.TextUnitType
+import facebook.internal.androidx.compose.ui.inspection.inspector.ParameterType.DimensionDp
+import java.lang.reflect.Field
+import java.lang.reflect.Modifier as JavaModifier
+import java.util.IdentityHashMap
+import kotlin.jvm.internal.FunctionReference
+import kotlin.jvm.internal.Lambda
+import kotlin.math.abs
+import kotlin.reflect.KClass
+import kotlin.reflect.KProperty
+import kotlin.reflect.KProperty1
+import kotlin.reflect.full.allSuperclasses
+import kotlin.reflect.full.declaredMemberProperties
+import kotlin.reflect.jvm.isAccessible
+import kotlin.reflect.jvm.javaField
+import kotlin.reflect.jvm.javaGetter
+
+private val reflectionScope: ReflectionScope = ReflectionScope()
+
+/**
+ * Factory of [NodeParameter]s.
+ *
+ * Each parameter value is converted to a user readable value.
+ */
+internal class ParameterFactory(private val inlineClassConverter: InlineClassConverter) {
+ /** A map from known values to a user readable string representation. */
+ private val valueLookup = mutableMapOf()
+
+ /** The classes we have loaded constants from. */
+ private val valuesLoaded = mutableSetOf>()
+
+ /**
+ * Do not load constant names from instances of these classes. We prefer showing the raw values of
+ * Color and Dimensions.
+ */
+ private val ignoredClasses = listOf(Color::class.java, Dp::class.java)
+ private var creatorCache: ParameterCreator? = null
+
+ /**
+ * Do not decompose instances or lookup constants from these package prefixes
+ *
+ * The following instances are known to contain self recursion:
+ * - kotlinx.coroutines.flow.StateFlowImpl
+ * - androidx.compose.ui.node.LayoutNode
+ */
+ private val ignoredPackagePrefixes =
+ listOf("android.", "java.", "javax.", "kotlinx.", "androidx.compose.ui.node.")
+
+ var density = Density(1.0f)
+
+ init {
+ val textDecorationCombination =
+ TextDecoration.combine(listOf(TextDecoration.LineThrough, TextDecoration.Underline))
+ valueLookup[textDecorationCombination] = "LineThrough+Underline"
+ valueLookup[Color.Unspecified] = "Unspecified"
+ valueLookup[RectangleShape] = "RectangleShape"
+ valuesLoaded.add(Enum::class.java)
+ valuesLoaded.add(Any::class.java)
+
+ // AbsoluteAlignment is not found from an instance of BiasAbsoluteAlignment,
+ // because Alignment has no file level class.
+ reflectionScope.withReflectiveAccess {
+ loadConstantsFromEnclosedClasses(AbsoluteAlignment::class.java)
+ }
+ }
+
+ /**
+ * Create a [NodeParameter] from the specified parameter [name] and [value].
+ *
+ * Attempt to convert the value to a user readable value. For now: return null when a conversion
+ * is not possible/found.
+ */
+ fun create(
+ rootId: Long,
+ nodeId: Long,
+ anchorId: Int,
+ name: String,
+ value: Any?,
+ kind: ParameterKind,
+ parameterIndex: Int,
+ maxRecursions: Int,
+ maxInitialIterableSize: Int
+ ): NodeParameter {
+ val creator = creatorCache ?: ParameterCreator()
+ try {
+ return reflectionScope.withReflectiveAccess {
+ creator.create(
+ rootId,
+ nodeId,
+ anchorId,
+ name,
+ value,
+ kind,
+ parameterIndex,
+ maxRecursions,
+ maxInitialIterableSize)
+ }
+ } finally {
+ creatorCache = creator
+ }
+ }
+
+ /**
+ * Create/expand the [NodeParameter] specified by [reference].
+ *
+ * @param rootId is the root id of the specified [nodeId].
+ * @param nodeId is the [InspectorNode.id] of the node the parameter belongs to.
+ * @param anchorId is the [InspectorNode.anchorId] of the node the parameter belongs to.
+ * @param name is the name of the [reference].parameterIndex'th parameter of the node.
+ * @param value is the value of the [reference].parameterIndex'th parameter of the node.
+ * @param startIndex is the index of the 1st wanted element of a List/Array.
+ * @param maxElements is the max number of elements wanted from a List/Array.
+ * @param maxRecursions is the max recursion into composite types starting from reference.
+ * @param maxInitialIterableSize is the max number of elements wanted in new List/Array values.
+ */
+ fun expand(
+ rootId: Long,
+ nodeId: Long,
+ anchorId: Int,
+ name: String,
+ value: Any?,
+ reference: NodeParameterReference,
+ startIndex: Int,
+ maxElements: Int,
+ maxRecursions: Int,
+ maxInitialIterableSize: Int
+ ): NodeParameter? {
+ val creator = creatorCache ?: ParameterCreator()
+ try {
+ return reflectionScope.withReflectiveAccess {
+ creator.expand(
+ rootId,
+ nodeId,
+ anchorId,
+ name,
+ value,
+ reference,
+ startIndex,
+ maxElements,
+ maxRecursions,
+ maxInitialIterableSize)
+ }
+ } finally {
+ creatorCache = creator
+ }
+ }
+
+ fun clearReferenceCache() {
+ val creator = creatorCache ?: return
+ creator.clearReferenceCache()
+ }
+
+ private fun loadConstantsFrom(javaClass: Class<*>) {
+ if (valuesLoaded.contains(javaClass) ||
+ ignoredPackagePrefixes.any { javaClass.name.startsWith(it) }) {
+ return
+ }
+ val related = generateSequence(javaClass) { it.superclass }.plus(javaClass.interfaces)
+ related.forEach { aClass ->
+ val topClass = generateSequence(aClass) { safeEnclosingClass(it) }.last()
+ loadConstantsFromEnclosedClasses(topClass)
+ findPackageLevelClass(topClass)?.let { loadConstantsFromStaticFinal(it) }
+ }
+ }
+
+ private fun safeEnclosingClass(klass: Class<*>): Class<*>? =
+ try {
+ klass.enclosingClass
+ } catch (_: Error) {
+ // Exceptions seen on API 23...
+ null
+ }
+
+ private fun findPackageLevelClass(javaClass: Class<*>): Class<*>? =
+ try {
+ // Note: This doesn't work when @file.JvmName is specified
+ Class.forName("${javaClass.name}Kt")
+ } catch (ex: Throwable) {
+ null
+ }
+
+ private fun loadConstantsFromEnclosedClasses(javaClass: Class<*>) {
+ if (valuesLoaded.contains(javaClass)) {
+ return
+ }
+ loadConstantsFromObjectInstance(javaClass.kotlin)
+ loadConstantsFromStaticFinal(javaClass)
+ valuesLoaded.add(javaClass)
+ javaClass.declaredClasses.forEach { loadConstantsFromEnclosedClasses(it) }
+ }
+
+ /**
+ * Load all constants from companion objects and singletons
+ *
+ * Exclude: primary types and types of ignoredClasses, open and lateinit vals.
+ */
+ private fun loadConstantsFromObjectInstance(kClass: KClass<*>) {
+ try {
+ val instance = kClass.objectInstance ?: return
+ kClass.declaredMemberProperties
+ .asSequence()
+ .filter { it.isFinal && !it.isLateinit }
+ .mapNotNull { constantValueOf(it, instance)?.let { key -> Pair(key, it.name) } }
+ .filter { !ignoredValue(it.first) }
+ .toMap(valueLookup)
+ } catch (_: Throwable) {
+ // KT-16479 : kotlin reflection does currently not support packages and files.
+ // We load top level values using Java reflection instead.
+ // Ignore other reflection errors as well
+ }
+ }
+
+ /**
+ * Load all constants from top level values from Java.
+ *
+ * Exclude: primary types and types of ignoredClasses. Since this is Java, inline types will also
+ * (unfortunately) be excluded.
+ */
+ private fun loadConstantsFromStaticFinal(javaClass: Class<*>) {
+ try {
+ javaClass.declaredMethods
+ .asSequence()
+ .filter {
+ it.returnType != Void.TYPE &&
+ JavaModifier.isStatic(it.modifiers) &&
+ JavaModifier.isFinal(it.modifiers) &&
+ !it.returnType.isPrimitive &&
+ it.parameterTypes.isEmpty() &&
+ it.name.startsWith("get")
+ }
+ .mapNotNull { javaClass.getDeclaredField(it.name.substring(3)) }
+ .mapNotNull { constantValueOf(it)?.let { key -> Pair(key, it.name) } }
+ .filter { !ignoredValue(it.first) }
+ .toMap(valueLookup)
+ } catch (_: ReflectiveOperationException) {
+ // ignore reflection errors
+ } catch (_: NoClassDefFoundError) {
+ // ignore missing classes on lower level SDKs
+ }
+ }
+
+ private fun constantValueOf(field: Field?): Any? =
+ try {
+ field?.isAccessible = true
+ field?.get(null)
+ } catch (_: ReflectiveOperationException) {
+ // ignore reflection errors
+ null
+ }
+
+ private fun constantValueOf(property: KProperty1, instance: Any): Any? =
+ try {
+ val field = property.javaField
+ field?.isAccessible = true
+ inlineClassConverter.castParameterValue(inlineResultClass(property), field?.get(instance))
+ } catch (_: ReflectiveOperationException) {
+ // ignore reflection errors
+ null
+ }
+
+ private fun inlineResultClass(property: KProperty1): String? {
+ // The Java getter name will be mangled if it contains parameters of an inline class.
+ // The mangled part starts with a '-'.
+ if (property.javaGetter?.name?.contains('-') == true) {
+ return property.returnType.toString()
+ }
+ return null
+ }
+
+ private fun ignoredValue(value: Any?): Boolean =
+ value == null ||
+ ignoredClasses.any { ignored -> ignored.isInstance(value) } ||
+ value::class.java.isPrimitive
+
+ /** Convenience class for building [NodeParameter]s. */
+ private inner class ParameterCreator {
+ private var rootId = 0L
+ private var nodeId = 0L
+ private var anchorId = 0
+ private var kind: ParameterKind = ParameterKind.Normal
+ private var parameterIndex = 0
+ private var maxRecursions = 0
+ private var maxInitialIterableSize = 0
+ private var recursions = 0
+ private val valueIndex = mutableListOf()
+ private val valueLazyReferenceMap = IdentityHashMap>()
+ private val rootValueIndexCache =
+ mutableMapOf>()
+ private var valueIndexMap = IdentityHashMap()
+
+ fun create(
+ rootId: Long,
+ nodeId: Long,
+ anchorId: Int,
+ name: String,
+ value: Any?,
+ kind: ParameterKind,
+ parameterIndex: Int,
+ maxRecursions: Int,
+ maxInitialIterableSize: Int
+ ): NodeParameter =
+ try {
+ setup(
+ rootId, nodeId, anchorId, kind, parameterIndex, maxRecursions, maxInitialIterableSize)
+ create(name, value, null) ?: createEmptyParameter(name)
+ } finally {
+ setup()
+ }
+
+ fun expand(
+ rootId: Long,
+ nodeId: Long,
+ anchorId: Int,
+ name: String,
+ value: Any?,
+ reference: NodeParameterReference,
+ startIndex: Int,
+ maxElements: Int,
+ maxRecursions: Int,
+ maxInitialIterableSize: Int
+ ): NodeParameter? {
+ setup(
+ rootId,
+ nodeId,
+ anchorId,
+ reference.kind,
+ reference.parameterIndex,
+ maxRecursions,
+ maxInitialIterableSize)
+ var parent: Pair? = null
+ var new = Pair(name, value)
+ for (i in reference.indices) {
+ parent = new
+ new = find(new.first, new.second, i) ?: return null
+ }
+ recursions = 0
+ valueIndex.addAll(reference.indices.asSequence())
+ val parameter =
+ if (startIndex == 0) {
+ create(new.first, new.second, parent?.second)
+ } else {
+ createFromCompositeValue(new.first, new.second, parent?.second, startIndex, maxElements)
+ }
+ if (parameter == null && reference.indices.isEmpty()) {
+ return createEmptyParameter(name)
+ }
+ return parameter
+ }
+
+ fun clearReferenceCache() {
+ rootValueIndexCache.clear()
+ }
+
+ private fun setup(
+ newRootId: Long = 0,
+ newNodeId: Long = 0,
+ newAnchorId: Int = 0,
+ newKind: ParameterKind = ParameterKind.Normal,
+ newParameterIndex: Int = 0,
+ maxRecursions: Int = 0,
+ maxInitialIterableSize: Int = 0
+ ) {
+ rootId = newRootId
+ nodeId = newNodeId
+ anchorId = newAnchorId
+ kind = newKind
+ parameterIndex = newParameterIndex
+ this.maxRecursions = maxRecursions
+ this.maxInitialIterableSize = maxInitialIterableSize
+ recursions = 0
+ valueIndex.clear()
+ valueLazyReferenceMap.clear()
+ valueIndexMap = rootValueIndexCache.getOrPut(newRootId) { IdentityHashMap() }
+ }
+
+ private fun create(name: String, value: Any?, parentValue: Any?): NodeParameter? {
+ if (value == null) {
+ return null
+ }
+ createFromSimpleValue(name, value)?.let {
+ return it
+ }
+
+ val existing =
+ valueIndexMap[value] ?: return createFromCompositeValue(name, value, parentValue)
+
+ // Do not decompose an instance we already decomposed.
+ // Instead reference the data that was already decomposed.
+ return createReferenceToExistingValue(name, value, parentValue, existing)
+ }
+
+ private fun create(
+ name: String,
+ value: Any?,
+ parentValue: Any?,
+ specifiedIndex: Int = 0
+ ): NodeParameter? = create(name, value, parentValue)?.apply { index = specifiedIndex }
+
+ private fun createFromSimpleValue(name: String, value: Any?): NodeParameter? {
+ if (value == null) {
+ return null
+ }
+ createFromConstant(name, value)?.let {
+ return it
+ }
+ return when (value) {
+ is AnnotatedString -> NodeParameter(name, ParameterType.String, value.text)
+ is BaselineShift -> createFromBaselineShift(name, value)
+ is Boolean -> NodeParameter(name, ParameterType.Boolean, value)
+ is ComposableLambda -> createFromCLambda(name, value)
+ is Color -> NodeParameter(name, ParameterType.Color, value.toArgb())
+ is Double -> NodeParameter(name, ParameterType.Double, value)
+ is Dp -> NodeParameter(name, DimensionDp, value.value)
+ is Enum<*> -> NodeParameter(name, ParameterType.String, value.toString())
+ is Float -> NodeParameter(name, ParameterType.Float, value)
+ is FunctionReference -> createFromFunctionReference(name, value)
+ is FontListFontFamily -> createFromFontListFamily(name, value)
+ is FontWeight -> NodeParameter(name, ParameterType.Int32, value.weight)
+ is Int -> NodeParameter(name, ParameterType.Int32, value)
+ is Lambda<*> -> createFromLambda(name, value)
+ is Locale -> NodeParameter(name, ParameterType.String, value.toString())
+ is Long -> NodeParameter(name, ParameterType.Int64, value)
+ is SolidColor -> NodeParameter(name, ParameterType.Color, value.value.toArgb())
+ is String -> NodeParameter(name, ParameterType.String, value)
+ is TextUnit -> createFromTextUnit(name, value)
+ is ImageVector -> createFromImageVector(name, value)
+ is View -> NodeParameter(name, ParameterType.String, value.javaClass.simpleName)
+ else -> null
+ }
+ }
+
+ private fun createFromCompositeValue(
+ name: String,
+ value: Any?,
+ parentValue: Any?,
+ startIndex: Int = 0,
+ maxElements: Int = maxInitialIterableSize
+ ): NodeParameter? =
+ when {
+ value == null -> null
+ value is Modifier -> createFromModifier(name, value)
+ value is InspectableValue -> createFromInspectableValue(name, value)
+ value is Sequence<*> -> createFromSequence(name, value, value, startIndex, maxElements)
+ value is Map<*, *> ->
+ createFromSequence(name, value, value.asSequence(), startIndex, maxElements)
+ value is Map.Entry<*, *> -> createFromMapEntry(name, value, parentValue)
+ value is Iterable<*> ->
+ createFromSequence(name, value, value.asSequence(), startIndex, maxElements)
+ value.javaClass.isArray -> createFromArray(name, value, startIndex, maxElements)
+ value is Offset -> createFromOffset(name, value)
+ value is Shadow -> createFromShadow(name, value)
+ value is TextStyle -> createFromTextStyle(name, value)
+ else -> createFromKotlinReflection(name, value)
+ }
+
+ private fun find(name: String, value: Any?, index: Int): Pair? =
+ when {
+ value == null -> null
+ value is Modifier -> findFromModifier(name, value, index)
+ value is InspectableValue -> findFromInspectableValue(value, index)
+ value is Sequence<*> -> findFromSequence(value, index)
+ value is Map<*, *> -> findFromSequence(value.asSequence(), index)
+ value is Map.Entry<*, *> -> findFromMapEntry(value, index)
+ value is Iterable<*> -> findFromSequence(value.asSequence(), index)
+ value.javaClass.isArray -> findFromArray(value, index)
+ value is Offset -> findFromOffset(value, index)
+ value is Shadow -> findFromShadow(value, index)
+ value is TextStyle -> findFromTextStyle(value, index)
+ else -> findFromKotlinReflection(value, index)
+ }
+
+ private fun createRecursively(
+ name: String,
+ value: Any?,
+ parentValue: Any?,
+ index: Int
+ ): NodeParameter? {
+ valueIndex.add(index)
+ recursions++
+ val parameter = create(name, value, parentValue)?.apply { this.index = index }
+ recursions--
+ valueIndex.removeLast()
+ return parameter
+ }
+
+ private fun shouldRecurseDeeper(): Boolean = recursions < maxRecursions
+
+ /**
+ * Create a [NodeParameter] as a reference to a previously created parameter.
+ *
+ * Use [createFromCompositeValue] to compute the data type and top value, however no children
+ * will be created. Instead a reference to the previously created parameter is specified.
+ */
+ private fun createReferenceToExistingValue(
+ name: String,
+ value: Any?,
+ parentValue: Any?,
+ ref: NodeParameterReference
+ ): NodeParameter? {
+ val remember = recursions
+ recursions = maxRecursions
+ val parameter = createFromCompositeValue(name, value, parentValue)?.apply { reference = ref }
+ recursions = remember
+ return parameter
+ }
+
+ /**
+ * Returns `true` if the value can be mapped to a [NodeParameter].
+ *
+ * Composite values should NOT be added to the [valueIndexMap] since we do not intend to include
+ * this parameter in the response.
+ */
+ private fun hasMappableValue(value: Any?): Boolean {
+ if (value == null) {
+ return false
+ }
+ if (valueIndexMap.containsKey(value)) {
+ return true
+ }
+ val remember = recursions
+ recursions = maxRecursions
+ val parameter = create("p", value, null)
+ recursions = remember
+ valueIndexMap.remove(value)
+ return parameter != null
+ }
+
+ /**
+ * Store the reference of this [NodeParameter] by its [value]
+ *
+ * If the value is seen in other parameter values again, there is no need to create child
+ * parameters a second time.
+ */
+ private fun NodeParameter.store(value: Any?): NodeParameter {
+ if (value != null) {
+ val index = valueIndexToReference()
+ valueIndexMap[value] = index
+ }
+ return this
+ }
+
+ /** Remove the [value] of this [NodeParameter] if there are no child elements. */
+ private fun NodeParameter.removeIfEmpty(value: Any?): NodeParameter {
+ if (value != null) {
+ if (elements.isEmpty()) {
+ valueIndexMap.remove(value)
+ }
+ val reference = valueIndexMap[value]
+ valueLazyReferenceMap.remove(value)?.forEach { it.reference = reference }
+ }
+ return this
+ }
+
+ /**
+ * Delay the creation of all child parameters of this composite parameter.
+ *
+ * If the child parameters are omitted because of [maxRecursions], store the parameter itself
+ * such that its reference can be updated if it turns out that child [NodeParameter]s need to be
+ * generated later.
+ */
+ private fun NodeParameter.withChildReference(value: Any): NodeParameter {
+ valueLazyReferenceMap.getOrPut(value, { mutableListOf() }).add(this)
+ reference = valueIndexToReference()
+ return this
+ }
+
+ private fun valueIndexToReference(): NodeParameterReference =
+ NodeParameterReference(nodeId, anchorId, kind, parameterIndex, valueIndex)
+
+ private fun createEmptyParameter(name: String): NodeParameter =
+ NodeParameter(name, ParameterType.String, "")
+
+ private fun createFromArray(
+ name: String,
+ value: Any,
+ startIndex: Int,
+ maxElements: Int
+ ): NodeParameter? {
+ val sequence = arrayToSequence(value) ?: return null
+ return createFromSequence(name, value, sequence, startIndex, maxElements)
+ }
+
+ private fun findFromArray(value: Any, index: Int): Pair? {
+ val sequence = arrayToSequence(value) ?: return null
+ return findFromSequence(sequence, index)
+ }
+
+ private fun arrayToSequence(value: Any): Sequence<*>? =
+ when (value) {
+ is Array<*> -> value.asSequence()
+ is ByteArray -> value.asSequence()
+ is IntArray -> value.asSequence()
+ is LongArray -> value.asSequence()
+ is FloatArray -> value.asSequence()
+ is DoubleArray -> value.asSequence()
+ is BooleanArray -> value.asSequence()
+ is CharArray -> value.asSequence()
+ else -> null
+ }
+
+ private fun createFromBaselineShift(name: String, value: BaselineShift): NodeParameter {
+ val converted =
+ when (value.multiplier) {
+ BaselineShift.None.multiplier -> "None"
+ BaselineShift.Subscript.multiplier -> "Subscript"
+ BaselineShift.Superscript.multiplier -> "Superscript"
+ else -> return NodeParameter(name, ParameterType.Float, value.multiplier)
+ }
+ return NodeParameter(name, ParameterType.String, converted)
+ }
+
+ private fun createFromCLambda(name: String, value: ComposableLambda): NodeParameter? =
+ try {
+ val lambda =
+ value.javaClass.getDeclaredField("_block").apply { isAccessible = true }.get(value)
+ NodeParameter(name, ParameterType.Lambda, arrayOf(lambda))
+ } catch (_: Throwable) {
+ null
+ }
+
+ private fun createFromConstant(name: String, value: Any): NodeParameter? {
+ loadConstantsFrom(value.javaClass)
+ return valueLookup[value]?.let { NodeParameter(name, ParameterType.String, it) }
+ }
+
+ // For now: select ResourceFontFont closest to W400 and Normal, and return the resId
+ private fun createFromFontListFamily(name: String, value: FontListFontFamily): NodeParameter? =
+ findBestResourceFont(value)?.let { NodeParameter(name, ParameterType.Resource, it.resId) }
+
+ private fun createFromFunctionReference(name: String, value: FunctionReference): NodeParameter =
+ NodeParameter(name, ParameterType.FunctionReference, arrayOf(value, value.name))
+
+ private fun createFromKotlinReflection(name: String, value: Any): NodeParameter? {
+ val simpleName = value::class.simpleName
+ val properties = lookup(value) ?: return null
+ val parameter = NodeParameter(name, ParameterType.String, simpleName)
+ return when {
+ properties.isEmpty() -> parameter
+ !shouldRecurseDeeper() -> parameter.withChildReference(value)
+ else -> {
+ val elements = parameter.store(value).elements
+ properties.values.mapIndexedNotNullTo(elements) { index, part ->
+ createRecursively(part.name, valueOf(part, value), value, index)
+ }
+ parameter.removeIfEmpty(value)
+ }
+ }
+ }
+
+ private fun findFromKotlinReflection(value: Any, index: Int): Pair? {
+ val properties = lookup(value)?.entries?.iterator()?.asSequence() ?: return null
+ val element = properties.elementAtOrNull(index)?.value ?: return null
+ return Pair(element.name, valueOf(element, value))
+ }
+
+ private fun lookup(value: Any): Map>? {
+ val kClass = value::class
+ val simpleName = kClass.simpleName
+ val qualifiedName = kClass.qualifiedName
+ if (simpleName == null ||
+ qualifiedName == null ||
+ ignoredPackagePrefixes.any { qualifiedName.startsWith(it) }) {
+ // Exit without creating a parameter for:
+ // - internal synthetic classes
+ // - certain android packages
+ return null
+ }
+ return try {
+ sequenceOf(kClass)
+ .plus(kClass.allSuperclasses.asSequence())
+ .flatMap { it.declaredMemberProperties.asSequence() }
+ .associateBy { it.name }
+ } catch (ex: Throwable) {
+ Log.w("Compose", "Could not decompose ${kClass.simpleName}", ex)
+ null
+ }
+ }
+
+ private fun valueOf(property: KProperty<*>, instance: Any): Any? =
+ try {
+ property.isAccessible = true
+ // Bug in kotlin reflection API: if the type is a nullable inline type with a null
+ // value, we get an IllegalArgumentException in this line:
+ property.getter.call(instance)
+ } catch (ex: Throwable) {
+ // TODO: Remove this warning since this is expected with nullable inline types
+ Log.w("Compose", "Could not get value of ${property.name}")
+ null
+ }
+
+ private fun createFromInspectableValue(name: String, value: InspectableValue): NodeParameter {
+ val tempValue = value.valueOverride ?: ""
+ val parameterName = name.ifEmpty { value.nameFallback } ?: "element"
+ val parameterValue = if (tempValue is InspectableValue) "" else tempValue
+ val parameter =
+ createFromSimpleValue(parameterName, parameterValue)
+ ?: NodeParameter(parameterName, ParameterType.String, "")
+ if (!shouldRecurseDeeper()) {
+ return parameter.withChildReference(value)
+ }
+ val elements = parameter.store(value).elements
+ value.inspectableElements.mapIndexedNotNullTo(elements) { index, element ->
+ createRecursively(element.name, element.value, value, index)
+ }
+ return parameter.removeIfEmpty(value)
+ }
+
+ private fun findFromInspectableValue(value: InspectableValue, index: Int): Pair? {
+ val elements = value.inspectableElements.toList()
+ if (index !in elements.indices) {
+ return null
+ }
+ val element = elements[index]
+ return Pair(element.name, element.value)
+ }
+
+ private fun createFromMapEntry(
+ name: String,
+ entry: Map.Entry<*, *>,
+ parentValue: Any?
+ ): NodeParameter? {
+ val key = createRecursively("key", entry.key, entry, 0) ?: return null
+ val value = createRecursively("value", entry.value, entry, 1) ?: return null
+ val keyName = (key.value?.toString() ?: "").ifEmpty { "entry" }
+ val valueName = value.value?.toString()?.ifEmpty { null }
+ val nodeName = if (parentValue is Map<*, *>) "[$keyName]" else name
+ return NodeParameter(nodeName, ParameterType.String, valueName).apply {
+ elements.add(key)
+ elements.add(value)
+ }
+ }
+
+ private fun findFromMapEntry(entry: Map.Entry<*, *>, index: Int): Pair? =
+ when (index) {
+ 0 -> Pair("key", entry.key)
+ 1 -> Pair("value", entry.value)
+ else -> null
+ }
+
+ private fun createFromSequence(
+ name: String,
+ value: Any,
+ sequence: Sequence<*>,
+ startIndex: Int,
+ maxElements: Int
+ ): NodeParameter {
+ val parameter = NodeParameter(name, ParameterType.Iterable, sequenceName(value))
+ return when {
+ !sequence.any() -> parameter
+ !shouldRecurseDeeper() -> parameter.withChildReference(value)
+ else -> {
+ val elements = parameter.store(value).elements
+ val rest = sequence.drop(startIndex).iterator()
+ var index = startIndex
+ while (rest.hasNext() && elements.size < maxElements) {
+ createRecursively("[$index]", rest.next(), value, index)?.let { elements.add(it) }
+ index++
+ }
+ while (rest.hasNext()) {
+ if (hasMappableValue(rest.next())) {
+ parameter.withChildReference(value)
+ break
+ }
+ }
+ parameter.removeIfEmpty(value)
+ }
+ }
+ }
+
+ private fun findFromSequence(value: Sequence<*>, index: Int): Pair? {
+ val element = value.elementAtOrNull(index) ?: return null
+ return Pair("[$index]", element)
+ }
+
+ private fun sequenceName(value: Any): String =
+ when (value) {
+ is Array<*> -> "Array[${value.size}]"
+ is ByteArray -> "ByteArray[${value.size}]"
+ is IntArray -> "IntArray[${value.size}]"
+ is LongArray -> "LongArray[${value.size}]"
+ is FloatArray -> "FloatArray[${value.size}]"
+ is DoubleArray -> "DoubleArray[${value.size}]"
+ is BooleanArray -> "BooleanArray[${value.size}]"
+ is CharArray -> "CharArray[${value.size}]"
+ is List<*> -> "List[${value.size}]"
+ is Set<*> -> "Set[${value.size}]"
+ is Map<*, *> -> "Map[${value.size}]"
+ is Collection<*> -> "Collection[${value.size}]"
+ is Iterable<*> -> "Iterable"
+ else -> "Sequence"
+ }
+
+ private fun createFromLambda(name: String, value: Lambda<*>): NodeParameter =
+ NodeParameter(name, ParameterType.Lambda, arrayOf(value))
+
+ private fun createFromModifier(name: String, value: Modifier): NodeParameter? =
+ when {
+ name.isNotEmpty() -> {
+ val parameter = NodeParameter(name, ParameterType.String, "")
+ val modifiers = unwrap(value)
+ when {
+ modifiers.isEmpty() -> parameter
+ !shouldRecurseDeeper() -> parameter.withChildReference(value)
+ else -> {
+ val elements = parameter.elements
+ modifiers.mapIndexedNotNullTo(elements) { index, element ->
+ createRecursively("", element, value, index)
+ }
+ parameter.store(value).removeIfEmpty(value)
+ }
+ }
+ }
+ value is InspectableValue -> createFromInspectableValue(name, value)
+ else -> null
+ }
+
+ private fun unwrap(value: Modifier): List {
+ val collector = ModifierCollector()
+ value.foldIn(collector) { acc, m -> acc.apply { add(m) } }
+ return collector.modifiers
+ }
+
+ private fun findFromModifier(name: String, value: Modifier, index: Int): Pair? =
+ when {
+ name.isNotEmpty() -> {
+ val modifiers = unwrap(value)
+ if (index in modifiers.indices) Pair("", modifiers[index]) else null
+ }
+ value is InspectableValue -> findFromInspectableValue(value, index)
+ else -> null
+ }
+
+ private fun createFromOffset(name: String, value: Offset): NodeParameter {
+ val parameter = NodeParameter(name, ParameterType.String, Offset::class.java.simpleName)
+ val elements = parameter.elements
+ val x = with(density) { value.x.toDp().value }
+ val y = with(density) { value.y.toDp().value }
+ elements.add(NodeParameter("x", DimensionDp, x))
+ elements.add(NodeParameter("y", DimensionDp, y).apply { index = 1 })
+ return parameter
+ }
+
+ private fun findFromOffset(value: Offset, index: Int): Pair? =
+ when (index) {
+ 0 -> Pair("x", with(density) { value.x.toDp() })
+ 1 -> Pair("y", with(density) { value.y.toDp() })
+ else -> null
+ }
+
+ // Special handling of blurRadius: convert to dp:
+ private fun createFromShadow(name: String, value: Shadow): NodeParameter? {
+ val parameter = createFromKotlinReflection(name, value) ?: return null
+ val elements = parameter.elements
+ val index = elements.indexOfFirst { it.name == "blurRadius" }
+ if (index >= 0) {
+ val existing = elements[index]
+ val blurRadius = with(density) { value.blurRadius.toDp().value }
+ elements[index] = NodeParameter("blurRadius", DimensionDp, blurRadius)
+ elements[index].index = existing.index
+ }
+ return parameter
+ }
+
+ private fun findFromShadow(value: Shadow, index: Int): Pair? {
+ val result = findFromKotlinReflection(value, index)
+ if (result == null || result.first != "blurRadius") {
+ return result
+ }
+ return Pair("blurRadius", with(density) { value.blurRadius.toDp() })
+ }
+
+ // Temporary handling of TextStyle: remove when TextStyle implements InspectableValue
+ // Hide: paragraphStyle, spanStyle, platformStyle, lineHeightStyle
+ private fun createFromTextStyle(name: String, value: TextStyle): NodeParameter? {
+ val parameter = NodeParameter(name, ParameterType.String, TextStyle::class.java.simpleName)
+ val elements = parameter.elements
+ create("color", value.color, value)?.let { elements.add(it) }
+ create("fontSize", value.fontSize, value, 1)?.let { elements.add(it) }
+ create("fontWeight", value.fontWeight, value, 2)?.let { elements.add(it) }
+ create("fontStyle", value.fontStyle, value, 3)?.let { elements.add(it) }
+ create("fontSynthesis", value.fontSynthesis, value, 4)?.let { elements.add(it) }
+ create("fontFamily", value.fontFamily, value, 5)?.let { elements.add(it) }
+ create("fontFeatureSettings", value.fontFeatureSettings, value, 6)?.let { elements.add(it) }
+ create("letterSpacing", value.letterSpacing, value, 7)?.let { elements.add(it) }
+ create("baselineShift", value.baselineShift, value, 8)?.let { elements.add(it) }
+ create("textGeometricTransform", value.textGeometricTransform, value, 9)?.let {
+ elements.add(it)
+ }
+ create("localeList", value.localeList, value, 10)?.let { elements.add(it) }
+ create("background", value.background, value, 11)?.let { elements.add(it) }
+ create("textDecoration", value.textDecoration, value, 12)?.let { elements.add(it) }
+ create("shadow", value.shadow, value, 13)?.let { elements.add(it) }
+ create("textDirection", value.textDirection, value, 14)?.let { elements.add(it) }
+ create("lineHeight", value.lineHeight, value, 15)?.let { elements.add(it) }
+ create("textIndent", value.textIndent, value, 16)?.let { elements.add(it) }
+ return parameter
+ }
+
+ private fun findFromTextStyle(value: TextStyle, index: Int): Pair? =
+ when (index) {
+ 0 -> Pair("color", value.color)
+ 1 -> Pair("fontSize", value.fontSize)
+ 2 -> Pair("fontWeight", value.fontWeight)
+ 3 -> Pair("fontStyle", value.fontStyle)
+ 4 -> Pair("fontSynthesis", value.fontSynthesis)
+ 5 -> Pair("fontFamily", value.fontFamily)
+ 6 -> Pair("fontFeatureSettings", value.fontFeatureSettings)
+ 7 -> Pair("letterSpacing", value.letterSpacing)
+ 8 -> Pair("baselineShift", value.baselineShift)
+ 9 -> Pair("textGeometricTransform", value.textGeometricTransform)
+ 10 -> Pair("localeList", value.localeList)
+ 11 -> Pair("background", value.background)
+ 12 -> Pair("textDecoration", value.textDecoration)
+ 13 -> Pair("shadow", value.shadow)
+ 14 -> Pair("textDirection", value.textDirection)
+ 15 -> Pair("lineHeight", value.lineHeight)
+ 16 -> Pair("textIndent", value.textIndent)
+ else -> null
+ }
+
+ @Suppress("DEPRECATION")
+ private fun createFromTextUnit(name: String, value: TextUnit): NodeParameter =
+ when (value.type) {
+ TextUnitType.Sp -> NodeParameter(name, ParameterType.DimensionSp, value.value)
+ TextUnitType.Em -> NodeParameter(name, ParameterType.DimensionEm, value.value)
+ else -> NodeParameter(name, ParameterType.String, "Unspecified")
+ }
+
+ private fun createFromImageVector(name: String, value: ImageVector): NodeParameter =
+ NodeParameter(name, ParameterType.String, value.name)
+
+ /**
+ * Select a resource font among the font in the family to represent the font
+ *
+ * Prefer the font closest to [FontWeight.Normal] and [FontStyle.Normal]
+ */
+ private fun findBestResourceFont(value: FontListFontFamily): ResourceFont? =
+ value.fonts.asSequence().filterIsInstance().minByOrNull {
+ abs(it.weight.weight - FontWeight.Normal.weight) + it.style.value
+ }
+ }
+
+ private class ModifierCollector {
+ val modifiers = mutableListOf()
+ var start: InspectableModifier? = null
+
+ fun add(element: Modifier.Element) =
+ when {
+ element == start?.end -> start = null
+ start != null -> {}
+ else -> {
+ modifiers.add(element)
+ start = element as? InspectableModifier
+ }
+ }
+ }
+}
diff --git a/android/plugins/jetpack-compose/src/main/java/facebook/internal/androidx/compose/ui/inspection/inspector/ReflectionScope.kt b/android/plugins/jetpack-compose/src/main/java/facebook/internal/androidx/compose/ui/inspection/inspector/ReflectionScope.kt
new file mode 100644
index 000000000..8deae6a1b
--- /dev/null
+++ b/android/plugins/jetpack-compose/src/main/java/facebook/internal/androidx/compose/ui/inspection/inspector/ReflectionScope.kt
@@ -0,0 +1,215 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package facebook.internal.androidx.compose.ui.inspection.inspector
+
+import android.annotation.SuppressLint
+import java.lang.reflect.Field
+import java.lang.reflect.Modifier
+import kotlin.jvm.internal.FunctionBase
+import kotlin.jvm.internal.FunctionReference
+import kotlin.jvm.internal.Lambda
+import kotlin.jvm.internal.MutablePropertyReference0
+import kotlin.jvm.internal.MutablePropertyReference1
+import kotlin.jvm.internal.MutablePropertyReference2
+import kotlin.jvm.internal.PropertyReference0
+import kotlin.jvm.internal.PropertyReference1
+import kotlin.jvm.internal.PropertyReference2
+import kotlin.jvm.internal.Reflection
+import kotlin.jvm.internal.ReflectionFactory
+import kotlin.reflect.KClass
+import kotlin.reflect.KClassifier
+import kotlin.reflect.KDeclarationContainer
+import kotlin.reflect.KFunction
+import kotlin.reflect.KMutableProperty0
+import kotlin.reflect.KMutableProperty1
+import kotlin.reflect.KMutableProperty2
+import kotlin.reflect.KProperty0
+import kotlin.reflect.KProperty1
+import kotlin.reflect.KProperty2
+import kotlin.reflect.KType
+import kotlin.reflect.KTypeParameter
+import kotlin.reflect.KTypeProjection
+import kotlin.reflect.KVariance
+import kotlin.reflect.jvm.internal.ReflectionFactoryImpl
+
+/**
+ * Scope that allows to use jarjar-ed kotlin-reflect artifact that is shipped with inspector itself.
+ *
+ * Issue with kotlin-reflect. Many of reflective calls such as "foo::class" rely on static functions
+ * defined in kotlin-stdlib's Reflection.java that delegate to ReflectionFactory. In order to
+ * initialize that factory kotlin-stdlib statically detects presence or absence of kotlin-reflect in
+ * classloader and chooses a factory accordingly. If there is no kotlin-reflect, very limited
+ * version of ReflectionFactory is used.
+ *
+ * It is an issue for inspectors because they could be loaded after that factory is initialised, and
+ * even if they are loaded before, they live in a separate child classloader, thus kotlin-reflect in
+ * inspector wouldn't exist for kotlin-stdlib in app.
+ *
+ * First step to avoid the issue is using ReflectionFactoryImpl that is bundled with inspector. Code
+ * for that would be fairly simple, for example instead of directly calling
+ * `kClass.declaredMemberProperties`, correct instance of kClass should be obtained from factory:
+ * `factory.getOrCreateKotlinClass(kClass.java).declaredMemberProperties`.
+ *
+ * That would work if code that works with correct KClass full implementation would never try to
+ * access a default factory installed in Reflection.java. Unfortunately it is not true, it
+ * eventually calls `CallableReference.getOwner()` in stdlib that uses default factory.
+ *
+ * As a result we have to replace the factory in Reflection.java. To avoid issues with user's code
+ * factory that we setup is smart, by default it simply delegates to a factory that was previously
+ * installed. Only within `reflectionScope.withReflectiveAccess{ }` factory from kotlin-reflect is
+ * used.
+ */
+@SuppressLint("BanUncheckedReflection")
+class ReflectionScope {
+
+ companion object {
+ init {
+ allowHiddenApi()
+ }
+ }
+
+ private val scopedReflectionFactory = installScopedReflectionFactory()
+
+ /** Runs `block` with access to kotlin-reflect. */
+ fun withReflectiveAccess(block: () -> T): T {
+ return scopedReflectionFactory.withMainFactory(block)
+ }
+
+ private fun installScopedReflectionFactory(): ScopedReflectionFactory {
+ val factoryField = Reflection::class.java.getDeclaredField("factory")
+ factoryField.isAccessible = true
+ val original: ReflectionFactory = factoryField.get(null) as ReflectionFactory
+ val modifiersField: Field = Field::class.java.getDeclaredField("accessFlags")
+ modifiersField.isAccessible = true
+ // make field non-final 😅 b/179685774 https://youtrack.jetbrains.com/issue/KT-44795
+ modifiersField.setInt(factoryField, factoryField.modifiers and Modifier.FINAL.inv())
+ val scopedReflectionFactory = ScopedReflectionFactory(original)
+ factoryField.set(null, scopedReflectionFactory)
+ return scopedReflectionFactory
+ }
+}
+
+@SuppressLint("BanUncheckedReflection")
+private fun allowHiddenApi() {
+ try {
+ val vmDebug = Class.forName("dalvik.system.VMDebug")
+ val allowHiddenApiReflectionFrom =
+ vmDebug.getDeclaredMethod("allowHiddenApiReflectionFrom", Class::class.java)
+ allowHiddenApiReflectionFrom.invoke(null, ReflectionScope::class.java)
+ } catch (e: Throwable) {
+ // ignore failure, let's try to proceed without it
+ }
+}
+
+private class ScopedReflectionFactory(
+ private val original: ReflectionFactory,
+) : ReflectionFactory() {
+ private val mainFactory = ReflectionFactoryImpl()
+ private val threadLocalFactory = ThreadLocal()
+
+ fun withMainFactory(block: () -> T): T {
+ threadLocalFactory.set(mainFactory)
+ try {
+ return block()
+ } finally {
+ threadLocalFactory.set(null)
+ }
+ }
+
+ val factory: ReflectionFactory
+ get() = threadLocalFactory.get() ?: original
+
+ override fun createKotlinClass(javaClass: Class<*>?): KClass<*> {
+ return factory.createKotlinClass(javaClass)
+ }
+
+ override fun createKotlinClass(javaClass: Class<*>?, internalName: String?): KClass<*> {
+ return factory.createKotlinClass(javaClass, internalName)
+ }
+
+ override fun getOrCreateKotlinPackage(
+ javaClass: Class<*>?,
+ moduleName: String?
+ ): KDeclarationContainer {
+ return factory.getOrCreateKotlinPackage(javaClass, moduleName)
+ }
+
+ override fun getOrCreateKotlinClass(javaClass: Class<*>?): KClass<*> {
+ return factory.getOrCreateKotlinClass(javaClass)
+ }
+
+ override fun getOrCreateKotlinClass(javaClass: Class<*>?, internalName: String?): KClass<*> {
+ return factory.getOrCreateKotlinClass(javaClass, internalName)
+ }
+
+ override fun renderLambdaToString(lambda: Lambda<*>?): String {
+ return factory.renderLambdaToString(lambda)
+ }
+
+ override fun renderLambdaToString(lambda: FunctionBase<*>?): String {
+ return factory.renderLambdaToString(lambda)
+ }
+
+ override fun function(f: FunctionReference?): KFunction<*> {
+ return factory.function(f)
+ }
+
+ override fun property0(p: PropertyReference0?): KProperty0<*> {
+ return factory.property0(p)
+ }
+
+ override fun mutableProperty0(p: MutablePropertyReference0?): KMutableProperty0<*> {
+ return factory.mutableProperty0(p)
+ }
+
+ override fun property1(p: PropertyReference1?): KProperty1<*, *> {
+ return factory.property1(p)
+ }
+
+ override fun mutableProperty1(p: MutablePropertyReference1?): KMutableProperty1<*, *> {
+ return factory.mutableProperty1(p)
+ }
+
+ override fun property2(p: PropertyReference2?): KProperty2<*, *, *> {
+ return factory.property2(p)
+ }
+
+ override fun mutableProperty2(p: MutablePropertyReference2?): KMutableProperty2<*, *, *> {
+ return factory.mutableProperty2(p)
+ }
+
+ override fun typeOf(
+ klass: KClassifier?,
+ arguments: MutableList?,
+ isMarkedNullable: Boolean
+ ): KType {
+ return factory.typeOf(klass, arguments, isMarkedNullable)
+ }
+
+ override fun typeParameter(
+ container: Any?,
+ name: String?,
+ variance: KVariance?,
+ isReified: Boolean
+ ): KTypeParameter {
+ return factory.typeParameter(container, name, variance, isReified)
+ }
+
+ override fun setUpperBounds(typeParameter: KTypeParameter?, bounds: MutableList?) {
+ factory.setUpperBounds(typeParameter, bounds)
+ }
+}
diff --git a/android/plugins/jetpack-compose/src/main/java/facebook/internal/androidx/compose/ui/inspection/util/AnchorMap.kt b/android/plugins/jetpack-compose/src/main/java/facebook/internal/androidx/compose/ui/inspection/util/AnchorMap.kt
new file mode 100644
index 000000000..b7af6df91
--- /dev/null
+++ b/android/plugins/jetpack-compose/src/main/java/facebook/internal/androidx/compose/ui/inspection/util/AnchorMap.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package facebook.internal.androidx.compose.ui.inspection.util
+
+import java.util.IdentityHashMap
+
+const val NO_ANCHOR_ID = 0
+
+/** A map of anchors with a unique id generator. */
+class AnchorMap {
+ private val anchorLookup = mutableMapOf()
+ private val idLookup = IdentityHashMap()
+
+ /** Return a unique id for the specified [anchor] instance. */
+ operator fun get(anchor: Any?): Int =
+ anchor?.let { idLookup.getOrPut(it) { generateUniqueId(it) } } ?: NO_ANCHOR_ID
+
+ /** Return the anchor associated with a given unique anchor [id]. */
+ operator fun get(id: Int): Any? = anchorLookup[id]
+
+ private fun generateUniqueId(anchor: Any): Int {
+ var id = anchor.hashCode()
+ while (id == NO_ANCHOR_ID || anchorLookup.containsKey(id)) {
+ id++
+ }
+ anchorLookup[id] = anchor
+ return id
+ }
+}
diff --git a/android/plugins/jetpack-compose/src/main/java/facebook/internal/androidx/compose/ui/inspection/util/IntArray.kt b/android/plugins/jetpack-compose/src/main/java/facebook/internal/androidx/compose/ui/inspection/util/IntArray.kt
new file mode 100644
index 000000000..8287eb7a3
--- /dev/null
+++ b/android/plugins/jetpack-compose/src/main/java/facebook/internal/androidx/compose/ui/inspection/util/IntArray.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package facebook.internal.androidx.compose.ui.inspection.util
+
+private val EMPTY_INT_ARRAY = intArrayOf()
+
+fun List.asIntArray() = if (isNotEmpty()) toIntArray() else EMPTY_INT_ARRAY
diff --git a/android/plugins/jetpack-compose/src/main/java/facebook/internal/androidx/compose/ui/inspection/util/ThreadUtils.kt b/android/plugins/jetpack-compose/src/main/java/facebook/internal/androidx/compose/ui/inspection/util/ThreadUtils.kt
new file mode 100644
index 000000000..80320d9f7
--- /dev/null
+++ b/android/plugins/jetpack-compose/src/main/java/facebook/internal/androidx/compose/ui/inspection/util/ThreadUtils.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package facebook.internal.androidx.compose.ui.inspection.util
+
+import android.os.Handler
+import android.os.Looper
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.Future
+
+object ThreadUtils {
+ fun assertOnMainThread() {
+ if (!Looper.getMainLooper().isCurrentThread) {
+ error("This work is required on the main thread")
+ }
+ }
+
+ fun assertOffMainThread() {
+ if (Looper.getMainLooper().isCurrentThread) {
+ error("This work is required off the main thread")
+ }
+ }
+
+ /**
+ * Run some logic on the main thread, returning a future that will contain any data computed by
+ * and returned from the block.
+ *
+ * If this method is called from the main thread, it will run immediately.
+ */
+ fun runOnMainThread(block: () -> T): Future {
+ return if (!Looper.getMainLooper().isCurrentThread) {
+ val future = CompletableFuture()
+ Handler.createAsync(Looper.getMainLooper()).post { future.complete(block()) }
+ future
+ } else {
+ CompletableFuture.completedFuture(block())
+ }
+ }
+}
diff --git a/android/plugins/leakcanary/build.gradle b/android/plugins/leakcanary/build.gradle
index 0c6388fb3..12bd46933 100644
--- a/android/plugins/leakcanary/build.gradle
+++ b/android/plugins/leakcanary/build.gradle
@@ -8,6 +8,7 @@
apply plugin: 'com.android.library'
android {
+ namespace 'com.facebook.flipper.plugins.leakcanary'
compileSdkVersion rootProject.compileSdkVersion
buildToolsVersion rootProject.buildToolsVersion
diff --git a/android/plugins/leakcanary2/build.gradle b/android/plugins/leakcanary2/build.gradle
index bfc6c47a2..def2cf57d 100644
--- a/android/plugins/leakcanary2/build.gradle
+++ b/android/plugins/leakcanary2/build.gradle
@@ -7,10 +7,10 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
-apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
android {
+ namespace 'com.facebook.flipper.plugins.leakcanary2'
compileSdkVersion rootProject.compileSdkVersion
buildToolsVersion rootProject.buildToolsVersion
@@ -19,8 +19,13 @@ android {
targetSdkVersion rootProject.targetSdkVersion
}
+ compileOptions {
+ targetCompatibility rootProject.javaTargetVersion
+ sourceCompatibility rootProject.javaTargetVersion
+ }
+
dependencies {
- compileOnly "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$KOTLIN_VERSION"
+ compileOnly "org.jetbrains.kotlin:kotlin-stdlib:$KOTLIN_VERSION"
implementation project(':android')
compileOnly deps.leakcanary2
compileOnly deps.jsr305
diff --git a/android/plugins/leakcanary2/src/main/java/com/facebook/flipper/plugins/leakcanary2/FlipperLeakEventListener.kt b/android/plugins/leakcanary2/src/main/java/com/facebook/flipper/plugins/leakcanary2/FlipperLeakEventListener.kt
new file mode 100644
index 000000000..3eeae988a
--- /dev/null
+++ b/android/plugins/leakcanary2/src/main/java/com/facebook/flipper/plugins/leakcanary2/FlipperLeakEventListener.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+package com.facebook.flipper.plugins.leakcanary2
+
+import com.facebook.flipper.android.AndroidFlipperClient
+import leakcanary.EventListener
+import shark.HeapAnalysis
+import shark.HeapAnalysisSuccess
+
+class FlipperLeakEventListener : EventListener {
+ private val leaks: MutableList = mutableListOf()
+
+ override fun onEvent(event: EventListener.Event) {
+ if (event is EventListener.Event.HeapAnalysisDone.HeapAnalysisSucceeded) {
+ val heapAnalysis = event.heapAnalysis
+ leaks.addAll(heapAnalysis.toLeakList())
+
+ AndroidFlipperClient.getInstanceIfInitialized()?.let { client ->
+ (client.getPlugin(LeakCanary2FlipperPlugin.ID) as? LeakCanary2FlipperPlugin)?.reportLeaks(
+ leaks)
+ }
+ }
+ }
+
+ private fun HeapAnalysis.toLeakList(): List {
+ return if (this is HeapAnalysisSuccess) {
+ allLeaks
+ .mapNotNull {
+ if (it.leakTraces.isNotEmpty()) {
+ it.leakTraces[0].toLeak(it.shortDescription)
+ } else {
+ null
+ }
+ }
+ .toList()
+ } else {
+ emptyList()
+ }
+ }
+}
diff --git a/android/plugins/leakcanary2/src/main/java/com/facebook/flipper/plugins/leakcanary2/FlipperLeakListener.kt b/android/plugins/leakcanary2/src/main/java/com/facebook/flipper/plugins/leakcanary2/FlipperLeakListener.kt
index 660edc7e2..ffc2c3df7 100644
--- a/android/plugins/leakcanary2/src/main/java/com/facebook/flipper/plugins/leakcanary2/FlipperLeakListener.kt
+++ b/android/plugins/leakcanary2/src/main/java/com/facebook/flipper/plugins/leakcanary2/FlipperLeakListener.kt
@@ -13,6 +13,7 @@ import leakcanary.OnHeapAnalyzedListener
import shark.HeapAnalysis
import shark.HeapAnalysisSuccess
+@Deprecated("Use FlipperLeakEventListener add to LeakCanary.config.eventListeners instead")
class FlipperLeakListener : OnHeapAnalyzedListener {
private val leaks: MutableList = mutableListOf()
diff --git a/android/plugins/litho/build.gradle b/android/plugins/litho/build.gradle
index e9f636e3c..4ca8b0e45 100644
--- a/android/plugins/litho/build.gradle
+++ b/android/plugins/litho/build.gradle
@@ -6,8 +6,10 @@
*/
apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
android {
+ namespace 'com.facebook.flipper.plugins.litho'
compileSdkVersion rootProject.compileSdkVersion
buildToolsVersion rootProject.buildToolsVersion
@@ -16,9 +18,15 @@ android {
targetSdkVersion rootProject.targetSdkVersion
}
+ compileOptions {
+ targetCompatibility rootProject.javaTargetVersion
+ sourceCompatibility rootProject.javaTargetVersion
+ }
+
dependencies {
compileOnly deps.lithoAnnotations
implementation project(':android')
+ implementation deps.kotlinCoroutinesAndroid
implementation deps.lithoCore
api deps.lithoEditorCore
api(deps.lithoEditorFlipper) {
diff --git a/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/UIDebuggerLithoSupport.kt b/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/UIDebuggerLithoSupport.kt
new file mode 100644
index 000000000..0101dc728
--- /dev/null
+++ b/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/UIDebuggerLithoSupport.kt
@@ -0,0 +1,126 @@
+/*
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+package com.facebook.flipper.plugins.uidebugger.litho
+
+import com.facebook.flipper.plugins.uidebugger.core.ConnectionListener
+import com.facebook.flipper.plugins.uidebugger.core.UIDContext
+import com.facebook.flipper.plugins.uidebugger.descriptors.DescriptorRegister
+import com.facebook.flipper.plugins.uidebugger.litho.descriptors.*
+import com.facebook.flipper.plugins.uidebugger.model.FrameworkEvent
+import com.facebook.flipper.plugins.uidebugger.model.FrameworkEventMetadata
+import com.facebook.litho.ComponentTree
+import com.facebook.litho.DebugComponent
+import com.facebook.litho.LithoView
+import com.facebook.litho.MatrixDrawable
+import com.facebook.litho.debug.LithoDebugEvent
+import com.facebook.litho.widget.TextDrawable
+import com.facebook.rendercore.debug.DebugEvent
+import com.facebook.rendercore.debug.DebugEventBus
+import com.facebook.rendercore.debug.DebugEventSubscriber
+import com.facebook.rendercore.debug.DebugMarkerEvent
+import com.facebook.rendercore.debug.DebugProcessEvent
+import com.facebook.rendercore.debug.Duration
+
+const val LithoTag = "Litho"
+const val LithoMountableTag = "LithoMountable"
+
+object UIDebuggerLithoSupport {
+
+ fun enable(context: UIDContext) {
+ addDescriptors(context.descriptorRegister)
+
+ val eventMeta =
+ listOf(
+ // Litho
+ FrameworkEventMetadata(
+ LithoDebugEvent.StateUpdateEnqueued,
+ "Set state was called, this will trigger resolve and then possibly layout and mount"),
+ FrameworkEventMetadata(
+ LithoDebugEvent.RenderRequest,
+ "A request to render the component tree again. It can be requested due to 1) set root 2) state update 3) size change or measurement"),
+ FrameworkEventMetadata(
+ LithoDebugEvent.ComponentTreeResolve,
+ "ComponentTree resolved the hierarchy into a LayoutState, non layout nodes are removed, see attributes for source of execution"),
+ FrameworkEventMetadata(
+ LithoDebugEvent.LayoutCommitted,
+ "A new layout state created (resolved and measured result) being committed; this layout state could get mounted next."),
+
+ // RenderCore
+
+ FrameworkEventMetadata(
+ DebugEvent.RenderTreeMounted, "The mount phase for the entire render tree"),
+ FrameworkEventMetadata(
+ DebugEvent.RenderUnitMounted,
+ "Component was added into the view hierarchy (this doesn't mean it is visible)"),
+ FrameworkEventMetadata(
+ DebugEvent.RenderUnitUpdated,
+ "The properties of a component's content were were rebinded"),
+ FrameworkEventMetadata(
+ DebugEvent.RenderUnitUnmounted, "Component was removed from the view hierarchy"),
+ FrameworkEventMetadata(DebugEvent.RenderUnitOnVisible, "Component became visible"),
+ FrameworkEventMetadata(DebugEvent.RenderUnitOnInvisible, "Component became invisible"),
+ )
+
+ val eventForwarder =
+ object : DebugEventSubscriber(*eventMeta.map { it.type }.toTypedArray()) {
+ override fun onEvent(event: DebugEvent) {
+ val timestamp =
+ when (event) {
+ is DebugMarkerEvent -> event.timestamp
+ is DebugProcessEvent -> event.timestamp
+ }
+ val treeId = event.renderStateId.toIntOrNull() ?: -1
+
+ val globalKey =
+ event.attributeOrNull("key")?.let {
+ DebugComponent.generateGlobalKey(treeId, it).hashCode()
+ }
+ val duration = event.attributeOrNull("duration")
+
+ val attributes = mutableMapOf()
+ val source =
+ event.attributeOrNull(
+ "source") // todo replace magic strings with DebugEventAttribute.Source once
+ // litho open source is released
+ if (source != null) {
+ attributes["source"] = source
+ }
+ context.addFrameworkEvent(
+ FrameworkEvent(
+ treeId,
+ globalKey ?: treeId,
+ event.type,
+ timestamp,
+ duration?.value,
+ event.threadName,
+ attributes))
+ }
+ }
+
+ context.connectionListeners.add(
+ object : ConnectionListener {
+ override fun onConnect() {
+ DebugEventBus.subscribe(eventForwarder)
+ }
+
+ override fun onDisconnect() {
+ DebugEventBus.unsubscribe(eventForwarder)
+ }
+ })
+
+ context.frameworkEventMetadata.addAll(eventMeta)
+ }
+
+ private fun addDescriptors(register: DescriptorRegister) {
+ register.register(LithoView::class.java, LithoViewDescriptor)
+ register.register(DebugComponent::class.java, DebugComponentDescriptor(register))
+ register.register(TextDrawable::class.java, TextDrawableDescriptor)
+ register.register(MatrixDrawable::class.java, MatrixDrawableDescriptor)
+ register.register(ComponentTree::class.java, ComponentTreeDescriptor(register))
+ }
+}
diff --git a/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/descriptors/ComponentTreeDescriptor.kt b/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/descriptors/ComponentTreeDescriptor.kt
new file mode 100644
index 000000000..22bd068a1
--- /dev/null
+++ b/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/descriptors/ComponentTreeDescriptor.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+package com.facebook.flipper.plugins.uidebugger.litho.descriptors
+
+import com.facebook.flipper.plugins.uidebugger.descriptors.DescriptorRegister
+import com.facebook.flipper.plugins.uidebugger.descriptors.Id
+import com.facebook.flipper.plugins.uidebugger.descriptors.NodeDescriptor
+import com.facebook.flipper.plugins.uidebugger.descriptors.OffsetChild
+import com.facebook.flipper.plugins.uidebugger.litho.LithoTag
+import com.facebook.flipper.plugins.uidebugger.model.Bounds
+import com.facebook.flipper.plugins.uidebugger.model.InspectableObject
+import com.facebook.flipper.plugins.uidebugger.model.MetadataId
+import com.facebook.flipper.plugins.uidebugger.util.Immediate
+import com.facebook.flipper.plugins.uidebugger.util.MaybeDeferred
+import com.facebook.litho.ComponentTree
+import com.facebook.litho.DebugComponent
+
+class ComponentTreeDescriptor(val register: DescriptorRegister) : NodeDescriptor {
+
+ private val qualifiedName = ComponentTree::class.qualifiedName ?: ""
+
+ override fun getId(node: ComponentTree): Id = node.id
+
+ override fun getBounds(node: ComponentTree): Bounds {
+ val rootComponent = DebugComponent.getRootInstance(node)
+ return if (rootComponent != null) {
+ Bounds.fromRect(rootComponent.boundsInParentDebugComponent)
+ } else {
+ Bounds(0, 0, 0, 0)
+ }
+ }
+
+ override fun getName(node: ComponentTree): String = "ComponentTree"
+
+ override fun getQualifiedName(node: ComponentTree): String = qualifiedName
+
+ override fun getChildren(node: ComponentTree): List {
+ val result = mutableListOf()
+ val debugComponent = DebugComponent.getRootInstance(node)
+ if (debugComponent != null) {
+ result.add(
+ // we want the component tree to take the size and any offset so we reset this one
+ OffsetChild.zero(
+ debugComponent, register.descriptorForClassUnsafe(debugComponent.javaClass)))
+ }
+ return result
+ }
+
+ override fun getActiveChild(node: ComponentTree): Any? = null
+
+ override fun getAttributes(
+ node: ComponentTree
+ ): MaybeDeferred> {
+ return Immediate(mapOf())
+ }
+
+ override fun getTags(node: ComponentTree): Set = setOf(LithoTag, "TreeRoot")
+}
diff --git a/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/descriptors/DebugComponentDescriptor.kt b/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/descriptors/DebugComponentDescriptor.kt
new file mode 100644
index 000000000..6de5e232d
--- /dev/null
+++ b/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/descriptors/DebugComponentDescriptor.kt
@@ -0,0 +1,188 @@
+/*
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+package com.facebook.flipper.plugins.uidebugger.litho.descriptors
+
+import android.graphics.Bitmap
+import com.facebook.flipper.plugins.uidebugger.descriptors.DescriptorRegister
+import com.facebook.flipper.plugins.uidebugger.descriptors.Id
+import com.facebook.flipper.plugins.uidebugger.descriptors.MetadataRegister
+import com.facebook.flipper.plugins.uidebugger.descriptors.NodeDescriptor
+import com.facebook.flipper.plugins.uidebugger.descriptors.OffsetChild
+import com.facebook.flipper.plugins.uidebugger.litho.LithoMountableTag
+import com.facebook.flipper.plugins.uidebugger.litho.LithoTag
+import com.facebook.flipper.plugins.uidebugger.litho.descriptors.props.ComponentDataExtractor
+import com.facebook.flipper.plugins.uidebugger.litho.descriptors.props.LayoutPropExtractor
+import com.facebook.flipper.plugins.uidebugger.model.Bounds
+import com.facebook.flipper.plugins.uidebugger.model.Inspectable
+import com.facebook.flipper.plugins.uidebugger.model.InspectableObject
+import com.facebook.flipper.plugins.uidebugger.model.InspectableValue
+import com.facebook.flipper.plugins.uidebugger.model.MetadataId
+import com.facebook.flipper.plugins.uidebugger.util.Deferred
+import com.facebook.flipper.plugins.uidebugger.util.MaybeDeferred
+import com.facebook.litho.Component
+import com.facebook.litho.DebugComponent
+import com.facebook.rendercore.FastMath
+import com.facebook.yoga.YogaEdge
+
+class DebugComponentDescriptor(val register: DescriptorRegister) : NodeDescriptor {
+ private val NAMESPACE = "DebugComponent"
+
+ /*
+ * Debug component is generated on the fly so use the underlying component instance which is
+ * immutable
+ */
+ override fun getId(node: DebugComponent): Id = node.globalKey.hashCode()
+
+ override fun getName(node: DebugComponent): String = node.component.simpleName
+
+ override fun getQualifiedName(node: com.facebook.litho.DebugComponent): String =
+ node.component::class.qualifiedName ?: ""
+
+ override fun getChildren(node: DebugComponent): List {
+ val result = mutableListOf()
+
+ val mountedContent = node.mountedContent
+
+ if (mountedContent == null) {
+ for (child in node.childComponents) {
+ result.add(child)
+ }
+ } else {
+
+ val layoutNode = node.layoutNode
+ val descriptor: NodeDescriptor =
+ register.descriptorForClassUnsafe(mountedContent.javaClass)
+ // mountables are always layout nodes
+ if (layoutNode != null) {
+ /**
+ * We need to override the mounted contents offset since the mounted contents android bounds
+ * are w.r.t its native parent but we want it w.r.t to the mountable.
+ *
+ * However padding on a mountable means that the content is inset within the mountables
+ * bounds so we need to adjust for this
+ */
+ result.add(
+ OffsetChild(
+ child = mountedContent,
+ descriptor = descriptor,
+ x = layoutNode.getLayoutPadding(YogaEdge.LEFT).let { FastMath.round(it) },
+ y = layoutNode.getLayoutPadding(YogaEdge.TOP).let { FastMath.round(it) },
+ ))
+ }
+ }
+
+ return result
+ }
+
+ override fun getActiveChild(node: DebugComponent): Any? = null
+
+ private val LayoutId =
+ MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "Litho Layout")
+
+ private val UserPropsId =
+ MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "Litho Props")
+
+ private val StateId =
+ MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "Litho State")
+
+ private val MountingDataId =
+ MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "Mount State")
+
+ private val isMountedAttributeId =
+ MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "mounted")
+
+ private val isVisibleAttributeId =
+ MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "visible")
+
+ override fun getAttributes(
+ node: DebugComponent
+ ): MaybeDeferred> {
+ return Deferred {
+ val attributeSections = mutableMapOf()
+
+ val mountingData = getMountingData(node)
+ attributeSections[MountingDataId] = InspectableObject(mountingData)
+
+ val layoutProps = LayoutPropExtractor.getProps(node)
+ attributeSections[LayoutId] = InspectableObject(layoutProps.toMap())
+
+ if (!node.canResolve()) {
+ val stateContainer = node.stateContainer
+ if (stateContainer != null) {
+ attributeSections[StateId] =
+ ComponentDataExtractor.getState(stateContainer, node.component.simpleName)
+ }
+
+ val props = ComponentDataExtractor.getProps(node.component)
+
+ attributeSections[UserPropsId] = InspectableObject(props.toMap())
+ }
+
+ attributeSections
+ }
+ }
+
+ override fun getBounds(node: DebugComponent): Bounds =
+ Bounds.fromRect(node.boundsInParentDebugComponent)
+
+ override fun getTags(node: DebugComponent): Set {
+ val tags = mutableSetOf(LithoTag)
+
+ if (node.component.mountType != Component.MountType.NONE) {
+ tags.add(LithoMountableTag)
+ }
+ return tags
+ }
+
+ override fun getSnapshot(node: DebugComponent, bitmap: Bitmap?): Bitmap? = null
+
+ override fun getInlineAttributes(node: DebugComponent): Map {
+ val attributes = mutableMapOf()
+ val key = node.key
+ val testKey = node.testKey
+ if (key != null && key.trim { it <= ' ' }.length > 0) {
+ attributes["key"] = key
+ }
+ if (testKey != null && testKey.trim { it <= ' ' }.length > 0) {
+ attributes["testKey"] = testKey
+ }
+ return attributes
+ }
+
+ private fun getMountingData(node: DebugComponent): Map {
+
+ val lithoView = node.lithoView
+ val mountingData = mutableMapOf()
+
+ if (lithoView == null) {
+ return mountingData
+ }
+
+ val mountState = lithoView.mountDelegateTarget ?: return mountingData
+ val componentTree = lithoView.componentTree ?: return mountingData
+
+ val component = node.component
+
+ if (component.mountType != Component.MountType.NONE) {
+ val renderUnit = DebugComponent.getRenderUnit(node, componentTree)
+ if (renderUnit != null) {
+ val renderUnitId = renderUnit.id
+ val isMounted = mountState.getContentById(renderUnitId) != null
+ mountingData[isMountedAttributeId] = InspectableValue.Boolean(isMounted)
+ }
+ }
+
+ val visibilityOutput = DebugComponent.getVisibilityOutput(node, componentTree)
+ if (visibilityOutput != null) {
+ val isVisible = DebugComponent.isVisible(node, lithoView)
+ mountingData[isVisibleAttributeId] = InspectableValue.Boolean(isVisible)
+ }
+
+ return mountingData
+ }
+}
diff --git a/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/descriptors/LithoViewDescriptor.kt b/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/descriptors/LithoViewDescriptor.kt
new file mode 100644
index 000000000..26ffe7b36
--- /dev/null
+++ b/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/descriptors/LithoViewDescriptor.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+package com.facebook.flipper.plugins.uidebugger.litho.descriptors
+
+import com.facebook.flipper.plugins.uidebugger.descriptors.ChainedDescriptor
+import com.facebook.flipper.plugins.uidebugger.descriptors.MetadataRegister
+import com.facebook.flipper.plugins.uidebugger.model.InspectableObject
+import com.facebook.flipper.plugins.uidebugger.model.InspectableValue
+import com.facebook.flipper.plugins.uidebugger.model.MetadataId
+import com.facebook.litho.LithoView
+
+object LithoViewDescriptor : ChainedDescriptor() {
+
+ private const val NAMESPACE = "LithoView"
+ private val SectionId =
+ MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, NAMESPACE)
+
+ override fun onGetName(node: LithoView): String = node.javaClass.simpleName
+
+ override fun onGetChildren(node: LithoView): List {
+ val componentTree = node.componentTree
+ if (componentTree != null) {
+ return listOf(componentTree)
+ }
+
+ return listOf()
+ }
+
+ private val IsIncrementalMountEnabledAttributeId =
+ MetadataRegister.register(
+ MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "isIncrementalMountEnabled")
+
+ override fun onGetAttributes(
+ node: LithoView,
+ attributeSections: MutableMap
+ ) {
+ attributeSections[SectionId] =
+ InspectableObject(
+ mapOf(
+ IsIncrementalMountEnabledAttributeId to
+ InspectableValue.Boolean(node.isIncrementalMountEnabled)))
+ }
+}
diff --git a/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/descriptors/MatrixDrawableDescriptor.kt b/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/descriptors/MatrixDrawableDescriptor.kt
new file mode 100644
index 000000000..d8f9117ab
--- /dev/null
+++ b/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/descriptors/MatrixDrawableDescriptor.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+package com.facebook.flipper.plugins.uidebugger.litho.descriptors
+
+import com.facebook.flipper.plugins.uidebugger.descriptors.ChainedDescriptor
+import com.facebook.litho.MatrixDrawable
+
+object MatrixDrawableDescriptor : ChainedDescriptor>() {
+
+ override fun onGetChildren(node: MatrixDrawable<*>): List? {
+ val mountedDrawable = node.mountedDrawable
+ return if (mountedDrawable != null) {
+ listOf(mountedDrawable)
+ } else {
+ listOf()
+ }
+ }
+
+ override fun onGetName(node: MatrixDrawable<*>): String = node.javaClass.simpleName
+}
diff --git a/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/descriptors/TextDrawableDescriptor.kt b/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/descriptors/TextDrawableDescriptor.kt
new file mode 100644
index 000000000..0ec132b6e
--- /dev/null
+++ b/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/descriptors/TextDrawableDescriptor.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+package com.facebook.flipper.plugins.uidebugger.litho.descriptors
+
+import com.facebook.flipper.plugins.uidebugger.descriptors.ChainedDescriptor
+import com.facebook.flipper.plugins.uidebugger.descriptors.MetadataRegister
+import com.facebook.flipper.plugins.uidebugger.model.Inspectable
+import com.facebook.flipper.plugins.uidebugger.model.InspectableObject
+import com.facebook.flipper.plugins.uidebugger.model.InspectableValue
+import com.facebook.flipper.plugins.uidebugger.model.MetadataId
+import com.facebook.litho.widget.TextDrawable
+
+object TextDrawableDescriptor : ChainedDescriptor() {
+
+ private const val NAMESPACE = "TextDrawable"
+ private val SectionId =
+ MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, NAMESPACE)
+
+ override fun onGetName(node: TextDrawable): String = node.javaClass.simpleName
+
+ private val TextAttributeId =
+ MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "text")
+
+ override fun onGetAttributes(
+ node: TextDrawable,
+ attributeSections: MutableMap
+ ) {
+ val props =
+ mapOf(TextAttributeId to InspectableValue.Text(node.text.toString()))
+
+ attributeSections[SectionId] = InspectableObject(props)
+ }
+}
diff --git a/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/descriptors/props/ComponentDataExtractor.kt b/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/descriptors/props/ComponentDataExtractor.kt
new file mode 100644
index 000000000..a07286d0e
--- /dev/null
+++ b/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/descriptors/props/ComponentDataExtractor.kt
@@ -0,0 +1,187 @@
+/*
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+package com.facebook.flipper.plugins.uidebugger.litho.descriptors.props
+
+import android.graphics.drawable.ColorDrawable
+import android.graphics.drawable.Drawable
+import android.util.Log
+import com.facebook.flipper.plugins.uidebugger.LogTag
+import com.facebook.flipper.plugins.uidebugger.descriptors.MetadataRegister
+import com.facebook.flipper.plugins.uidebugger.model.*
+import com.facebook.litho.Component
+import com.facebook.litho.SpecGeneratedComponent
+import com.facebook.litho.StateContainer
+import com.facebook.litho.annotations.Prop
+import com.facebook.litho.annotations.ResType
+import com.facebook.litho.annotations.State
+import com.facebook.litho.editor.EditorRegistry
+import com.facebook.litho.editor.model.EditorArray
+import com.facebook.litho.editor.model.EditorBool
+import com.facebook.litho.editor.model.EditorColor
+import com.facebook.litho.editor.model.EditorNumber
+import com.facebook.litho.editor.model.EditorPick
+import com.facebook.litho.editor.model.EditorShape
+import com.facebook.litho.editor.model.EditorString
+import com.facebook.litho.editor.model.EditorValue
+import com.facebook.litho.editor.model.EditorValue.EditorVisitor
+
+object ComponentDataExtractor {
+
+ fun getProps(component: Component): Map {
+ val props = mutableMapOf()
+
+ val isSpecComponent = component is SpecGeneratedComponent
+
+ for (declaredField in component.javaClass.declaredFields) {
+ declaredField.isAccessible = true
+
+ val name = declaredField.name
+ val declaredFieldAnnotation = declaredField.getAnnotation(Prop::class.java)
+
+ // Only expose `@Prop` annotated fields for Spec components
+ if (isSpecComponent && declaredFieldAnnotation == null) {
+ continue
+ }
+
+ val prop =
+ try {
+ declaredField[component]
+ } catch (e: IllegalAccessException) {
+ continue
+ }
+
+ if (declaredFieldAnnotation != null) {
+ val resType = declaredFieldAnnotation.resType
+ if (resType == ResType.COLOR) {
+ if (prop != null) {
+ val identifier = getMetadataId(component.simpleName, name)
+ props[identifier] = InspectableValue.Color(Color.fromColor(prop as Int))
+ }
+ continue
+ } else if (resType == ResType.DRAWABLE) {
+ val identifier = getMetadataId(component.simpleName, name)
+ props[identifier] = fromDrawable(prop as Drawable?)
+ continue
+ }
+ }
+
+ val editorValue =
+ try {
+ EditorRegistry.read(declaredField.type, declaredField, component)
+ } catch (e: Exception) {
+ Log.d(
+ LogTag,
+ "Unable to retrieve prop ${declaredField.name} on type ${component.simpleName}")
+ EditorString("error fetching prop")
+ }
+
+ if (editorValue != null) {
+ addProp(props, component.simpleName, name, editorValue)
+ }
+ }
+
+ return props
+ }
+
+ fun getState(stateContainer: StateContainer, componentName: String): InspectableObject {
+
+ val stateFields = mutableMapOf()
+ for (field in stateContainer.javaClass.declaredFields) {
+ field.isAccessible = true
+ val stateAnnotation = field.getAnnotation(State::class.java)
+ val isKStateField = field.name == "states"
+ if (stateAnnotation != null || isKStateField) {
+ val id = getMetadataId(componentName, field.name)
+ val editorValue: EditorValue? = EditorRegistry.read(field.type, field, stateContainer)
+ if (editorValue != null) {
+ stateFields[id] = toInspectable(field.name, editorValue)
+ }
+ }
+ }
+ return InspectableObject(stateFields)
+ }
+
+ private fun getMetadataId(
+ namespace: String,
+ key: String,
+ mutable: Boolean = false,
+ possibleValues: Set? = emptySet()
+ ): MetadataId {
+ val metadata = MetadataRegister.get(namespace, key)
+ val identifier =
+ metadata?.id
+ ?: MetadataRegister.register(
+ MetadataRegister.TYPE_ATTRIBUTE, namespace, key, mutable, possibleValues)
+ return identifier
+ }
+
+ private fun addProp(
+ props: MutableMap,
+ namespace: String,
+ name: String,
+ value: EditorValue
+ ) {
+ var possibleValues: MutableSet? = null
+ if (value is EditorPick) {
+ possibleValues = mutableSetOf()
+ value.values.forEach { possibleValues.add(InspectableValue.Text(it)) }
+ }
+
+ val identifier = getMetadataId(namespace, name, false, possibleValues)
+ props[identifier] = toInspectable(name, value)
+ }
+
+ private fun toInspectable(name: String, editorValue: EditorValue): Inspectable {
+ return editorValue.`when`(
+ object : EditorVisitor {
+ override fun isShape(shape: EditorShape): Inspectable {
+
+ val fields = mutableMapOf()
+ shape.value.entries.forEach { entry ->
+ val value = toInspectable(entry.key, entry.value)
+
+ val shapeEditorValue = entry.value
+ var possibleValues: MutableSet? = null
+ if (shapeEditorValue is EditorPick) {
+ possibleValues = mutableSetOf()
+ shapeEditorValue.values.forEach { possibleValues.add(InspectableValue.Text(it)) }
+ }
+
+ val identifier = getMetadataId(name, entry.key, false, possibleValues)
+ fields[identifier] = value
+ }
+
+ return InspectableObject(fields)
+ }
+
+ override fun isArray(array: EditorArray?): Inspectable {
+ val values = array?.value?.map { value -> toInspectable(name, value) }
+ return InspectableArray(values ?: listOf())
+ }
+
+ override fun isPick(pick: EditorPick): Inspectable = InspectableValue.Enum(pick.selected)
+
+ override fun isNumber(number: EditorNumber): Inspectable =
+ InspectableValue.Number(number.value)
+
+ override fun isColor(number: EditorColor): Inspectable =
+ InspectableValue.Color(number.value.toInt().let { Color.fromColor(it) })
+
+ override fun isString(string: EditorString): Inspectable =
+ InspectableValue.Text(string.value ?: "")
+
+ override fun isBool(bool: EditorBool): Inspectable = InspectableValue.Boolean(bool.value)
+ })
+ }
+
+ private fun fromDrawable(d: Drawable?): Inspectable =
+ when (d) {
+ is ColorDrawable -> InspectableValue.Color(Color.fromColor(d.color))
+ else -> InspectableValue.Unknown(d.toString())
+ }
+}
diff --git a/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/descriptors/props/LayoutPropExtractor.kt b/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/descriptors/props/LayoutPropExtractor.kt
new file mode 100644
index 000000000..a2f2ef651
--- /dev/null
+++ b/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/descriptors/props/LayoutPropExtractor.kt
@@ -0,0 +1,432 @@
+/*
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+package com.facebook.flipper.plugins.uidebugger.litho.descriptors.props
+
+import android.graphics.drawable.ColorDrawable
+import android.graphics.drawable.Drawable
+import com.facebook.flipper.plugins.uidebugger.common.enumToInspectableSet
+import com.facebook.flipper.plugins.uidebugger.descriptors.MetadataRegister
+import com.facebook.flipper.plugins.uidebugger.model.*
+import com.facebook.litho.DebugComponent
+import com.facebook.yoga.*
+
+object LayoutPropExtractor {
+ private const val NAMESPACE = "LayoutPropExtractor"
+
+ private var BackgroundId =
+ MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "background")
+ private var ForegroundId =
+ MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "foreground")
+
+ private val DirectionId =
+ MetadataRegister.register(
+ MetadataRegister.TYPE_ATTRIBUTE,
+ NAMESPACE,
+ "direction",
+ false,
+ enumToInspectableSet())
+ private val FlexDirectionId =
+ MetadataRegister.register(
+ MetadataRegister.TYPE_ATTRIBUTE,
+ NAMESPACE,
+ "flexDirection",
+ false,
+ enumToInspectableSet())
+ private val JustifyContentId =
+ MetadataRegister.register(
+ MetadataRegister.TYPE_ATTRIBUTE,
+ NAMESPACE,
+ "justifyContent",
+ false,
+ enumToInspectableSet())
+ private val AlignItemsId =
+ MetadataRegister.register(
+ MetadataRegister.TYPE_ATTRIBUTE,
+ NAMESPACE,
+ "alignItems",
+ false,
+ enumToInspectableSet())
+ private val AlignSelfId =
+ MetadataRegister.register(
+ MetadataRegister.TYPE_ATTRIBUTE,
+ NAMESPACE,
+ "alignSelf",
+ false,
+ enumToInspectableSet())
+ private val AlignContentId =
+ MetadataRegister.register(
+ MetadataRegister.TYPE_ATTRIBUTE,
+ NAMESPACE,
+ "alignContent",
+ false,
+ enumToInspectableSet())
+ private val PositionTypeId =
+ MetadataRegister.register(
+ MetadataRegister.TYPE_ATTRIBUTE,
+ NAMESPACE,
+ "positionType",
+ false,
+ enumToInspectableSet())
+
+ private val FlexGrowId =
+ MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "flexGrow")
+ private val FlexShrinkId =
+ MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "flexShrink")
+ private val FlexBasisId =
+ MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "flexBasis")
+ private val WidthId =
+ MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "width")
+ private val HeightId =
+ MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "height")
+ private val MinWidthId =
+ MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "minWidth")
+ private val MinHeightId =
+ MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "minHeight")
+ private val MaxWidthId =
+ MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "maxWidth")
+ private val MaxHeightId =
+ MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "maxHeight")
+ private val AspectRatioId =
+ MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "aspectRatio")
+
+ private val MarginId =
+ MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "margin")
+ private val PaddingId =
+ MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "padding")
+ private val BorderId =
+ MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "border")
+ private val PositionId =
+ MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "position")
+
+ private val LeftId = MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "left")
+ private val TopId = MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "top")
+ private val RightId =
+ MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "right")
+ private val BottomId =
+ MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "bottom")
+ private val StartId =
+ MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "start")
+ private val EndId = MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "end")
+ private val HorizontalId =
+ MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "horizontal")
+ private val VerticalId =
+ MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "vertical")
+ private val AllId = MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "all")
+
+ private val HasViewOutputId =
+ MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "hasViewOutput")
+ private val AlphaId =
+ MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "alpha")
+ private val ScaleId =
+ MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "scale")
+ private val RotationId =
+ MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "rotation")
+
+ private val EmptyId = MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "")
+ private val NoneId = MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "none")
+ private val SizeId = MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "size")
+ private val ViewOutputId =
+ MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "viewOutput")
+
+ fun getInspectableBox(
+ left: YogaValue?,
+ top: YogaValue?,
+ right: YogaValue?,
+ bottom: YogaValue?,
+ horizontal: YogaValue?,
+ vertical: YogaValue?,
+ all: YogaValue?,
+ start: YogaValue?,
+ end: YogaValue?
+ ): InspectableObject {
+ val props = mutableMapOf()
+
+ var actualLeft = 0
+ var actualTop = 0
+ var actualRight = 0
+ var actualBottom = 0
+
+ all?.let { yogaValue ->
+ if (yogaValue.unit != YogaUnit.UNDEFINED) {
+ if (yogaValue.unit == YogaUnit.POINT || yogaValue.unit == YogaUnit.PERCENT) {
+ val intValue = yogaValue.value.toInt()
+ actualLeft = intValue
+ actualTop = intValue
+ actualRight = intValue
+ actualBottom = intValue
+ }
+
+ props[AllId] = InspectableValue.Text(yogaValue.toString())
+ }
+ }
+
+ horizontal?.let { yogaValue ->
+ if (yogaValue.unit != YogaUnit.UNDEFINED) {
+ if (yogaValue.unit == YogaUnit.POINT || yogaValue.unit == YogaUnit.PERCENT) {
+ val intValue = yogaValue.value.toInt()
+ actualLeft = intValue
+ actualRight = intValue
+ }
+
+ props[HorizontalId] = InspectableValue.Text(yogaValue.toString())
+ }
+ }
+
+ vertical?.let { yogaValue ->
+ if (yogaValue.unit != YogaUnit.UNDEFINED) {
+ if (yogaValue.unit == YogaUnit.POINT || yogaValue.unit == YogaUnit.PERCENT) {
+ val intValue = yogaValue.value.toInt()
+ actualTop = intValue
+ actualBottom = intValue
+ }
+
+ props[VerticalId] = InspectableValue.Text(yogaValue.toString())
+ }
+ }
+
+ left?.let { yogaValue ->
+ if (yogaValue.unit != YogaUnit.UNDEFINED) {
+ if (yogaValue.unit == YogaUnit.POINT || yogaValue.unit == YogaUnit.PERCENT) {
+ val intValue = yogaValue.value.toInt()
+ actualLeft = intValue
+ }
+
+ props[LeftId] = InspectableValue.Text(yogaValue.toString())
+ }
+ }
+
+ right?.let { yogaValue ->
+ if (yogaValue.unit != YogaUnit.UNDEFINED) {
+ if (yogaValue.unit == YogaUnit.POINT || yogaValue.unit == YogaUnit.PERCENT) {
+ val intValue = yogaValue.value.toInt()
+ actualRight = intValue
+ }
+
+ props[RightId] = InspectableValue.Text(yogaValue.toString())
+ }
+ }
+
+ top?.let { yogaValue ->
+ if (yogaValue.unit != YogaUnit.UNDEFINED) {
+ if (yogaValue.unit == YogaUnit.POINT || yogaValue.unit == YogaUnit.PERCENT) {
+ val intValue = yogaValue.value.toInt()
+ actualTop = intValue
+ }
+
+ props[TopId] = InspectableValue.Text(yogaValue.toString())
+ }
+ }
+
+ bottom?.let { yogaValue ->
+ if (yogaValue.unit != YogaUnit.UNDEFINED) {
+ if (yogaValue.unit == YogaUnit.POINT || yogaValue.unit == YogaUnit.PERCENT) {
+ val intValue = yogaValue.value.toInt()
+ actualBottom = intValue
+ }
+
+ props[BottomId] = InspectableValue.Text(yogaValue.toString())
+ }
+ }
+
+ props[EmptyId] =
+ InspectableValue.SpaceBox(SpaceBox(actualTop, actualRight, actualBottom, actualLeft))
+
+ return InspectableObject(props)
+ }
+
+ fun getInspectableBoxRaw(
+ left: Float?,
+ top: Float?,
+ right: Float?,
+ bottom: Float?,
+ horizontal: Float?,
+ vertical: Float?,
+ all: Float?,
+ start: Float?,
+ end: Float?
+ ): InspectableObject {
+ val props = mutableMapOf()
+
+ var actualLeft = 0
+ var actualTop = 0
+ var actualRight = 0
+ var actualBottom = 0
+
+ all?.let { value ->
+ if (!value.isNaN()) {
+ val intValue = value.toInt()
+ actualLeft = intValue
+ actualTop = intValue
+ actualRight = intValue
+ actualBottom = intValue
+ props[AllId] = InspectableValue.Number(value)
+ }
+ }
+
+ horizontal?.let { value ->
+ if (!value.isNaN()) {
+ val intValue = value.toInt()
+ actualLeft = intValue
+ actualRight = intValue
+ props[HorizontalId] = InspectableValue.Number(value)
+ }
+ }
+
+ vertical?.let { value ->
+ if (!value.isNaN()) {
+ val intValue = value.toInt()
+ actualTop = intValue
+ actualBottom = intValue
+ props[VerticalId] = InspectableValue.Number(value)
+ }
+ }
+
+ left?.let { value ->
+ if (!value.isNaN()) {
+ val intValue = value.toInt()
+ actualLeft = intValue
+ props[LeftId] = InspectableValue.Number(value)
+ }
+ }
+
+ right?.let { value ->
+ if (!value.isNaN()) {
+ val intValue = value.toInt()
+ actualRight = intValue
+ props[RightId] = InspectableValue.Number(value)
+ }
+ }
+
+ top?.let { value ->
+ if (!value.isNaN()) {
+ val intValue = value.toInt()
+ actualTop = intValue
+ props[TopId] = InspectableValue.Number(value)
+ }
+ }
+
+ bottom?.let { value ->
+ if (!value.isNaN()) {
+ val intValue = value.toInt()
+ actualBottom = intValue
+ props[BottomId] = InspectableValue.Number(value)
+ }
+ }
+
+ props[EmptyId] =
+ InspectableValue.SpaceBox(SpaceBox(actualTop, actualRight, actualBottom, actualLeft))
+
+ return InspectableObject(props)
+ }
+
+ fun getProps(component: DebugComponent): Map {
+ val props = mutableMapOf()
+
+ val layout = component.layoutNode ?: return props
+
+ props[AlignItemsId] = InspectableValue.Enum(layout.alignItems.name)
+ props[AlignSelfId] = InspectableValue.Enum(layout.alignSelf.name)
+ props[AlignContentId] = InspectableValue.Enum(layout.alignContent.name)
+
+ props[AspectRatioId] = InspectableValue.Text(layout.aspectRatio.toString())
+
+ layout.background?.let { drawable -> props[BackgroundId] = fromDrawable(drawable) }
+
+ props[DirectionId] = InspectableValue.Enum(layout.layoutDirection.name)
+
+ props[FlexBasisId] = InspectableValue.Text(layout.flexBasis.toString())
+ props[FlexDirectionId] = InspectableValue.Enum(layout.flexDirection.name)
+ props[FlexGrowId] = InspectableValue.Text(layout.flexGrow.toString())
+ props[FlexShrinkId] = InspectableValue.Text(layout.flexShrink.toString())
+
+ layout.foreground?.let { drawable -> props[ForegroundId] = fromDrawable(drawable) }
+
+ props[JustifyContentId] = InspectableValue.Enum(layout.justifyContent.name)
+
+ props[PositionTypeId] = InspectableValue.Enum(layout.positionType.name)
+
+ val size: MutableMap = mutableMapOf()
+ size[WidthId] = InspectableValue.Text(layout.width.toString())
+ if (layout.minWidth.unit != YogaUnit.UNDEFINED)
+ size[MinWidthId] = InspectableValue.Text(layout.minWidth.toString())
+ if (layout.maxWidth.unit != YogaUnit.UNDEFINED)
+ size[MaxWidthId] = InspectableValue.Text(layout.maxWidth.toString())
+ size[HeightId] = InspectableValue.Text(layout.height.toString())
+ if (layout.minHeight.unit != YogaUnit.UNDEFINED)
+ size[MinHeightId] = InspectableValue.Text(layout.minHeight.toString())
+ if (layout.maxHeight.unit != YogaUnit.UNDEFINED)
+ size[MaxHeightId] = InspectableValue.Text(layout.maxHeight.toString())
+
+ props[SizeId] = InspectableObject(size)
+
+ props[MarginId] =
+ getInspectableBox(
+ layout.getMargin(YogaEdge.LEFT),
+ layout.getMargin(YogaEdge.TOP),
+ layout.getMargin(YogaEdge.RIGHT),
+ layout.getMargin(YogaEdge.BOTTOM),
+ layout.getMargin(YogaEdge.HORIZONTAL),
+ layout.getMargin(YogaEdge.VERTICAL),
+ layout.getMargin(YogaEdge.ALL),
+ layout.getMargin(YogaEdge.START),
+ layout.getMargin(YogaEdge.END))
+
+ props[PaddingId] =
+ getInspectableBox(
+ layout.getPadding(YogaEdge.LEFT),
+ layout.getPadding(YogaEdge.TOP),
+ layout.getPadding(YogaEdge.RIGHT),
+ layout.getPadding(YogaEdge.BOTTOM),
+ layout.getPadding(YogaEdge.HORIZONTAL),
+ layout.getPadding(YogaEdge.VERTICAL),
+ layout.getPadding(YogaEdge.ALL),
+ layout.getPadding(YogaEdge.START),
+ layout.getPadding(YogaEdge.END))
+
+ props[BorderId] =
+ getInspectableBoxRaw(
+ layout.getBorderWidth(YogaEdge.LEFT),
+ layout.getBorderWidth(YogaEdge.TOP),
+ layout.getBorderWidth(YogaEdge.RIGHT),
+ layout.getBorderWidth(YogaEdge.BOTTOM),
+ layout.getBorderWidth(YogaEdge.HORIZONTAL),
+ layout.getBorderWidth(YogaEdge.VERTICAL),
+ layout.getBorderWidth(YogaEdge.ALL),
+ layout.getBorderWidth(YogaEdge.START),
+ layout.getBorderWidth(YogaEdge.END))
+
+ props[PositionId] =
+ getInspectableBox(
+ layout.getPosition(YogaEdge.LEFT),
+ layout.getPosition(YogaEdge.TOP),
+ layout.getPosition(YogaEdge.RIGHT),
+ layout.getPosition(YogaEdge.BOTTOM),
+ layout.getPosition(YogaEdge.HORIZONTAL),
+ layout.getPosition(YogaEdge.VERTICAL),
+ layout.getPosition(YogaEdge.ALL),
+ layout.getPosition(YogaEdge.START),
+ layout.getPosition(YogaEdge.END))
+
+ val viewOutput: MutableMap = mutableMapOf()
+ viewOutput[HasViewOutputId] = InspectableValue.Boolean(layout.hasViewOutput())
+ if (layout.hasViewOutput()) {
+ viewOutput[AlphaId] = InspectableValue.Number(layout.alpha)
+ viewOutput[RotationId] = InspectableValue.Number(layout.rotation)
+ viewOutput[ScaleId] = InspectableValue.Number(layout.scale)
+ }
+ props[ViewOutputId] = InspectableObject(viewOutput)
+
+ return props
+ }
+
+ private fun fromDrawable(d: Drawable?): Inspectable =
+ when (d) {
+ is ColorDrawable -> InspectableValue.Color(Color.fromColor(d.color))
+ else -> InspectableValue.Unknown(d.toString())
+ }
+}
diff --git a/android/plugins/litho/src/test/java/com/facebook/flipper/plugins/uidebugger/litho/KStateContainerExtractionTest.kt b/android/plugins/litho/src/test/java/com/facebook/flipper/plugins/uidebugger/litho/KStateContainerExtractionTest.kt
new file mode 100644
index 000000000..b87333700
--- /dev/null
+++ b/android/plugins/litho/src/test/java/com/facebook/flipper/plugins/uidebugger/litho/KStateContainerExtractionTest.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import com.facebook.flipper.plugins.uidebugger.litho.descriptors.props.ComponentDataExtractor
+import com.facebook.flipper.plugins.uidebugger.model.InspectableArray
+import com.facebook.flipper.plugins.uidebugger.model.InspectableObject
+import com.facebook.flipper.plugins.uidebugger.model.InspectableValue
+import com.facebook.litho.KStateContainer
+import junit.framework.Assert.assertEquals
+import org.junit.Test
+
+class KStateContainerExtractionTest {
+ @Test
+ @Throws(Exception::class)
+ fun testCanExtractKState() {
+
+ // this test ensures that our reflection based extraction doesn't break if the KState class
+ // structure changes
+ val stateContainer = KStateContainer.withNewState(null, "foo")
+
+ val result = ComponentDataExtractor.getState(stateContainer, "Comp1")
+
+ assertEquals(
+ result,
+ InspectableObject(mapOf(1 to InspectableArray(listOf(InspectableValue.Text("foo"))))))
+ }
+}
diff --git a/android/plugins/network/build.gradle b/android/plugins/network/build.gradle
index c4a3cd27a..f2e02bdd7 100644
--- a/android/plugins/network/build.gradle
+++ b/android/plugins/network/build.gradle
@@ -8,6 +8,7 @@
apply plugin: 'com.android.library'
android {
+ namespace 'com.facebook.flipper.plugins.network'
compileSdkVersion rootProject.compileSdkVersion
buildToolsVersion rootProject.buildToolsVersion
diff --git a/android/plugins/retrofit2-protobuf/build.gradle b/android/plugins/retrofit2-protobuf/build.gradle
index e0efdc12c..4a4c383e2 100644
--- a/android/plugins/retrofit2-protobuf/build.gradle
+++ b/android/plugins/retrofit2-protobuf/build.gradle
@@ -7,10 +7,10 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
-apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
android {
+ namespace 'com.facebook.flipper.plugins.retrofit2protobuf'
compileSdkVersion rootProject.compileSdkVersion
buildToolsVersion rootProject.buildToolsVersion
@@ -19,8 +19,13 @@ android {
targetSdkVersion rootProject.targetSdkVersion
}
+ compileOptions {
+ targetCompatibility rootProject.javaTargetVersion
+ sourceCompatibility rootProject.javaTargetVersion
+ }
+
dependencies {
- implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$KOTLIN_VERSION"
+ implementation "org.jetbrains.kotlin:kotlin-stdlib:$KOTLIN_VERSION"
implementation project(':android')
implementation project(':network-plugin')
implementation deps.protobuf
diff --git a/android/sample/AndroidManifest.xml b/android/sample/AndroidManifest.xml
index c7e79a8dd..54f0a453d 100644
--- a/android/sample/AndroidManifest.xml
+++ b/android/sample/AndroidManifest.xml
@@ -11,6 +11,8 @@
xmlns:tools="http://schemas.android.com/tools"
package="com.facebook.flipper.sample">
+
-
@@ -87,6 +89,15 @@
+
+
+
+
+
+
+
+
{
+ ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 300);
+ valueAnimator.addUpdateListener(
+ animator -> txtValueAnimator.setTranslationX((Float) animator.getAnimatedValue()));
+ valueAnimator.setInterpolator(new LinearInterpolator());
+ valueAnimator.setDuration(10000);
+ valueAnimator.start();
+ });
- animBlink = AnimationUtils.loadAnimation(getApplicationContext(), R.anim.blink);
- // blink
btnBlink.setOnClickListener(
- new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- txtBlink.setVisibility(View.VISIBLE);
- txtBlink.startAnimation(animBlink);
- }
+ v -> {
+ txtBlink.setVisibility(View.VISIBLE);
+ txtBlink.startAnimation(animBlink);
});
animRotate = AnimationUtils.loadAnimation(getApplicationContext(), R.anim.rotate);
- // Rotate
- btnRotate.setOnClickListener(
- new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- txtRotate.startAnimation(animRotate);
- }
- });
+ btnRotate.setOnClickListener(v -> txtRotate.startAnimation(animRotate));
animMove = AnimationUtils.loadAnimation(getApplicationContext(), R.anim.move);
- // Move
- btnMove.setOnClickListener(
- new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- txtMove.startAnimation(animMove);
- }
- });
+
+ btnMove.setOnClickListener(v -> txtMove.startAnimation(animMove));
animBounce = AnimationUtils.loadAnimation(getApplicationContext(), R.anim.bounce);
- // Slide Down
- btnBounce.setOnClickListener(
- new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- txtBounce.startAnimation(animBounce);
- }
- });
- animSequential = AnimationUtils.loadAnimation(getApplicationContext(), R.anim.sequential);
- // Sequential
- btnSequential.setOnClickListener(
- new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- txtSeq.startAnimation(animSequential);
- }
- });
+ btnBounce.setOnClickListener(v -> txtBounce.startAnimation(animBounce));
+ animSequential = AnimationUtils.loadAnimation(getApplicationContext(), R.anim.sequential);
+
+ btnSequential.setOnClickListener(v -> txtSeq.startAnimation(animSequential));
}
}
diff --git a/android/sample/src/main/java/com/facebook/flipper/sample/ButtonsActivity.java b/android/sample/src/main/java/com/facebook/flipper/sample/ButtonsActivity.java
new file mode 100644
index 000000000..74a6fcb27
--- /dev/null
+++ b/android/sample/src/main/java/com/facebook/flipper/sample/ButtonsActivity.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+package com.facebook.flipper.sample;
+
+import android.os.Bundle;
+import android.widget.Button;
+import android.widget.TextView;
+import androidx.appcompat.app.AlertDialog;
+import androidx.fragment.app.FragmentActivity;
+
+public class ButtonsActivity extends FragmentActivity {
+
+ int count = 0;
+ TextView text;
+ Button button;
+ Button dialogOld;
+ Button dialogFragment;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_buttons);
+
+ text = findViewById(R.id.count);
+
+ button = findViewById(com.facebook.flipper.sample.R.id.btn_inc);
+ dialogOld = findViewById(R.id.dialog_old_api);
+ dialogFragment = findViewById(R.id.dialog_fragment);
+ button.setOnClickListener(view -> ButtonsActivity.this.text.setText(String.valueOf(++count)));
+
+ dialogFragment.setOnClickListener(
+ btn -> {
+ TestDialogFragment startGameDialogFragment = new TestDialogFragment();
+ startGameDialogFragment.show(getSupportFragmentManager(), "dialog");
+ });
+
+ dialogOld.setOnClickListener(
+ btn -> {
+ new AlertDialog.Builder(this)
+ .setTitle("Old Dialog")
+ .setMessage("This is an old dialog")
+ .setNegativeButton(android.R.string.no, null)
+ .setIcon(android.R.drawable.ic_dialog_alert)
+ .show();
+ });
+ }
+}
diff --git a/android/sample/src/main/java/com/facebook/flipper/sample/FlipperSampleApplication.java b/android/sample/src/main/java/com/facebook/flipper/sample/FlipperSampleApplication.java
index 63ded5e97..0a1076f07 100644
--- a/android/sample/src/main/java/com/facebook/flipper/sample/FlipperSampleApplication.java
+++ b/android/sample/src/main/java/com/facebook/flipper/sample/FlipperSampleApplication.java
@@ -14,6 +14,7 @@ import com.facebook.drawee.backends.pipeline.Fresco;
import com.facebook.flipper.android.AndroidFlipperClient;
import com.facebook.flipper.core.FlipperClient;
import com.facebook.flipper.sample.network.NetworkClient;
+import com.facebook.fresco.vito.init.FrescoVito;
import com.facebook.soloader.SoLoader;
public class FlipperSampleApplication extends Application {
@@ -22,9 +23,12 @@ public class FlipperSampleApplication extends Application {
super.onCreate();
SoLoader.init(this, false);
Fresco.initialize(this);
+ FrescoVito.initialize();
final FlipperClient client = AndroidFlipperClient.getInstance(this);
- final FlipperInitializer.IntializationResult initializationResult =
+ assert client != null;
+
+ final FlipperInitializer.InitializationResult initializationResult =
FlipperInitializer.initFlipperPlugins(this, client);
NetworkClient.getInstance().setOkHttpClient(initializationResult.getOkHttpClient());
diff --git a/android/sample/src/main/java/com/facebook/flipper/sample/FragmentTestFragment.java b/android/sample/src/main/java/com/facebook/flipper/sample/FragmentTestFragment.java
index 4bd31be23..cfccc9d52 100644
--- a/android/sample/src/main/java/com/facebook/flipper/sample/FragmentTestFragment.java
+++ b/android/sample/src/main/java/com/facebook/flipper/sample/FragmentTestFragment.java
@@ -15,39 +15,39 @@ import android.widget.TextView;
import androidx.fragment.app.Fragment;
public class FragmentTestFragment extends Fragment {
- View mView;
- int mTicker;
+ View view;
+ int ticker;
public FragmentTestFragment() {
- mTicker = 0;
+ ticker = 0;
}
private void updateTicker() {
try {
- ViewGroup viewGroup = (ViewGroup) mView;
+ ViewGroup viewGroup = (ViewGroup) view;
TextView textView = (TextView) viewGroup.getChildAt(1);
- String text = String.valueOf(mTicker++);
+ String text = String.valueOf(ticker++);
textView.setText(text);
} finally {
// 100% guarantee that this always happens, even if
// your update method throws an exception
- mView.postDelayed(
+ view.postDelayed(
new Runnable() {
@Override
public void run() {
updateTicker();
}
},
- 10000);
+ 1000);
}
}
@Override
public View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
- mView = inflater.inflate(R.layout.fragment_test, container, false);
- mView.postDelayed(
+ view = inflater.inflate(R.layout.fragment_test, container, false);
+ view.postDelayed(
new Runnable() {
@Override
public void run() {
@@ -56,6 +56,6 @@ public class FragmentTestFragment extends Fragment {
},
1000);
- return mView;
+ return view;
}
}
diff --git a/android/sample/src/main/java/com/facebook/flipper/sample/IncrementActivity.java b/android/sample/src/main/java/com/facebook/flipper/sample/IncrementActivity.java
deleted file mode 100644
index 7e35ac7b3..000000000
--- a/android/sample/src/main/java/com/facebook/flipper/sample/IncrementActivity.java
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Copyright (c) Meta Platforms, Inc. and affiliates.
- *
- * This source code is licensed under the MIT license found in the
- * LICENSE file in the root directory of this source tree.
- */
-
-package com.facebook.flipper.sample;
-
-import android.app.Activity;
-import android.os.Bundle;
-import android.view.View;
-import android.widget.Button;
-import android.widget.TextView;
-
-public class IncrementActivity extends Activity {
-
- int count = 0;
- TextView text;
- Button button;
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_increment);
-
- text = (TextView) findViewById(R.id.count);
-
- button = (Button) findViewById(com.facebook.flipper.sample.R.id.btn_inc);
- button.setOnClickListener(
- new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- IncrementActivity.this.text.setText(String.valueOf(++count));
- }
- });
- }
-}
diff --git a/android/sample/src/main/java/com/facebook/flipper/sample/JetpackComposeActivity.kt b/android/sample/src/main/java/com/facebook/flipper/sample/JetpackComposeActivity.kt
new file mode 100644
index 000000000..8498774b0
--- /dev/null
+++ b/android/sample/src/main/java/com/facebook/flipper/sample/JetpackComposeActivity.kt
@@ -0,0 +1,30 @@
+package com.facebook.flipper.sample
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun MessageCard(name: String) {
+ Row(modifier = Modifier.padding(all = 8.dp)) { Text(text = "Hello $name!") }
+}
+
+@Preview
+@Composable
+fun PreviewMessageCard() {
+ MessageCard("Android")
+}
+
+class JetpackComposeActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContent { MessageCard("Flipper") }
+ }
+}
diff --git a/android/sample/src/main/java/com/facebook/flipper/sample/ListActivity.java b/android/sample/src/main/java/com/facebook/flipper/sample/ListActivity.java
index 0f65041c3..35bd952c6 100644
--- a/android/sample/src/main/java/com/facebook/flipper/sample/ListActivity.java
+++ b/android/sample/src/main/java/com/facebook/flipper/sample/ListActivity.java
@@ -24,7 +24,7 @@ public class ListActivity extends Activity {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_list);
- listView = (ListView) findViewById(R.id.list);
+ listView = findViewById(R.id.list);
list = new ArrayList<>();
list.add("Apple");
@@ -33,7 +33,7 @@ public class ListActivity extends Activity {
list.add("Orange");
list.add("Lychee");
list.add("Guava");
- list.add("Peech");
+ list.add("Peach");
list.add("Melon");
list.add("Watermelon");
list.add("Papaya");
diff --git a/android/sample/src/main/java/com/facebook/flipper/sample/MainActivity.java b/android/sample/src/main/java/com/facebook/flipper/sample/MainActivity.java
index bf558c524..c5c4c17f1 100644
--- a/android/sample/src/main/java/com/facebook/flipper/sample/MainActivity.java
+++ b/android/sample/src/main/java/com/facebook/flipper/sample/MainActivity.java
@@ -29,7 +29,9 @@ public class MainActivity extends AppCompatActivity {
final FlipperClient client = AndroidFlipperClient.getInstanceIfInitialized();
if (client != null) {
final ExampleFlipperPlugin samplePlugin = client.getPluginByClass(ExampleFlipperPlugin.class);
- samplePlugin.setActivity(this);
+ if (samplePlugin != null) {
+ samplePlugin.setActivity(this);
+ }
}
}
}
diff --git a/android/sample/src/main/java/com/facebook/flipper/sample/RootComponentSpec.java b/android/sample/src/main/java/com/facebook/flipper/sample/RootComponentSpec.java
index af0fb1ec2..09c7eb9c4 100644
--- a/android/sample/src/main/java/com/facebook/flipper/sample/RootComponentSpec.java
+++ b/android/sample/src/main/java/com/facebook/flipper/sample/RootComponentSpec.java
@@ -8,10 +8,10 @@
package com.facebook.flipper.sample;
import android.content.Intent;
-import com.facebook.drawee.backends.pipeline.Fresco;
-import com.facebook.drawee.interfaces.DraweeController;
+import android.net.Uri;
import com.facebook.flipper.android.diagnostics.FlipperDiagnosticActivity;
import com.facebook.flipper.sample.network.NetworkClient;
+import com.facebook.fresco.vito.litho.FrescoVitoImage2;
import com.facebook.litho.ClickEvent;
import com.facebook.litho.Column;
import com.facebook.litho.Component;
@@ -22,7 +22,6 @@ import com.facebook.litho.annotations.OnCreateLayout;
import com.facebook.litho.annotations.OnEvent;
import com.facebook.litho.annotations.OnUpdateState;
import com.facebook.litho.annotations.State;
-import com.facebook.litho.fresco.FrescoImage;
import com.facebook.litho.widget.Text;
import com.facebook.litho.widget.VerticalScroll;
import com.facebook.yoga.YogaEdge;
@@ -32,9 +31,6 @@ public class RootComponentSpec {
@OnCreateLayout
static Component onCreateLayout(final ComponentContext c, @State boolean displayImage) {
- final DraweeController controller =
- Fresco.newDraweeControllerBuilder().setUri("https://fbflipper.com/img/icon.png").build();
-
Column col =
Column.create(c)
.child(
@@ -109,24 +105,33 @@ public class RootComponentSpec {
.clickHandler(RootComponent.openAnimationsActivity(c)))
.child(
Text.create(c)
- .text("Navigate to increment activity")
+ .text("Navigate to buttons activity")
.key("11")
.marginDip(YogaEdge.ALL, 10)
.textSizeSp(20)
.clickHandler(RootComponent.openIncrementActivity(c)))
.child(
Text.create(c)
- .text("Crash this app")
+ .text("Navigate to Jetpack Compose activity")
.key("12")
.marginDip(YogaEdge.ALL, 10)
.textSizeSp(20)
+ .clickHandler(RootComponent.openJetpackComposeActivity(c)))
+ .child(
+ Text.create(c)
+ .text("Crash this app")
+ .key("13")
+ .marginDip(YogaEdge.ALL, 10)
+ .textSizeSp(20)
.clickHandler(RootComponent.triggerCrash(c)))
.child(
- FrescoImage.create(c)
- .controller(controller)
- .marginDip(YogaEdge.ALL, 10)
- .widthDip(150)
- .heightDip(150))
+ displayImage
+ ? FrescoVitoImage2.create(c)
+ .uri(Uri.parse("https://fbflipper.com/img/icon.png"))
+ .marginDip(YogaEdge.ALL, 10)
+ .widthDip(150)
+ .heightDip(150)
+ : null)
.build();
return VerticalScroll.create(c).childComponent(col).build();
@@ -200,7 +205,13 @@ public class RootComponentSpec {
@OnEvent(ClickEvent.class)
static void openIncrementActivity(final ComponentContext c) {
- final Intent intent = new Intent(c.getAndroidContext(), IncrementActivity.class);
+ final Intent intent = new Intent(c.getAndroidContext(), ButtonsActivity.class);
+ c.getAndroidContext().startActivity(intent);
+ }
+
+ @OnEvent(ClickEvent.class)
+ static void openJetpackComposeActivity(final ComponentContext c) {
+ final Intent intent = new Intent(c.getAndroidContext(), JetpackComposeActivity.class);
c.getAndroidContext().startActivity(intent);
}
}
diff --git a/android/sample/src/main/java/com/facebook/flipper/sample/TestDialogFragment.java b/android/sample/src/main/java/com/facebook/flipper/sample/TestDialogFragment.java
new file mode 100644
index 000000000..cf39d498b
--- /dev/null
+++ b/android/sample/src/main/java/com/facebook/flipper/sample/TestDialogFragment.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+package com.facebook.flipper.sample;
+
+import android.app.Dialog;
+import android.os.Bundle;
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AlertDialog;
+import androidx.fragment.app.DialogFragment;
+
+public class TestDialogFragment extends DialogFragment {
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ // Use the Builder class for convenient dialog construction
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ builder.setMessage("This is a dialog fragment").setPositiveButton("Yes", (dialog, id) -> {});
+
+ return builder.create();
+ }
+}
diff --git a/android/sample/src/main/res/layout/activity_animations.xml b/android/sample/src/main/res/layout/activity_animations.xml
index e53b67722..e9be343b6 100644
--- a/android/sample/src/main/res/layout/activity_animations.xml
+++ b/android/sample/src/main/res/layout/activity_animations.xml
@@ -12,6 +12,7 @@
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
+
+
+
+
+
+
+
+
+
diff --git a/android/sample/src/main/res/layout/activity_buttons.xml b/android/sample/src/main/res/layout/activity_buttons.xml
new file mode 100644
index 000000000..6f9c9c7bb
--- /dev/null
+++ b/android/sample/src/main/res/layout/activity_buttons.xml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/android/sample/src/main/res/layout/activity_increment.xml b/android/sample/src/main/res/layout/activity_increment.xml
deleted file mode 100644
index d35ff207f..000000000
--- a/android/sample/src/main/res/layout/activity_increment.xml
+++ /dev/null
@@ -1,26 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/android/sample/src/release/java/com/facebook/flipper/sample/FlipperInitializer.java b/android/sample/src/release/java/com/facebook/flipper/sample/FlipperInitializer.java
index 4559c82d2..048f8deb7 100644
--- a/android/sample/src/release/java/com/facebook/flipper/sample/FlipperInitializer.java
+++ b/android/sample/src/release/java/com/facebook/flipper/sample/FlipperInitializer.java
@@ -13,11 +13,11 @@ import java.util.concurrent.TimeUnit;
import okhttp3.OkHttpClient;
public final class FlipperInitializer {
- public interface IntializationResult {
+ public interface InitializationResult {
OkHttpClient getOkHttpClient();
}
- public static IntializationResult initFlipperPlugins(Context context, FlipperClient client) {
+ public static InitializationResult initFlipperPlugins(Context context, FlipperClient client) {
final OkHttpClient okHttpClient =
new OkHttpClient.Builder()
.connectTimeout(60, TimeUnit.SECONDS)
@@ -25,7 +25,7 @@ public final class FlipperInitializer {
.writeTimeout(10, TimeUnit.MINUTES)
.build();
- return new IntializationResult() {
+ return new InitializationResult() {
@Override
public OkHttpClient getOkHttpClient() {
return okHttpClient;
diff --git a/android/src/main/cpp/sonar.cpp b/android/src/main/cpp/sonar.cpp
index 25f4b9169..d9415668e 100644
--- a/android/src/main/cpp/sonar.cpp
+++ b/android/src/main/cpp/sonar.cpp
@@ -44,8 +44,15 @@ void handleException(const std::exception& e) {
__android_log_write(ANDROID_LOG_ERROR, "FLIPPER", message.c_str());
}
-std::unique_ptr sonarScheduler;
-std::unique_ptr connectionScheduler;
+std::unique_ptr& sonarScheduler() {
+ static std::unique_ptr scheduler;
+ return scheduler;
+}
+
+std::unique_ptr& connectionScheduler() {
+ static std::unique_ptr scheduler;
+ return scheduler;
+}
class JEventBase : public jni::HybridClass {
public:
@@ -269,9 +276,9 @@ class JFlipperWebSocket : public facebook::flipper::FlipperSocket {
messageHandler_ = std::move(messageHandler);
}
- virtual bool connect(FlipperConnectionManager* manager) override {
+ virtual void connect(FlipperConnectionManager* manager) override {
if (socket_ != nullptr) {
- return true;
+ return;
}
std::string connectionURL = endpoint_.secure ? "wss://" : "ws://";
@@ -290,38 +297,9 @@ class JFlipperWebSocket : public facebook::flipper::FlipperSocket {
auto secure = endpoint_.secure;
- std::promise promise;
- auto connected = promise.get_future();
-
- connecting_ = true;
-
socket_ = make_global(JFlipperSocketImpl::create(connectionURL));
socket_->setEventHandler(JFlipperSocketEventHandlerImpl::newObjectCxxArgs(
- [this, &promise, eventHandler = eventHandler_](SocketEvent event) {
- /**
- Only fulfill the promise the first time the event handler is used.
- If the open event is received, then set the promise value to true.
- For any other event, consider a failure and set to false.
- */
- if (this->connecting_) {
- this->connecting_ = false;
- if (event == SocketEvent::OPEN) {
- promise.set_value(true);
- } else if (event == SocketEvent::SSL_ERROR) {
- try {
- promise.set_exception(
- std::make_exception_ptr(folly::AsyncSocketException(
- folly::AsyncSocketException::SSL_ERROR,
- "SSL handshake failed")));
- } catch (...) {
- // set_exception() may throw an exception
- // In that case, just set the value to false.
- promise.set_value(false);
- }
- } else {
- promise.set_value(false);
- }
- }
+ [eventHandler = eventHandler_](SocketEvent event) {
eventHandler(event);
},
[messageHandler = messageHandler_](const std::string& message) {
@@ -341,14 +319,6 @@ class JFlipperWebSocket : public facebook::flipper::FlipperSocket {
return JFlipperObject::create(std::move(object_));
}));
socket_->connect();
-
- auto state = connected.wait_for(std::chrono::seconds(10));
- if (state == std::future_status::ready) {
- return connected.get();
- }
-
- disconnect();
- return false;
}
virtual void disconnect() override {
@@ -373,6 +343,13 @@ class JFlipperWebSocket : public facebook::flipper::FlipperSocket {
if (socket_ == nullptr) {
return;
}
+ // Ensure the payload size is valid before sending.
+ // The maximum allowed size for a message payload is 2^53 - 1. But that is
+ // for the entire message, including any additional metadata.
+ if (message.length() > pow(2, 53) - 1) {
+ throw std::length_error("Payload is too big to send");
+ }
+
socket_->send(message);
completion();
}
@@ -408,7 +385,6 @@ class JFlipperWebSocket : public facebook::flipper::FlipperSocket {
facebook::flipper::SocketMessageHandler messageHandler_;
jni::global_ref socket_;
- bool connecting_;
};
class JFlipperSocketProvider : public facebook::flipper::FlipperSocketProvider {
@@ -767,6 +743,7 @@ class JFlipperClient : public jni::HybridClass {
makeNativeMethod("getInstance", JFlipperClient::getInstance),
makeNativeMethod("start", JFlipperClient::start),
makeNativeMethod("stop", JFlipperClient::stop),
+ makeNativeMethod("isConnected", JFlipperClient::isConnected),
makeNativeMethod("addPluginNative", JFlipperClient::addPlugin),
makeNativeMethod("removePluginNative", JFlipperClient::removePlugin),
makeNativeMethod(
@@ -812,6 +789,19 @@ class JFlipperClient : public jni::HybridClass {
}
}
+ bool isConnected() {
+ try {
+ return FlipperClient::instance()->isConnected();
+ } catch (const std::exception& e) {
+ handleException(e);
+ } catch (const std::exception* e) {
+ if (e) {
+ handleException(*e);
+ }
+ }
+ return false;
+ }
+
void addPlugin(jni::alias_ref plugin) {
try {
auto wrapper =
@@ -949,9 +939,9 @@ class JFlipperClient : public jni::HybridClass {
const std::string app,
const std::string appId,
const std::string privateAppDirectory) {
- sonarScheduler =
+ sonarScheduler() =
std::make_unique(callbackWorker->eventBase());
- connectionScheduler =
+ connectionScheduler() =
std::make_unique(connectionWorker->eventBase());
FlipperClient::init(
@@ -962,8 +952,8 @@ class JFlipperClient : public jni::HybridClass {
std::move(app),
std::move(appId),
std::move(privateAppDirectory)},
- sonarScheduler.get(),
- connectionScheduler.get(),
+ sonarScheduler().get(),
+ connectionScheduler().get(),
insecurePort,
securePort,
altInsecurePort,
diff --git a/android/src/main/java/com/facebook/flipper/android/AndroidFlipperClient.java b/android/src/main/java/com/facebook/flipper/android/AndroidFlipperClient.java
index d0e1f6ff5..3ae551480 100644
--- a/android/src/main/java/com/facebook/flipper/android/AndroidFlipperClient.java
+++ b/android/src/main/java/com/facebook/flipper/android/AndroidFlipperClient.java
@@ -26,7 +26,8 @@ public final class AndroidFlipperClient {
private static final String[] REQUIRED_PERMISSIONS =
new String[] {"android.permission.INTERNET", "android.permission.ACCESS_WIFI_STATE"};
- public static synchronized FlipperClient getInstance(Context context) {
+ public static synchronized FlipperClient getInstance(
+ Context context, String id, String deviceName, String processName, String packageName) {
if (!sIsInitialized) {
if (!(BuildConfig.IS_INTERNAL_BUILD || BuildConfig.LOAD_FLIPPER_EXPLICIT)) {
Log.e("Flipper", "Attempted to initialize in non-internal build");
@@ -58,16 +59,23 @@ public final class AndroidFlipperClient {
FlipperProps.getAltSecurePort(),
getServerHost(app),
"Android",
- getFriendlyDeviceName(),
- getId(),
- getRunningAppName(app),
- getPackageName(app),
+ deviceName,
+ id,
+ processName,
+ packageName,
privateAppDirectory);
sIsInitialized = true;
}
return FlipperClientImpl.getInstance();
}
+ public static synchronized FlipperClient getInstance(Context context) {
+ final Context app =
+ context.getApplicationContext() == null ? context : context.getApplicationContext();
+ return getInstance(
+ context, getId(), getFriendlyDeviceName(), getRunningAppName(app), getPackageName(app));
+ }
+
@Nullable
public static synchronized FlipperClient getInstanceIfInitialized() {
if (!sIsInitialized) {
diff --git a/android/src/main/java/com/facebook/flipper/android/FlipperClientImpl.java b/android/src/main/java/com/facebook/flipper/android/FlipperClientImpl.java
index 752e1a5ab..9fd5506f2 100644
--- a/android/src/main/java/com/facebook/flipper/android/FlipperClientImpl.java
+++ b/android/src/main/java/com/facebook/flipper/android/FlipperClientImpl.java
@@ -89,6 +89,9 @@ class FlipperClientImpl implements FlipperClient {
@Override
public native void stop();
+ @Override
+ public native boolean isConnected();
+
@Override
public native void subscribeForUpdates(FlipperStateUpdateListener stateListener);
diff --git a/android/src/main/java/com/facebook/flipper/android/FlipperSocketImpl.java b/android/src/main/java/com/facebook/flipper/android/FlipperSocketImpl.java
index 2ff095d73..7479889e5 100644
--- a/android/src/main/java/com/facebook/flipper/android/FlipperSocketImpl.java
+++ b/android/src/main/java/com/facebook/flipper/android/FlipperSocketImpl.java
@@ -37,10 +37,10 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.concurrent.TimeUnit;
+import javax.net.SocketFactory;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLParameters;
-import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import org.java_websocket.client.WebSocketClient;
@@ -67,6 +67,22 @@ class FlipperSocketImpl extends WebSocketClient implements FlipperSocket {
this.mEventHandler = eventHandler;
}
+ private void clearEventHandler() {
+ this.mEventHandler =
+ new FlipperSocketEventHandler() {
+ @Override
+ public void onConnectionEvent(SocketEvent event) {}
+
+ @Override
+ public void onMessageReceived(String message) {}
+
+ @Override
+ public FlipperObject onAuthenticationChallengeReceived() {
+ return null;
+ }
+ };
+ }
+
@Override
public void flipperConnect() {
if ((this.isOpen())) {
@@ -79,6 +95,7 @@ class FlipperSocketImpl extends WebSocketClient implements FlipperSocket {
* certificate exchange.
*/
FlipperObject authenticationObject = this.mEventHandler.onAuthenticationChallengeReceived();
+ SocketFactory socketFactory;
if (authenticationObject.contains("certificates_client_path")
&& authenticationObject.contains("certificates_client_pass")) {
@@ -100,21 +117,23 @@ class FlipperSocketImpl extends WebSocketClient implements FlipperSocket {
sslContext.init(
kmf.getKeyManagers(), new TrustManager[] {new FlipperTrustManager(cert_ca_path)}, null);
- SSLSocketFactory factory = sslContext.getSocketFactory();
-
- this.setSocketFactory(
- new DelegatingSocketFactory(factory) {
- @Override
- protected Socket configureSocket(Socket socket) {
- TrafficStats.setThreadStatsTag(SOCKET_TAG);
- return socket;
- }
- });
+ socketFactory = sslContext.getSocketFactory();
+ } else {
+ socketFactory = SocketFactory.getDefault();
}
+ this.setSocketFactory(
+ new DelegatingSocketFactory(socketFactory) {
+ @Override
+ protected Socket configureSocket(Socket socket) {
+ TrafficStats.setThreadStatsTag(SOCKET_TAG);
+ return socket;
+ }
+ });
+
this.connect();
} catch (Exception e) {
- Log.e("Flipper", "Failed to initialize the socket before connect. " + e.getMessage());
+ Log.e("flipper", "Failed to initialize the socket before connect. Error: " + e.getMessage());
this.mEventHandler.onConnectionEvent(FlipperSocketEventHandler.SocketEvent.ERROR);
}
}
@@ -137,6 +156,9 @@ class FlipperSocketImpl extends WebSocketClient implements FlipperSocket {
@Override
public void onClose(int code, String reason, boolean remote) {
this.mEventHandler.onConnectionEvent(FlipperSocketEventHandler.SocketEvent.CLOSE);
+ // Clear the existing event handler as to ensure no other events are processed after the close
+ // is handled.
+ this.clearEventHandler();
}
/**
@@ -148,6 +170,7 @@ class FlipperSocketImpl extends WebSocketClient implements FlipperSocket {
*/
@Override
public void onError(Exception ex) {
+
// Check the exception for OpenSSL error and change the event type.
// Required for Flipper as the current implementation treats these errors differently.
if (ex instanceof javax.net.ssl.SSLHandshakeException) {
@@ -155,24 +178,14 @@ class FlipperSocketImpl extends WebSocketClient implements FlipperSocket {
} else {
this.mEventHandler.onConnectionEvent(FlipperSocketEventHandler.SocketEvent.ERROR);
}
+ // Clear the existing event handler as to ensure no other events are processed after the close
+ // is handled.
+ this.clearEventHandler();
}
@Override
public void flipperDisconnect() {
- this.mEventHandler.onConnectionEvent(FlipperSocketEventHandler.SocketEvent.CLOSE);
- this.mEventHandler =
- new FlipperSocketEventHandler() {
- @Override
- public void onConnectionEvent(SocketEvent event) {}
-
- @Override
- public void onMessageReceived(String message) {}
-
- @Override
- public FlipperObject onAuthenticationChallengeReceived() {
- return null;
- }
- };
+ this.clearEventHandler();
super.close();
}
diff --git a/android/src/main/java/com/facebook/flipper/android/utils/FlipperUtils.java b/android/src/main/java/com/facebook/flipper/android/utils/FlipperUtils.java
index 316e83595..dca1fdab7 100644
--- a/android/src/main/java/com/facebook/flipper/android/utils/FlipperUtils.java
+++ b/android/src/main/java/com/facebook/flipper/android/utils/FlipperUtils.java
@@ -16,14 +16,19 @@ public final class FlipperUtils {
private FlipperUtils() {}
- public static boolean shouldEnableFlipper(final Context context) {
+ public static boolean shouldEnableFlipper(
+ final Context context, final boolean allowDebuggingServices) {
return (BuildConfig.IS_INTERNAL_BUILD || BuildConfig.LOAD_FLIPPER_EXPLICIT)
&& !isEndToEndTest()
- && isMainProcess(context)
+ && (allowDebuggingServices || isMainProcess(context))
// Flipper has issue with ASAN build. They cannot be concurrently enabled.
&& !BuildConfig.IS_ASAN_BUILD;
}
+ public static boolean shouldEnableFlipper(final Context context) {
+ return shouldEnableFlipper(context, false);
+ }
+
private static boolean isEndToEndTest() {
final String value = System.getenv("BUDDY_SONAR_DISABLED");
if (value == null || value.length() == 0) {
diff --git a/android/src/main/java/com/facebook/flipper/core/FlipperClient.java b/android/src/main/java/com/facebook/flipper/core/FlipperClient.java
index a6560f64a..7dec198a6 100644
--- a/android/src/main/java/com/facebook/flipper/core/FlipperClient.java
+++ b/android/src/main/java/com/facebook/flipper/core/FlipperClient.java
@@ -24,6 +24,8 @@ public interface FlipperClient {
void stop();
+ boolean isConnected();
+
void subscribeForUpdates(FlipperStateUpdateListener stateListener);
void unsubscribe();
diff --git a/android/src/main/java/com/facebook/flipper/plugins/databases/impl/SqliteDatabaseDriver.java b/android/src/main/java/com/facebook/flipper/plugins/databases/impl/SqliteDatabaseDriver.java
index 86d141a8b..1e6eedcda 100644
--- a/android/src/main/java/com/facebook/flipper/plugins/databases/impl/SqliteDatabaseDriver.java
+++ b/android/src/main/java/com/facebook/flipper/plugins/databases/impl/SqliteDatabaseDriver.java
@@ -162,9 +162,9 @@ public class SqliteDatabaseDriver extends DatabaseDriver indexedColumnNames = new ArrayList<>();
String indexName = indexesCursor.getString(indexesCursor.getColumnIndex("name"));
- Cursor indexInfoCursor = database.query("PRAGMA index_info(" + indexName + ")", null);
+ Cursor indexInfoCursor = database.query("PRAGMA index_info(" + indexName + ")");
try {
while (indexInfoCursor.moveToNext()) {
indexedColumnNames.add(
@@ -307,7 +307,7 @@ public class SqliteDatabaseDriver extends DatabaseDriver> rows = cursorToList(cursor);
diff --git a/android/src/main/java/com/facebook/flipper/plugins/inspector/descriptors/utils/AccessibilityRoleUtil.java b/android/src/main/java/com/facebook/flipper/plugins/inspector/descriptors/utils/AccessibilityRoleUtil.java
index cfd1273e8..4ad9ce960 100644
--- a/android/src/main/java/com/facebook/flipper/plugins/inspector/descriptors/utils/AccessibilityRoleUtil.java
+++ b/android/src/main/java/com/facebook/flipper/plugins/inspector/descriptors/utils/AccessibilityRoleUtil.java
@@ -11,6 +11,7 @@ import android.view.View;
import android.view.accessibility.AccessibilityNodeInfo;
import androidx.core.view.ViewCompat;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat;
import javax.annotation.Nullable;
/**
@@ -119,6 +120,17 @@ public class AccessibilityRoleUtil {
}
if (role.equals(AccessibilityRole.NONE)) {
+ if (AccessibilityUtil.supportsAction(
+ nodeInfo, AccessibilityActionCompat.ACTION_PAGE_UP.getId())
+ || AccessibilityUtil.supportsAction(
+ nodeInfo, AccessibilityActionCompat.ACTION_PAGE_DOWN.getId())
+ || AccessibilityUtil.supportsAction(
+ nodeInfo, AccessibilityActionCompat.ACTION_PAGE_LEFT.getId())
+ || AccessibilityUtil.supportsAction(
+ nodeInfo, AccessibilityActionCompat.ACTION_PAGE_RIGHT.getId())) {
+ return AccessibilityRole.PAGER;
+ }
+
AccessibilityNodeInfoCompat.CollectionInfoCompat collection = nodeInfo.getCollectionInfo();
if (collection != null) {
// RecyclerView will be classified as a list or grid.
diff --git a/android/src/main/java/com/facebook/flipper/plugins/inspector/descriptors/utils/AccessibilityUtil.java b/android/src/main/java/com/facebook/flipper/plugins/inspector/descriptors/utils/AccessibilityUtil.java
index 75bdf6723..ddbb88fb6 100644
--- a/android/src/main/java/com/facebook/flipper/plugins/inspector/descriptors/utils/AccessibilityUtil.java
+++ b/android/src/main/java/com/facebook/flipper/plugins/inspector/descriptors/utils/AccessibilityUtil.java
@@ -211,7 +211,7 @@ public final class AccessibilityUtil {
}
}
- private static boolean supportsAction(AccessibilityNodeInfoCompat node, int action) {
+ protected static boolean supportsAction(AccessibilityNodeInfoCompat node, int action) {
if (node != null) {
final int supportedActions = node.getActions();
diff --git a/android/src/main/java/com/facebook/flipper/plugins/react/ReactFlipperPlugin.java b/android/src/main/java/com/facebook/flipper/plugins/react/ReactFlipperPlugin.java
index ac75701f6..ec2fd35a5 100644
--- a/android/src/main/java/com/facebook/flipper/plugins/react/ReactFlipperPlugin.java
+++ b/android/src/main/java/com/facebook/flipper/plugins/react/ReactFlipperPlugin.java
@@ -10,7 +10,7 @@ package com.facebook.flipper.plugins.react;
import com.facebook.flipper.core.FlipperConnection;
import com.facebook.flipper.core.FlipperPlugin;
-// This plugin is not needed, but kept here for backward compatilibty
+// This plugin is not needed, but kept here for backward compatibility
@Deprecated
public class ReactFlipperPlugin implements FlipperPlugin {
diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/UIDebuggerFlipperPlugin.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/UIDebuggerFlipperPlugin.kt
index ecdd2417e..003c51d8b 100644
--- a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/UIDebuggerFlipperPlugin.kt
+++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/UIDebuggerFlipperPlugin.kt
@@ -7,24 +7,23 @@
package com.facebook.flipper.plugins.uidebugger
-import android.app.Application
+import android.util.Log
import com.facebook.flipper.core.FlipperConnection
import com.facebook.flipper.core.FlipperPlugin
-import com.facebook.flipper.plugins.uidebugger.core.ApplicationRef
-import com.facebook.flipper.plugins.uidebugger.core.ConnectionRef
-import com.facebook.flipper.plugins.uidebugger.core.Context
-import com.facebook.flipper.plugins.uidebugger.core.NativeScanScheduler
+import com.facebook.flipper.plugins.uidebugger.core.*
+import com.facebook.flipper.plugins.uidebugger.descriptors.ApplicationRefDescriptor
+import com.facebook.flipper.plugins.uidebugger.descriptors.MetadataRegister
import com.facebook.flipper.plugins.uidebugger.model.InitEvent
-import com.facebook.flipper.plugins.uidebugger.scheduler.Scheduler
+import com.facebook.flipper.plugins.uidebugger.model.MetadataUpdateEvent
import kotlinx.serialization.json.Json
-val LogTag = "FlipperUIDebugger"
+const val LogTag = "ui-debugger"
-class UIDebuggerFlipperPlugin(val application: Application) : FlipperPlugin {
+class UIDebuggerFlipperPlugin(val context: UIDContext) : FlipperPlugin {
- private val context: Context = Context(ApplicationRef(application), ConnectionRef(null))
-
- private val nativeScanScheduler = Scheduler(NativeScanScheduler(context))
+ init {
+ Log.i(LogTag, "Initializing ui-debugger")
+ }
override fun getId(): String {
return "ui-debugger"
@@ -33,22 +32,37 @@ class UIDebuggerFlipperPlugin(val application: Application) : FlipperPlugin {
@Throws(Exception::class)
override fun onConnect(connection: FlipperConnection) {
this.context.connectionRef.connection = connection
-
- val rootDescriptor =
- context.descriptorRegister.descriptorForClassUnsafe(context.applicationRef.javaClass)
+ this.context.bitmapPool.makeReady()
connection.send(
InitEvent.name,
Json.encodeToString(
- InitEvent.serializer(), InitEvent(rootDescriptor.getId(context.applicationRef))))
+ InitEvent.serializer(),
+ InitEvent(
+ ApplicationRefDescriptor.getId(context.applicationRef),
+ context.frameworkEventMetadata)))
- nativeScanScheduler.start()
+ connection.send(
+ MetadataUpdateEvent.name,
+ Json.encodeToString(
+ MetadataUpdateEvent.serializer(),
+ MetadataUpdateEvent(MetadataRegister.extractPendingMetadata())))
+
+ context.treeObserverManager.start()
+
+ context.connectionListeners.forEach { it.onConnect() }
}
@Throws(Exception::class)
override fun onDisconnect() {
this.context.connectionRef.connection = null
- this.nativeScanScheduler.stop()
+
+ MetadataRegister.reset()
+
+ context.treeObserverManager.stop()
+ context.bitmapPool.recycleAll()
+ context.connectionListeners.forEach { it.onDisconnect() }
+ context.clearFrameworkEvents()
}
override fun runInBackground(): Boolean {
diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/commands/Command.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/commands/Command.kt
index b442ef599..5a79866e5 100644
--- a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/commands/Command.kt
+++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/commands/Command.kt
@@ -11,10 +11,10 @@ import com.facebook.flipper.core.FlipperObject
import com.facebook.flipper.core.FlipperReceiver
import com.facebook.flipper.core.FlipperResponder
import com.facebook.flipper.plugins.common.MainThreadFlipperReceiver
-import com.facebook.flipper.plugins.uidebugger.core.Context
+import com.facebook.flipper.plugins.uidebugger.core.UIDContext
/** An interface for extensions to the UIDebugger plugin */
-abstract class Command(val context: Context) {
+abstract class Command(val context: UIDContext) {
/** The command identifier to respond to */
abstract fun identifier(): String
/** Execute the command */
diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/commands/CommandRegister.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/commands/CommandRegister.kt
index 054977382..9ab970c32 100644
--- a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/commands/CommandRegister.kt
+++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/commands/CommandRegister.kt
@@ -9,10 +9,8 @@ package com.facebook.flipper.plugins.uidebugger.commands
import com.facebook.flipper.core.FlipperConnection
-sealed class CommandRegister {
- companion object {
- fun register(connection: FlipperConnection, cmd: T) where T : Command {
- connection.receive(cmd.identifier(), cmd.receiver())
- }
+object CommandRegister {
+ fun register(connection: FlipperConnection, cmd: T) where T : Command {
+ connection.receive(cmd.identifier(), cmd.receiver())
}
}
diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/common/BitmapPool.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/common/BitmapPool.kt
new file mode 100644
index 000000000..778fc2e7c
--- /dev/null
+++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/common/BitmapPool.kt
@@ -0,0 +1,80 @@
+/*
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+package com.facebook.flipper.plugins.uidebugger.common
+
+import android.graphics.Bitmap
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+
+/** BitmapPool is intended to be used on the main thread. In other words, it is not thread-safe. */
+class BitmapPool(private val config: Bitmap.Config = Bitmap.Config.RGB_565) {
+
+ interface ReusableBitmap {
+ val bitmap: Bitmap?
+
+ fun readyForReuse()
+ }
+
+ val mainScope = CoroutineScope(Dispatchers.Main)
+
+ private val container: MutableMap> = mutableMapOf()
+ private var isRecycled = false
+
+ private fun generateKey(width: Int, height: Int): String = "$width,$height"
+
+ fun recycleAll() {
+ isRecycled = true
+ container.forEach { (_, bitmaps) ->
+ bitmaps.forEach { bitmap -> bitmap.recycle() }
+ bitmaps.clear()
+ }
+
+ container.clear()
+ }
+
+ fun makeReady() {
+ isRecycled = false
+ }
+
+ fun getBitmap(width: Int, height: Int): ReusableBitmap {
+ val key = generateKey(width, height)
+ val bitmaps = container[key]
+
+ return if (bitmaps == null || bitmaps.isEmpty()) {
+ LeasedBitmap(Bitmap.createBitmap(width, height, config))
+ } else {
+ LeasedBitmap(bitmaps.removeLast())
+ }
+ }
+
+ inner class LeasedBitmap(override val bitmap: Bitmap) : ReusableBitmap {
+ override fun readyForReuse() {
+ val key = generateKey(bitmap.width, bitmap.height)
+
+ mainScope.launch {
+ if (isRecycled) {
+ bitmap.recycle()
+ } else {
+ var bitmaps = container[key]
+ if (bitmaps == null) {
+ bitmaps = mutableListOf()
+ container[key] = bitmaps
+ }
+ bitmaps.add(bitmap)
+ }
+ }
+ }
+ }
+
+ companion object {
+ fun createBitmapWithDefaultConfig(width: Int, height: Int): Bitmap {
+ return Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565)
+ }
+ }
+}
diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/common/EnumMapping.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/common/EnumMapping.kt
index dbea0b0c9..5468dc901 100644
--- a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/common/EnumMapping.kt
+++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/common/EnumMapping.kt
@@ -9,34 +9,63 @@ package com.facebook.flipper.plugins.uidebugger.common
import android.util.Log
import com.facebook.flipper.plugins.uidebugger.LogTag
+import com.facebook.flipper.plugins.uidebugger.model.InspectableValue
// Maintains 2 way mapping between some enum value and a readable string representation
-open class EnumMapping(val mapping: Map) {
+open class EnumMapping(private val mapping: Map) {
fun getStringRepresentation(enumValue: T): String {
val entry = mapping.entries.find { (_, value) -> value == enumValue }
- if (entry != null) {
- return entry.key
+ return if (entry != null) {
+ entry.key
} else {
- Log.w(
+ Log.v(
LogTag,
"Could not convert enum value ${enumValue.toString()} to string, known values ${mapping.entries}")
- return NoMapping
+ NoMapping
}
}
fun getEnumValue(key: String): T {
- val value =
- mapping[key]
- ?: throw UIDebuggerException(
- "Could not convert string ${key} to enum value, possible values ${mapping.entries} ")
- return value
+ return mapping[key]
+ ?: throw UIDebuggerException(
+ "Could not convert string $key to enum value, possible values ${mapping.entries} ")
}
- fun toInspectable(value: T, mutable: Boolean): InspectableValue.Enum {
- return InspectableValue.Enum(EnumData(mapping.keys, getStringRepresentation(value)), mutable)
+ fun getInspectableValues(): Set {
+ val set: MutableSet = mutableSetOf()
+ mapping.entries.forEach { set.add(InspectableValue.Text(it.key)) }
+ return set
}
+
+ fun toInspectable(value: T): InspectableValue.Enum {
+ return InspectableValue.Enum(getStringRepresentation(value))
+ }
+
companion object {
- val NoMapping = "__UNKNOWN_ENUM_VALUE__"
+ const val NoMapping = "__UNKNOWN_ENUM_VALUE__"
}
}
+
+inline fun > enumerator(): Iterator = enumValues().iterator()
+
+inline fun > enumToSet(): Set {
+ val set = mutableSetOf()
+ val values = enumerator()
+ values.forEach { set.add(it.name) }
+ return set
+}
+
+inline fun > enumToInspectableSet(): Set {
+ val set = mutableSetOf()
+ val values = enumerator()
+ values.forEach { set.add(InspectableValue.Text(it.name)) }
+ return set
+}
+
+inline fun > enumMapping(): EnumMapping {
+ val map = mutableMapOf()
+ val values = enumerator()
+ values.forEach { map[it.name] = it }
+ return EnumMapping(map)
+}
diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/ActivityTracker.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/ActivityTracker.kt
new file mode 100644
index 000000000..15e707cb8
--- /dev/null
+++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/ActivityTracker.kt
@@ -0,0 +1,143 @@
+/*
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+package com.facebook.flipper.plugins.uidebugger.core
+
+import android.annotation.SuppressLint
+import android.app.Activity
+import android.app.Application
+import android.os.Build
+import android.os.Bundle
+import java.lang.ref.WeakReference
+import java.lang.reflect.Field
+import java.lang.reflect.Method
+
+object ActivityTracker : Application.ActivityLifecycleCallbacks {
+ interface ActivityStackChangedListener {
+ fun onActivityAdded(activity: Activity, stack: List)
+
+ fun onActivityStackChanged(stack: List)
+
+ fun onActivityDestroyed(activity: Activity, stack: List)
+ }
+
+ private val activities: MutableList> = mutableListOf()
+ private val trackedActivities: MutableSet = mutableSetOf()
+ private var activityStackChangedListener: ActivityStackChangedListener? = null
+
+ fun setActivityStackChangedListener(listener: ActivityStackChangedListener?) {
+ activityStackChangedListener = listener
+ }
+
+ fun start(application: Application) {
+ initialiseActivities()
+ application.registerActivityLifecycleCallbacks(this)
+ }
+
+ private fun trackActivity(activity: Activity) {
+ if (trackedActivities.contains(System.identityHashCode(activity))) {
+ return
+ }
+
+ trackedActivities.add(System.identityHashCode(activity))
+ activities.add(WeakReference(activity))
+
+ FragmentTracker.trackFragmentsOfActivity(activity)
+
+ activityStackChangedListener?.onActivityAdded(activity, this.activitiesStack)
+ activityStackChangedListener?.onActivityStackChanged(this.activitiesStack)
+ }
+
+ private fun untrackActivity(activity: Activity) {
+ trackedActivities.remove(System.identityHashCode(activity))
+ val activityIterator: MutableIterator> = activities.iterator()
+
+ while (activityIterator.hasNext()) {
+ if (activityIterator.next().get() === activity) {
+ activityIterator.remove()
+ }
+ }
+
+ FragmentTracker.untrackFragmentsOfActivity(activity)
+
+ activityStackChangedListener?.onActivityDestroyed(activity, this.activitiesStack)
+ activityStackChangedListener?.onActivityStackChanged(this.activitiesStack)
+ }
+
+ override fun onActivityCreated(activity: Activity, bundle: Bundle?) {
+ trackActivity(activity)
+ }
+
+ override fun onActivityStarted(activity: Activity) {
+ trackActivity(activity)
+ }
+
+ override fun onActivityResumed(activity: Activity) {}
+
+ override fun onActivityPaused(activity: Activity) {}
+
+ override fun onActivityStopped(activity: Activity) {}
+
+ override fun onActivitySaveInstanceState(activity: Activity, bundle: Bundle) {}
+
+ override fun onActivityDestroyed(activity: Activity) {
+ untrackActivity(activity)
+ }
+
+ val activitiesStack: List
+ get() {
+ val stack: MutableList = ArrayList(activities.size)
+ val activityIterator: MutableIterator> = activities.iterator()
+ while (activityIterator.hasNext()) {
+ val activity: Activity? = activityIterator.next().get()
+ if (activity == null) {
+ activityIterator.remove()
+ } else {
+ stack.add(activity)
+ }
+ }
+ return stack
+ }
+
+ /**
+ * Activity tracker is used to track activities. However, it cannot track via life-cycle events
+ * all those activities that were created prior to initialisation via the `start(application:
+ * Application)` method.
+ *
+ * As such, the method below makes a 'best effort' to find these untracked activities and add them
+ * to the tracked list.
+ */
+ @SuppressLint("PrivateApi", "DiscouragedPrivateApi")
+ fun initialiseActivities() {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
+ return
+ }
+
+ try {
+ val activityThreadClass: Class<*> = Class.forName("android.app.ActivityThread")
+ val currentActivityThreadMethod: Method =
+ activityThreadClass.getMethod("currentActivityThread")
+ val currentActivityThread: Any? = currentActivityThreadMethod.invoke(null)
+
+ currentActivityThread?.let { activityThread ->
+ val mActivitiesField: Field = activityThreadClass.getDeclaredField("mActivities")
+ mActivitiesField.isAccessible = true
+ val mActivities = mActivitiesField.get(activityThread) as android.util.ArrayMap<*, *>
+ for (record in mActivities.values) {
+ val recordClass: Class<*> = record.javaClass
+ val activityField: Field = recordClass.getDeclaredField("activity")
+ activityField.isAccessible = true
+
+ val activity = activityField.get(record)
+ if (activity != null && activity is Activity) {
+ trackActivity(activity)
+ }
+ }
+ }
+ } catch (e: Exception) {}
+ }
+}
diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/ApplicationInspector.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/ApplicationInspector.kt
deleted file mode 100644
index 93e8e735c..000000000
--- a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/ApplicationInspector.kt
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * Copyright (c) Meta Platforms, Inc. and affiliates.
- *
- * This source code is licensed under the MIT license found in the
- * LICENSE file in the root directory of this source tree.
- */
-
-package com.facebook.flipper.plugins.uidebugger.core
-
-import android.view.View
-import android.view.ViewTreeObserver
-import com.facebook.flipper.plugins.uidebugger.descriptors.DescriptorRegister
-
-class ApplicationInspector(val context: Context) {
- val descriptorRegister = DescriptorRegister.withDefaults()
- val traversal = LayoutTraversal(descriptorRegister, context.applicationRef)
-
- fun attachListeners(view: View) {
- // An OnGlobalLayoutListener watches the entire hierarchy for layout changes
- // (so registering one of these on any View in a hierarchy will cause it to be triggered
- // when any View in that hierarchy is laid out or changes visibility).
- view
- .getViewTreeObserver()
- .addOnGlobalLayoutListener(
- object : ViewTreeObserver.OnGlobalLayoutListener {
- override fun onGlobalLayout() {}
- })
- view
- .getViewTreeObserver()
- .addOnPreDrawListener(
- object : ViewTreeObserver.OnPreDrawListener {
- override fun onPreDraw(): Boolean {
- return true
- }
- })
- }
-
- fun observe() {
- val rootResolver = RootViewResolver()
- rootResolver.attachListener(
- object : RootViewResolver.Listener {
- override fun onRootViewAdded(view: View) {
- attachListeners(view)
- }
-
- override fun onRootViewRemoved(view: View) {}
- override fun onRootViewsChanged(views: java.util.List) {}
- })
-
- val activeRoots = rootResolver.listActiveRootViews()
- activeRoots?.let { roots -> for (root: RootViewResolver.RootView in roots) {} }
- }
-}
diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/ApplicationObserver.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/ApplicationObserver.kt
deleted file mode 100644
index 650e0459d..000000000
--- a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/ApplicationObserver.kt
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- * Copyright (c) Meta Platforms, Inc. and affiliates.
- *
- * This source code is licensed under the MIT license found in the
- * LICENSE file in the root directory of this source tree.
- */
-
-package com.facebook.flipper.plugins.uidebugger.core
-
-import android.app.Activity
-import android.app.Application
-import android.os.Bundle
-import android.view.View
-import java.lang.ref.WeakReference
-
-class ApplicationObserver(val application: Application) : Application.ActivityLifecycleCallbacks {
- interface ActivityStackChangedListener {
- fun onActivityStackChanged(stack: List)
- }
-
- interface ActivityDestroyedListener {
- fun onActivityDestroyed(activity: Activity)
- }
-
- private val rootsResolver: RootViewResolver
- private val activities: MutableList>
- private var activityStackChangedlistener: ActivityStackChangedListener? = null
- private var activityDestroyedListener: ActivityDestroyedListener? = null
-
- override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
- activities.add(WeakReference(activity))
- activityStackChangedlistener?.let { listener ->
- listener.onActivityStackChanged(this.activitiesStack)
- }
- }
- override fun onActivityStarted(activity: Activity) {}
- override fun onActivityResumed(activity: Activity) {}
- override fun onActivityPaused(activity: Activity) {}
- override fun onActivityStopped(activity: Activity) {}
- override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
- override fun onActivityDestroyed(activity: Activity) {
- val activityIterator: MutableIterator> = activities.iterator()
-
- while (activityIterator.hasNext()) {
- if (activityIterator.next().get() === activity) {
- activityIterator.remove()
- }
- }
-
- activityDestroyedListener?.let { listener -> listener.onActivityDestroyed(activity) }
-
- activityStackChangedlistener?.let { listener ->
- listener.onActivityStackChanged(this.activitiesStack)
- }
- }
-
- fun setActivityStackChangedListener(listener: ActivityStackChangedListener?) {
- activityStackChangedlistener = listener
- }
-
- fun setActivityDestroyedListener(listener: ActivityDestroyedListener?) {
- activityDestroyedListener = listener
- }
-
- val activitiesStack: List
- get() {
- val stack: MutableList = ArrayList(activities.size)
- val activityIterator: MutableIterator> = activities.iterator()
- while (activityIterator.hasNext()) {
- val activity: Activity? = activityIterator.next().get()
- if (activity == null) {
- activityIterator.remove()
- } else {
- stack.add(activity)
- }
- }
- return stack
- }
-
- val rootViews: List
- get() {
- val roots = rootsResolver.listActiveRootViews()
- roots?.let { roots ->
- val viewRoots: MutableList = ArrayList(roots.size)
- for (root in roots) {
- viewRoots.add(root.view)
- }
- return viewRoots
- }
-
- return emptyList()
- }
-
- init {
- rootsResolver = RootViewResolver()
- application.registerActivityLifecycleCallbacks(this)
- activities = ArrayList>()
- }
-}
diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/ApplicationRef.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/ApplicationRef.kt
index 04d66daf8..1886cec59 100644
--- a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/ApplicationRef.kt
+++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/ApplicationRef.kt
@@ -9,91 +9,20 @@ package com.facebook.flipper.plugins.uidebugger.core
import android.app.Activity
import android.app.Application
-import android.os.Bundle
-import android.view.View
-import java.lang.ref.WeakReference
-class ApplicationRef(val application: Application) : Application.ActivityLifecycleCallbacks {
- interface ActivityStackChangedListener {
- fun onActivityStackChanged(stack: List)
+class ApplicationRef(val application: Application) {
+ init {
+ ActivityTracker.start(application)
}
- interface ActivityDestroyedListener {
- fun onActivityDestroyed(activity: Activity)
- }
-
- private val rootsResolver: RootViewResolver
- private val activities: MutableList>
- private var activityStackChangedlistener: ActivityStackChangedListener? = null
- private var activityDestroyedListener: ActivityDestroyedListener? = null
-
- override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
- activities.add(WeakReference(activity))
- activityStackChangedlistener?.let { listener ->
- listener.onActivityStackChanged(this.activitiesStack)
- }
- }
- override fun onActivityStarted(activity: Activity) {}
- override fun onActivityResumed(activity: Activity) {}
- override fun onActivityPaused(activity: Activity) {}
- override fun onActivityStopped(activity: Activity) {}
- override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
- override fun onActivityDestroyed(activity: Activity) {
- val activityIterator: MutableIterator> = activities.iterator()
-
- while (activityIterator.hasNext()) {
- if (activityIterator.next().get() === activity) {
- activityIterator.remove()
- }
- }
-
- activityDestroyedListener?.let { listener -> listener.onActivityDestroyed(activity) }
-
- activityStackChangedlistener?.let { listener ->
- listener.onActivityStackChanged(this.activitiesStack)
- }
- }
-
- fun setActivityStackChangedListener(listener: ActivityStackChangedListener?) {
- activityStackChangedlistener = listener
- }
-
- fun setActivityDestroyedListener(listener: ActivityDestroyedListener?) {
- activityDestroyedListener = listener
- }
+ // the root view resolver will contain all root views 100% It is needed for 2 cases:
+ // 1. In some cases an activity will not be picked up by the activity tracker,
+ // the root view resolver will at least find the decor view
+ // 2. Dialog fragments
+ val rootsResolver: RootViewResolver = RootViewResolver()
val activitiesStack: List
get() {
- val stack: MutableList = ArrayList(activities.size)
- val activityIterator: MutableIterator> = activities.iterator()
- while (activityIterator.hasNext()) {
- val activity: Activity? = activityIterator.next().get()
- if (activity == null) {
- activityIterator.remove()
- } else {
- stack.add(activity)
- }
- }
- return stack
+ return ActivityTracker.activitiesStack
}
-
- val rootViews: List
- get() {
- val roots = rootsResolver.listActiveRootViews()
- roots?.let { roots ->
- val viewRoots: MutableList = ArrayList(roots.size)
- for (root in roots) {
- viewRoots.add(root.view)
- }
- return viewRoots
- }
-
- return emptyList()
- }
-
- init {
- rootsResolver = RootViewResolver()
- application.registerActivityLifecycleCallbacks(this)
- activities = ArrayList>()
- }
}
diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/Context.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/Context.kt
deleted file mode 100644
index fb795e4df..000000000
--- a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/Context.kt
+++ /dev/null
@@ -1,19 +0,0 @@
-/*
- * Copyright (c) Meta Platforms, Inc. and affiliates.
- *
- * This source code is licensed under the MIT license found in the
- * LICENSE file in the root directory of this source tree.
- */
-
-package com.facebook.flipper.plugins.uidebugger.core
-
-import com.facebook.flipper.core.FlipperConnection
-import com.facebook.flipper.plugins.uidebugger.descriptors.DescriptorRegister
-
-data class Context(
- val applicationRef: ApplicationRef,
- val connectionRef: ConnectionRef,
- val descriptorRegister: DescriptorRegister = DescriptorRegister.withDefaults()
-)
-
-data class ConnectionRef(var connection: FlipperConnection?)
diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/FragmentTracker.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/FragmentTracker.kt
new file mode 100644
index 000000000..65e040de8
--- /dev/null
+++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/FragmentTracker.kt
@@ -0,0 +1,274 @@
+/*
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+package com.facebook.flipper.plugins.uidebugger.core
+
+import android.app.Activity
+import android.os.Build
+import android.os.Bundle
+import android.view.View
+import java.lang.ref.WeakReference
+
+object FragmentTracker {
+
+ class FragmentRef {
+ val supportFragment: WeakReference