Merge branch '0.227' into universalBuild

This commit is contained in:
2023-10-18 10:19:18 +02:00
1013 changed files with 58711 additions and 20312 deletions

View File

@@ -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:

View File

@@ -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

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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 }}'

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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:

View File

@@ -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

View File

@@ -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: |

View File

@@ -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

View File

@@ -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 }}"}'

12
.gitignore vendored
View File

@@ -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

View File

@@ -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'

View File

@@ -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 <FBDefines/FBDefines.h>
@@ -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'

View File

@@ -14,7 +14,7 @@
</p>
<p align="center">
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.
</p>
![Flipper](website/static/img/inspector.png)
@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -7,5 +7,5 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.facebook.flipper">
package="com.facebook.flipper.noop">
</manifest>

View File

@@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.facebook.flipper.plugins.fresco">
</manifest>

View File

@@ -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);
}

View File

@@ -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<CacheKey> ALWAYS_TRUE_PREDICATE =
new Predicate<CacheKey>() {
@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<FlipperObject> 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<Integer, Integer> 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<EncodedImage> t =
Fresco.getImagePipelineFactory()
.getMainBufferedDiskCache()
.get(cacheKey, new AtomicBoolean(false));
t.continueWith(
new Continuation<EncodedImage, Void>() {
public Void then(Task<EncodedImage> 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<DiskStorage.DiskDumpInfoEntry> 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<DumpInfoEntry<CacheKey, CloseableImage>> images) {
FlipperArray.Builder builder = new FlipperArray.Builder();
for (DumpInfoEntry<CacheKey, CloseableImage> 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<DiskStorage.DiskDumpInfoEntry> 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<Bitmap> 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<CacheKey> 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<Object> 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();
}
}

View File

@@ -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);
}
}

View File

@@ -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<String, String> stringMap) {
if (stringMap == null) {
return null;
}
FlipperObject.Builder optionsJson = new FlipperObject.Builder();
for (Map.Entry<String, String> 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);
}

View File

@@ -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
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'
}
}
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'
}
}
}
apply plugin: 'com.vanniktech.maven.publish'

View File

@@ -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

View File

@@ -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)
}
}

View File

@@ -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<ComposeInnerViewNode> {
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<Any> {
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<Map<MetadataId, InspectableObject>> {
if (node.view is ViewGroup) {
return ViewGroupDescriptor.getAttributes(node.view)
}
return ViewDescriptor.getAttributes(node.view)
}
override fun getTags(node: ComposeInnerViewNode): Set<String> {
if (node.view is ViewGroup) {
return ViewGroupDescriptor.getTags(node.view)
}
return ViewDescriptor.getTags(node.view)
}
}

View File

@@ -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<ComposeNode> {
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<Any> {
return node.children
}
override fun getAttributes(node: ComposeNode): MaybeDeferred<Map<MetadataId, InspectableObject>> {
val builder = mutableMapOf<MetadataId, InspectableObject>()
val props = mutableMapOf<Int, Inspectable>()
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<Int, Inspectable>()
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<Int, Inspectable>()
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<Int, Inspectable>()
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<String> = setOf(BaseTags.Android, "Compose")
override fun getId(node: ComposeNode): Id = node.inspectorNode.id.toInt()
}

View File

@@ -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<ComposeView>() {
override fun onGetName(node: ComposeView): String = node.javaClass.simpleName
private fun transform(view: View, nodes: List<InspectorNode>): List<ComposeNode> {
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<Any> {
val children = mutableListOf<Any>()
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
}
}

View File

@@ -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)
}

View File

@@ -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<Any> = collectChildren()
private fun collectChildren(): List<Any> {
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
}
}

View File

@@ -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

View File

@@ -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<String, (Any) -> 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
}
}

View File

@@ -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<RawParameter>,
/** The id of a android View embedded under this node. */
val viewId: Long,
/** The merged semantics information of this Composable. */
val mergedSemantics: List<RawParameter>,
/** The un-merged semantics information of this Composable. */
val unmergedSemantics: List<RawParameter>,
/** The children nodes of this Composable. */
val children: List<InspectorNode>
) {
/** 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<RawParameter> =
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<LayoutInfo>()
val mergedSemantics = mutableListOf<RawParameter>()
val unmergedSemantics = mutableListOf<RawParameter>()
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<RawParameter>()
var viewId = UNDEFINED_ID
val children = mutableListOf<InspectorNode>()
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())
}

View File

@@ -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<MutableInspectorNode>()
private var generatedId = -1L
private val subCompositions = SubCompositionRoots()
/** Map from [LayoutInfo] to the nearest [InspectorNode] that contains it */
private val claimedNodes = IdentityHashMap<LayoutInfo, InspectorNode>()
/** Map from parent tree to child trees that are about to be stitched together */
private val treeMap = IdentityHashMap<MutableInspectorNode, MutableList<MutableInspectorNode>>()
/** Map from owner node to child trees that are about to be stitched to this owner */
private val ownerMap = IdentityHashMap<InspectorNode, MutableList<MutableInspectorNode>>()
/** Map from semantics id to a list of merged semantics information */
private val semanticsMap = mutableMapOf<Int, List<RawParameter>>()
/* Map of seemantics id to a list of unmerged semantics information */
private val unmergedSemanticsMap = mutableMapOf<Int, List<RawParameter>>()
/** Set of tree nodes that were stitched into another tree */
private val stitched = Collections.newSetFromMap(IdentityHashMap<MutableInspectorNode, Boolean>())
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<InspectorNode> {
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<CompositionData> ?: 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<CompositionData> ?: 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<InspectorNode>): List<InspectorNode> =
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<NodeParameter> {
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<CompositionData>, view: View): List<InspectorNode> {
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<MutableInspectorNode>): List<InspectorNode> {
val layoutToTreeMap = IdentityHashMap<LayoutInfo, MutableInspectorNode>()
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<InspectorNode>()
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<InspectorNode>,
tree: MutableInspectorNode
): List<InspectorNode> {
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>
): 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<MutableInspectorNode>,
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>
): 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<GraphicLayerInfo>()
.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<LayoutInfo>, view: View): Boolean =
layoutNodes
.asSequence()
.flatMap { node ->
node
.getModifierInfo()
.asSequence()
.map { it.extra }
.filterIsInstance<GraphicLayerInfo>()
.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<MutableInspectorNode>
): 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<Long, InspectorNode>()
/** 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<ViewRootForInspector>().singleOrNull()?.let {
return it
}
val refs = group.data.filterIsInstance<Ref<*>>().map { it.value }
return refs.filterIsInstance<ViewRootForInspector>().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<InspectorNode>): List<InspectorNode> {
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<InspectorNode>,
/** 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
}
}

View File

@@ -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<NodeParameter>()
/** 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,
}

View File

@@ -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<Int>
) : 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
}

View File

@@ -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"),
)

View File

@@ -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 <T> 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<ReflectionFactory>()
fun <T> 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<KTypeProjection>?,
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<KType>?) {
factory.setUpperBounds(typeParameter, bounds)
}
}

View File

@@ -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<Int, Any>()
private val idLookup = IdentityHashMap<Any, Int>()
/** 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
}
}

View File

@@ -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<Int>.asIntArray() = if (isNotEmpty()) toIntArray() else EMPTY_INT_ARRAY

View File

@@ -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 <T> runOnMainThread(block: () -> T): Future<T> {
return if (!Looper.getMainLooper().isCurrentThread) {
val future = CompletableFuture<T>()
Handler.createAsync(Looper.getMainLooper()).post { future.complete(block()) }
future
} else {
CompletableFuture.completedFuture(block())
}
}
}

View File

@@ -8,6 +8,7 @@
apply plugin: 'com.android.library'
android {
namespace 'com.facebook.flipper.plugins.leakcanary'
compileSdkVersion rootProject.compileSdkVersion
buildToolsVersion rootProject.buildToolsVersion

View File

@@ -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

View File

@@ -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<Leak> = 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<Leak> {
return if (this is HeapAnalysisSuccess) {
allLeaks
.mapNotNull {
if (it.leakTraces.isNotEmpty()) {
it.leakTraces[0].toLeak(it.shortDescription)
} else {
null
}
}
.toList()
} else {
emptyList()
}
}
}

View File

@@ -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<Leak> = mutableListOf()

View File

@@ -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) {

View File

@@ -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<String>("key")?.let {
DebugComponent.generateGlobalKey(treeId, it).hashCode()
}
val duration = event.attributeOrNull<Duration>("duration")
val attributes = mutableMapOf<String, String>()
val source =
event.attributeOrNull<String>(
"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))
}
}

View File

@@ -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<ComponentTree> {
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<Any> {
val result = mutableListOf<Any>()
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<Map<MetadataId, InspectableObject>> {
return Immediate(mapOf())
}
override fun getTags(node: ComponentTree): Set<String> = setOf(LithoTag, "TreeRoot")
}

View File

@@ -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<DebugComponent> {
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<Any> {
val result = mutableListOf<Any>()
val mountedContent = node.mountedContent
if (mountedContent == null) {
for (child in node.childComponents) {
result.add(child)
}
} else {
val layoutNode = node.layoutNode
val descriptor: NodeDescriptor<Any> =
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<Map<MetadataId, InspectableObject>> {
return Deferred {
val attributeSections = mutableMapOf<MetadataId, InspectableObject>()
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<String> {
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<String, String> {
val attributes = mutableMapOf<String, String>()
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<Id, Inspectable> {
val lithoView = node.lithoView
val mountingData = mutableMapOf<MetadataId, Inspectable>()
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
}
}

View File

@@ -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<LithoView>() {
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<Any> {
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<MetadataId, InspectableObject>
) {
attributeSections[SectionId] =
InspectableObject(
mapOf(
IsIncrementalMountEnabledAttributeId to
InspectableValue.Boolean(node.isIncrementalMountEnabled)))
}
}

View File

@@ -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<MatrixDrawable<*>>() {
override fun onGetChildren(node: MatrixDrawable<*>): List<Any>? {
val mountedDrawable = node.mountedDrawable
return if (mountedDrawable != null) {
listOf(mountedDrawable)
} else {
listOf()
}
}
override fun onGetName(node: MatrixDrawable<*>): String = node.javaClass.simpleName
}

View File

@@ -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<TextDrawable>() {
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<MetadataId, InspectableObject>
) {
val props =
mapOf<Int, Inspectable>(TextAttributeId to InspectableValue.Text(node.text.toString()))
attributeSections[SectionId] = InspectableObject(props)
}
}

View File

@@ -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<MetadataId, Inspectable> {
val props = mutableMapOf<MetadataId, Inspectable>()
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<MetadataId, Inspectable>()
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<InspectableValue>? = 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<MetadataId, Inspectable>,
namespace: String,
name: String,
value: EditorValue
) {
var possibleValues: MutableSet<InspectableValue>? = 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<Inspectable> {
override fun isShape(shape: EditorShape): Inspectable {
val fields = mutableMapOf<MetadataId, Inspectable>()
shape.value.entries.forEach { entry ->
val value = toInspectable(entry.key, entry.value)
val shapeEditorValue = entry.value
var possibleValues: MutableSet<InspectableValue>? = 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())
}
}

View File

@@ -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<YogaDirection>())
private val FlexDirectionId =
MetadataRegister.register(
MetadataRegister.TYPE_ATTRIBUTE,
NAMESPACE,
"flexDirection",
false,
enumToInspectableSet<YogaFlexDirection>())
private val JustifyContentId =
MetadataRegister.register(
MetadataRegister.TYPE_ATTRIBUTE,
NAMESPACE,
"justifyContent",
false,
enumToInspectableSet<YogaJustify>())
private val AlignItemsId =
MetadataRegister.register(
MetadataRegister.TYPE_ATTRIBUTE,
NAMESPACE,
"alignItems",
false,
enumToInspectableSet<YogaAlign>())
private val AlignSelfId =
MetadataRegister.register(
MetadataRegister.TYPE_ATTRIBUTE,
NAMESPACE,
"alignSelf",
false,
enumToInspectableSet<YogaAlign>())
private val AlignContentId =
MetadataRegister.register(
MetadataRegister.TYPE_ATTRIBUTE,
NAMESPACE,
"alignContent",
false,
enumToInspectableSet<YogaAlign>())
private val PositionTypeId =
MetadataRegister.register(
MetadataRegister.TYPE_ATTRIBUTE,
NAMESPACE,
"positionType",
false,
enumToInspectableSet<YogaPositionType>())
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<MetadataId, Inspectable>()
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<MetadataId, Inspectable>()
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<MetadataId, Inspectable> {
val props = mutableMapOf<MetadataId, Inspectable>()
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<MetadataId, Inspectable> = 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<MetadataId, Inspectable> = 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())
}
}

View File

@@ -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"))))))
}
}

View File

@@ -8,6 +8,7 @@
apply plugin: 'com.android.library'
android {
namespace 'com.facebook.flipper.plugins.network'
compileSdkVersion rootProject.compileSdkVersion
buildToolsVersion rootProject.buildToolsVersion

View File

@@ -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

View File

@@ -11,6 +11,8 @@
xmlns:tools="http://schemas.android.com/tools"
package="com.facebook.flipper.sample">
<uses-permission android:name="android.permission.INTERNET" />
<uses-sdk android:minSdkVersion="15"
android:targetSdkVersion="31"/>
<application
android:name=".FlipperSampleApplication"
@@ -60,7 +62,7 @@
<data android:scheme="flipper" android:host="list_activity" />
</intent-filter>
</activity>
<activity android:name=".IncrementActivity"
<activity android:name=".ButtonsActivity"
android:exported="true" android:hardwareAccelerated="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
@@ -87,6 +89,15 @@
<data android:scheme="flipper" android:host="fragment_test_activity" />
</intent-filter>
</activity>
<activity android:name=".JetpackComposeActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="flipper" android:host="jetpack_compose_activity" />
</intent-filter>
</activity>
<activity android:name="com.facebook.flipper.android.diagnostics.FlipperDiagnosticActivity"
android:exported="true"/>
<activity android:name="com.facebook.flipper.connectivitytest.ConnectionTestActivity"

View File

@@ -5,12 +5,17 @@
* LICENSE file in the root directory of this source tree.
*/
apply plugin: 'com.android.application'
plugins {
id 'com.android.application'
id 'kotlin-android'
}
android {
namespace 'com.facebook.flipper.sample'
compileSdkVersion rootProject.compileSdkVersion
buildToolsVersion rootProject.buildToolsVersion
ndkVersion rootProject.ndkVersion
defaultConfig {
minSdkVersion 21
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
@@ -34,9 +39,25 @@ android {
}
}
compileOptions {
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_1_8
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.4.6"
}
kotlinOptions {
jvmTarget = "1.8"
}
packagingOptions {
pickFirst "**/libcrypto.so"
pickFirst "**/libevent-2.1.so"
pickFirst "**/libevent_core-2.1.so"
pickFirst "**/libevent_extra-2.1.so"
pickFirst "**/libflipper.so"
pickFirst "**/libssl.so"
}
}
@@ -50,13 +71,27 @@ dependencies {
implementation deps.lithoWidget
implementation deps.lithoAnnotations
implementation deps.lithoFresco
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
annotationProcessor deps.lithoProcessor
// Compose
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.activity:activity-ktx:1.7.2'
implementation 'androidx.compose.runtime:runtime:1.4.3'
implementation 'androidx.activity:activity-compose:1.7.2'
implementation 'androidx.compose.ui:ui:1.4.3'
implementation 'androidx.compose.material3:material3:1.1.1'
implementation 'androidx.compose.ui:ui-tooling:1.4.3'
implementation 'androidx.compose.ui:ui-tooling-preview:1.4.3'
// Third-party
implementation deps.soloader
implementation deps.okhttp3
implementation deps.fresco
implementation deps.frescoUiCommon
implementation deps.frescoVito
implementation deps.frescoVitoLitho
implementation deps.inferAnnotations
debugImplementation deps.flipperFrescoPlugin
// Integration test
androidTestImplementation deps.testCore
@@ -69,8 +104,8 @@ dependencies {
testImplementation deps.junit
debugImplementation project(':android')
debugImplementation project(':fresco-plugin')
debugImplementation project(':network-plugin')
debugImplementation project(':litho-plugin')
debugImplementation project(':jetpack-compose-plugin')
releaseImplementation project(':noop')
}

View File

@@ -16,12 +16,17 @@ import com.facebook.flipper.plugins.example.ExampleFlipperPlugin;
import com.facebook.flipper.plugins.fresco.FrescoFlipperPlugin;
import com.facebook.flipper.plugins.inspector.DescriptorMapping;
import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin;
import com.facebook.flipper.plugins.jetpackcompose.UIDebuggerComposeSupport;
import com.facebook.flipper.plugins.navigation.NavigationFlipperPlugin;
import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor;
import com.facebook.flipper.plugins.network.NetworkFlipperPlugin;
import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin;
import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin.SharedPreferencesDescriptor;
import com.facebook.flipper.plugins.uidebugger.UIDebuggerFlipperPlugin;
import com.facebook.flipper.plugins.uidebugger.core.UIDContext;
import com.facebook.flipper.plugins.uidebugger.descriptors.DescriptorRegister;
import com.facebook.flipper.plugins.uidebugger.litho.UIDebuggerLithoSupport;
import com.facebook.flipper.plugins.uidebugger.observers.TreeObserverFactory;
import com.facebook.litho.config.ComponentsConfiguration;
import com.facebook.litho.editor.flipper.LithoFlipperDescriptors;
import java.util.Arrays;
@@ -29,11 +34,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 DescriptorMapping descriptorMapping = DescriptorMapping.withDefaults();
final NetworkFlipperPlugin networkPlugin = new NetworkFlipperPlugin();
@@ -56,7 +61,14 @@ public final class FlipperInitializer {
client.addPlugin(CrashReporterPlugin.getInstance());
client.addPlugin(new DatabasesFlipperPlugin(context));
client.addPlugin(NavigationFlipperPlugin.getInstance());
client.addPlugin(new UIDebuggerFlipperPlugin((Application) context));
DescriptorRegister descriptorRegister = DescriptorRegister.Companion.withDefaults();
TreeObserverFactory treeObserverFactory = TreeObserverFactory.Companion.withDefaults();
UIDContext uidContext = UIDContext.Companion.create((Application) context);
UIDebuggerLithoSupport.INSTANCE.enable(uidContext);
UIDebuggerComposeSupport.INSTANCE.enable(uidContext);
client.addPlugin(new UIDebuggerFlipperPlugin(uidContext));
client.start();
final OkHttpClient okHttpClient =
@@ -67,7 +79,7 @@ public final class FlipperInitializer {
.writeTimeout(10, TimeUnit.MINUTES)
.build();
return new IntializationResult() {
return new InitializationResult() {
@Override
public OkHttpClient getOkHttpClient() {
return okHttpClient;

View File

@@ -7,85 +7,69 @@
package com.facebook.flipper.sample;
import android.animation.ValueAnimator;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.view.animation.LinearInterpolator;
import android.widget.Button;
import android.widget.TextView;
public class AnimationsActivity extends Activity {
Button btnBlink, btnRotate, btnMove, btnBounce, btnSequential;
Button btnBlink, btnRotate, btnMove, btnBounce, btnSequential, btnValueAnimator;
Animation animBlink, animRotate, animMove, animBounce, animSequential;
TextView txtBlink, txtRotate, txtMove, txtBounce, txtSeq;
TextView txtBlink, txtRotate, txtMove, txtBounce, txtSeq, txtValueAnimator;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_animations);
btnBlink = (Button) findViewById(R.id.btnBlink);
btnRotate = (Button) findViewById(R.id.btnRotate);
btnMove = (Button) findViewById(R.id.btnMove);
btnBounce = (Button) findViewById(R.id.btnBounce);
btnSequential = (Button) findViewById(R.id.btnSequential);
txtBlink = (TextView) findViewById(R.id.txt_blink);
txtRotate = (TextView) findViewById(R.id.txt_rotate);
txtMove = (TextView) findViewById(R.id.txt_move);
txtBounce = (TextView) findViewById(R.id.txt_bounce);
txtSeq = (TextView) findViewById(R.id.txt_seq);
btnBlink = findViewById(R.id.btnBlink);
btnBlink.setTranslationX(500);
btnRotate = findViewById(R.id.btnRotate);
btnMove = findViewById(R.id.btnMove);
btnBounce = findViewById(R.id.btnBounce);
btnSequential = findViewById(R.id.btnSequential);
btnValueAnimator = findViewById(R.id.btnValueAnimator);
txtBlink = findViewById(R.id.txt_blink);
txtRotate = findViewById(R.id.txt_rotate);
txtMove = findViewById(R.id.txt_move);
txtBounce = findViewById(R.id.txt_bounce);
txtSeq = findViewById(R.id.txt_seq);
txtValueAnimator = findViewById(R.id.txtValueAnimator);
btnValueAnimator.setOnClickListener(
b -> {
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) {
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));
}
}

View File

@@ -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();
});
}
}

View File

@@ -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());

View File

@@ -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;
}
}

View File

@@ -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));
}
});
}
}

View File

@@ -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") }
}
}

View File

@@ -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");

View File

@@ -29,7 +29,9 @@ public class MainActivity extends AppCompatActivity {
final FlipperClient client = AndroidFlipperClient.getInstanceIfInitialized();
if (client != null) {
final ExampleFlipperPlugin samplePlugin = client.getPluginByClass(ExampleFlipperPlugin.class);
if (samplePlugin != null) {
samplePlugin.setActivity(this);
}
}
}
}

View File

@@ -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)
displayImage
? FrescoVitoImage2.create(c)
.uri(Uri.parse("https://fbflipper.com/img/icon.png"))
.marginDip(YogaEdge.ALL, 10)
.widthDip(150)
.heightDip(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);
}
}

View File

@@ -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();
}
}

View File

@@ -12,6 +12,7 @@
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
@@ -103,6 +104,30 @@
android:id="@+id/txt_rotate"
/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
>
<Button
android:id="@+id/btnValueAnimator"
android:layout_width="50dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Value animator"
/>
<TextView
android:id="@+id/txtValueAnimator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Value Animator"
/>
</LinearLayout>
</LinearLayout>

View File

@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ButtonsActivity"
android:orientation="vertical"
>
<TextView
android:id="@+id/count"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="0"/>
<Button
android:id="@+id/btn_inc"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/darker_gray"
android:text="Inc"
android:textColor="@android:color/holo_blue_dark"
/>
<Button
android:id="@+id/dialog_old_api"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/darker_gray"
android:text="Dialog Old Api"
android:textColor="@android:color/holo_blue_dark"
/>
<Button
android:id="@+id/dialog_fragment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/darker_gray"
android:text="Dialog Fragment"
android:textColor="@android:color/holo_blue_dark"
/>
</LinearLayout>

View File

@@ -1,26 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".IncrementActivity"
android:orientation="vertical"
>
<TextView
android:id="@+id/count"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="0" />
<Button
android:id="@+id/btn_inc"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/darker_gray"
android:text="Inc"
android:textColor="@android:color/holo_blue_dark"
/>
</LinearLayout>

View File

@@ -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;

View File

@@ -44,8 +44,15 @@ void handleException(const std::exception& e) {
__android_log_write(ANDROID_LOG_ERROR, "FLIPPER", message.c_str());
}
std::unique_ptr<facebook::flipper::Scheduler> sonarScheduler;
std::unique_ptr<facebook::flipper::Scheduler> connectionScheduler;
std::unique_ptr<facebook::flipper::Scheduler>& sonarScheduler() {
static std::unique_ptr<facebook::flipper::Scheduler> scheduler;
return scheduler;
}
std::unique_ptr<facebook::flipper::Scheduler>& connectionScheduler() {
static std::unique_ptr<facebook::flipper::Scheduler> scheduler;
return scheduler;
}
class JEventBase : public jni::HybridClass<JEventBase> {
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<bool> 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<JFlipperSocketImpl> socket_;
bool connecting_;
};
class JFlipperSocketProvider : public facebook::flipper::FlipperSocketProvider {
@@ -767,6 +743,7 @@ class JFlipperClient : public jni::HybridClass<JFlipperClient> {
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<JFlipperClient> {
}
}
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<JFlipperPlugin> plugin) {
try {
auto wrapper =
@@ -949,9 +939,9 @@ class JFlipperClient : public jni::HybridClass<JFlipperClient> {
const std::string app,
const std::string appId,
const std::string privateAppDirectory) {
sonarScheduler =
sonarScheduler() =
std::make_unique<FollyScheduler>(callbackWorker->eventBase());
connectionScheduler =
connectionScheduler() =
std::make_unique<FollyScheduler>(connectionWorker->eventBase());
FlipperClient::init(
@@ -962,8 +952,8 @@ class JFlipperClient : public jni::HybridClass<JFlipperClient> {
std::move(app),
std::move(appId),
std::move(privateAppDirectory)},
sonarScheduler.get(),
connectionScheduler.get(),
sonarScheduler().get(),
connectionScheduler().get(),
insecurePort,
securePort,
altInsecurePort,

View File

@@ -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) {

View File

@@ -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);

View File

@@ -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();
socketFactory = sslContext.getSocketFactory();
} else {
socketFactory = SocketFactory.getDefault();
}
this.setSocketFactory(
new DelegatingSocketFactory(factory) {
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();
}

View File

@@ -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) {

View File

@@ -24,6 +24,8 @@ public interface FlipperClient {
void stop();
boolean isConnected();
void subscribeForUpdates(FlipperStateUpdateListener stateListener);
void unsubscribe();

View File

@@ -162,9 +162,9 @@ public class SqliteDatabaseDriver extends DatabaseDriver<SqliteDatabaseDescripto
SupportSQLiteDatabase database =
sqliteDatabaseConnectionProvider.openDatabase(databaseDescriptor.file);
try {
Cursor structureCursor = database.query("PRAGMA table_info(" + table + ")", null);
Cursor foreignKeysCursor = database.query("PRAGMA foreign_key_list(" + table + ")", null);
Cursor indexesCursor = database.query("PRAGMA index_list(" + table + ")", null);
Cursor structureCursor = database.query("PRAGMA table_info(" + table + ")");
Cursor foreignKeysCursor = database.query("PRAGMA foreign_key_list(" + table + ")");
Cursor indexesCursor = database.query("PRAGMA index_list(" + table + ")");
try {
// Structure & foreign keys
@@ -210,7 +210,7 @@ public class SqliteDatabaseDriver extends DatabaseDriver<SqliteDatabaseDescripto
while (indexesCursor.moveToNext()) {
List<String> 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<SqliteDatabaseDescripto
private static DatabaseExecuteSqlResponse executeSelect(
SupportSQLiteDatabase database, String query) {
Cursor cursor = database.query(query, null);
Cursor cursor = database.query(query);
try {
String[] columnNames = cursor.getColumnNames();
List<List<Object>> rows = cursorToList(cursor);

View File

@@ -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.

View File

@@ -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();

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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 */

View File

@@ -9,10 +9,8 @@ package com.facebook.flipper.plugins.uidebugger.commands
import com.facebook.flipper.core.FlipperConnection
sealed class CommandRegister {
companion object {
object CommandRegister {
fun <T> register(connection: FlipperConnection, cmd: T) where T : Command {
connection.receive(cmd.identifier(), cmd.receiver())
}
}
}

View File

@@ -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<String, MutableList<Bitmap>> = 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)
}
}
}

View File

@@ -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<T>(val mapping: Map<String, T>) {
open class EnumMapping<T>(private val mapping: Map<String, T>) {
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]
return mapping[key]
?: throw UIDebuggerException(
"Could not convert string ${key} to enum value, possible values ${mapping.entries} ")
return value
"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<InspectableValue> {
val set: MutableSet<InspectableValue> = 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 <reified T : Enum<T>> enumerator(): Iterator<T> = enumValues<T>().iterator()
inline fun <reified T : Enum<T>> enumToSet(): Set<String> {
val set = mutableSetOf<String>()
val values = enumerator<T>()
values.forEach { set.add(it.name) }
return set
}
inline fun <reified T : Enum<T>> enumToInspectableSet(): Set<InspectableValue> {
val set = mutableSetOf<InspectableValue>()
val values = enumerator<T>()
values.forEach { set.add(InspectableValue.Text(it.name)) }
return set
}
inline fun <reified T : Enum<T>> enumMapping(): EnumMapping<T> {
val map = mutableMapOf<String, T>()
val values = enumerator<T>()
values.forEach { map[it.name] = it }
return EnumMapping<T>(map)
}

View File

@@ -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<Activity>)
fun onActivityStackChanged(stack: List<Activity>)
fun onActivityDestroyed(activity: Activity, stack: List<Activity>)
}
private val activities: MutableList<WeakReference<Activity>> = mutableListOf()
private val trackedActivities: MutableSet<Int> = 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>(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<WeakReference<Activity>> = 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<Activity>
get() {
val stack: MutableList<Activity> = ArrayList(activities.size)
val activityIterator: MutableIterator<WeakReference<Activity>> = 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) {}
}
}

View File

@@ -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<View>) {}
})
val activeRoots = rootResolver.listActiveRootViews()
activeRoots?.let { roots -> for (root: RootViewResolver.RootView in roots) {} }
}
}

View File

@@ -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<Activity>)
}
interface ActivityDestroyedListener {
fun onActivityDestroyed(activity: Activity)
}
private val rootsResolver: RootViewResolver
private val activities: MutableList<WeakReference<Activity>>
private var activityStackChangedlistener: ActivityStackChangedListener? = null
private var activityDestroyedListener: ActivityDestroyedListener? = null
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
activities.add(WeakReference<Activity>(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<WeakReference<Activity>> = 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<Activity>
get() {
val stack: MutableList<Activity> = ArrayList<Activity>(activities.size)
val activityIterator: MutableIterator<WeakReference<Activity>> = 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<View>
get() {
val roots = rootsResolver.listActiveRootViews()
roots?.let { roots ->
val viewRoots: MutableList<View> = ArrayList<View>(roots.size)
for (root in roots) {
viewRoots.add(root.view)
}
return viewRoots
}
return emptyList()
}
init {
rootsResolver = RootViewResolver()
application.registerActivityLifecycleCallbacks(this)
activities = ArrayList<WeakReference<Activity>>()
}
}

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