Merge branch '0.227' into universalBuild
This commit is contained in:
@@ -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:
|
||||
|
||||
13
.github/workflows/android-sample.yml
vendored
13
.github/workflows/android-sample.yml
vendored
@@ -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
|
||||
|
||||
9
.github/workflows/docs.yml
vendored
9
.github/workflows/docs.yml
vendored
@@ -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"
|
||||
|
||||
7
.github/workflows/iOS-Sample.yml
vendored
7
.github/workflows/iOS-Sample.yml
vendored
@@ -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
|
||||
|
||||
5
.github/workflows/iOS-dependent-pod-lint.yml
vendored
5
.github/workflows/iOS-dependent-pod-lint.yml
vendored
@@ -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
|
||||
|
||||
69
.github/workflows/iOS-pod-lint.yml
vendored
69
.github/workflows/iOS-pod-lint.yml
vendored
@@ -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
|
||||
|
||||
15
.github/workflows/issues.yml
vendored
15
.github/workflows/issues.yml
vendored
@@ -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 }}'
|
||||
|
||||
20
.github/workflows/js.yml
vendored
20
.github/workflows/js.yml
vendored
@@ -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
|
||||
|
||||
11
.github/workflows/nodejs-doctor.yml
vendored
11
.github/workflows/nodejs-doctor.yml
vendored
@@ -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
|
||||
|
||||
11
.github/workflows/nodejs-pkg.yml
vendored
11
.github/workflows/nodejs-pkg.yml
vendored
@@ -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
|
||||
|
||||
20
.github/workflows/nodejs.yml
vendored
20
.github/workflows/nodejs.yml
vendored
@@ -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
|
||||
|
||||
12
.github/workflows/packer.yml
vendored
12
.github/workflows/packer.yml
vendored
@@ -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
|
||||
|
||||
22
.github/workflows/publish-android.yml
vendored
22
.github/workflows/publish-android.yml
vendored
@@ -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:
|
||||
|
||||
7
.github/workflows/publish-npm.yml
vendored
7
.github/workflows/publish-npm.yml
vendored
@@ -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
|
||||
|
||||
9
.github/workflows/publish-pods.yml
vendored
9
.github/workflows/publish-pods.yml
vendored
@@ -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: |
|
||||
|
||||
35
.github/workflows/react-native-example.yml
vendored
35
.github/workflows/react-native-example.yml
vendored
@@ -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
|
||||
|
||||
119
.github/workflows/release.yml
vendored
119
.github/workflows/release.yml
vendored
@@ -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
12
.gitignore
vendored
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
|
||||

|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
-->
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.facebook.flipper">
|
||||
package="com.facebook.flipper.noop">
|
||||
</manifest>
|
||||
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -6,8 +6,10 @@
|
||||
*/
|
||||
|
||||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
android {
|
||||
namespace 'com.facebook.flipper.plugins.jetpackcompose'
|
||||
compileSdkVersion rootProject.compileSdkVersion
|
||||
buildToolsVersion rootProject.buildToolsVersion
|
||||
|
||||
@@ -16,19 +18,18 @@ android {
|
||||
targetSdkVersion rootProject.targetSdkVersion
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
targetCompatibility rootProject.javaTargetVersion
|
||||
sourceCompatibility rootProject.javaTargetVersion
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(':android')
|
||||
implementation deps.fresco
|
||||
implementation deps.frescoFlipper
|
||||
compileOnly deps.jsr305
|
||||
|
||||
api deps.boltsTasks
|
||||
|
||||
// Exclude the actual stetho dep as we only want some of the fresco APIs here
|
||||
implementation(deps.frescoStetho) {
|
||||
exclude group: 'com.facebook.stetho'
|
||||
}
|
||||
implementation 'androidx.compose.ui:ui:1.4.3'
|
||||
implementation 'androidx.compose.ui:ui-tooling:1.4.3'
|
||||
implementation 'org.jetbrains.kotlin:kotlin-reflect:1.9.0'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
apply plugin: 'com.vanniktech.maven.publish'
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"),
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
apply plugin: 'com.android.library'
|
||||
|
||||
android {
|
||||
namespace 'com.facebook.flipper.plugins.leakcanary'
|
||||
compileSdkVersion rootProject.compileSdkVersion
|
||||
buildToolsVersion rootProject.buildToolsVersion
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)))
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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"))))))
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
apply plugin: 'com.android.library'
|
||||
|
||||
android {
|
||||
namespace 'com.facebook.flipper.plugins.network'
|
||||
compileSdkVersion rootProject.compileSdkVersion
|
||||
buildToolsVersion rootProject.buildToolsVersion
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
txtBlink.setVisibility(View.VISIBLE);
|
||||
txtBlink.startAnimation(animBlink);
|
||||
}
|
||||
v -> {
|
||||
txtBlink.setVisibility(View.VISIBLE);
|
||||
txtBlink.startAnimation(animBlink);
|
||||
});
|
||||
|
||||
animRotate = AnimationUtils.loadAnimation(getApplicationContext(), R.anim.rotate);
|
||||
|
||||
// Rotate
|
||||
btnRotate.setOnClickListener(
|
||||
new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
txtRotate.startAnimation(animRotate);
|
||||
}
|
||||
});
|
||||
btnRotate.setOnClickListener(v -> txtRotate.startAnimation(animRotate));
|
||||
animMove = AnimationUtils.loadAnimation(getApplicationContext(), R.anim.move);
|
||||
// Move
|
||||
btnMove.setOnClickListener(
|
||||
new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
txtMove.startAnimation(animMove);
|
||||
}
|
||||
});
|
||||
|
||||
btnMove.setOnClickListener(v -> txtMove.startAnimation(animMove));
|
||||
|
||||
animBounce = AnimationUtils.loadAnimation(getApplicationContext(), R.anim.bounce);
|
||||
// Slide Down
|
||||
btnBounce.setOnClickListener(
|
||||
new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
txtBounce.startAnimation(animBounce);
|
||||
}
|
||||
});
|
||||
animSequential = AnimationUtils.loadAnimation(getApplicationContext(), R.anim.sequential);
|
||||
// Sequential
|
||||
btnSequential.setOnClickListener(
|
||||
new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
|
||||
txtSeq.startAnimation(animSequential);
|
||||
}
|
||||
});
|
||||
btnBounce.setOnClickListener(v -> txtBounce.startAnimation(animBounce));
|
||||
animSequential = AnimationUtils.loadAnimation(getApplicationContext(), R.anim.sequential);
|
||||
|
||||
btnSequential.setOnClickListener(v -> txtSeq.startAnimation(animSequential));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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") }
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
@@ -29,7 +29,9 @@ public class MainActivity extends AppCompatActivity {
|
||||
final FlipperClient client = AndroidFlipperClient.getInstanceIfInitialized();
|
||||
if (client != null) {
|
||||
final ExampleFlipperPlugin samplePlugin = client.getPluginByClass(ExampleFlipperPlugin.class);
|
||||
samplePlugin.setActivity(this);
|
||||
if (samplePlugin != null) {
|
||||
samplePlugin.setActivity(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
package com.facebook.flipper.sample;
|
||||
|
||||
import android.content.Intent;
|
||||
import com.facebook.drawee.backends.pipeline.Fresco;
|
||||
import com.facebook.drawee.interfaces.DraweeController;
|
||||
import android.net.Uri;
|
||||
import com.facebook.flipper.android.diagnostics.FlipperDiagnosticActivity;
|
||||
import com.facebook.flipper.sample.network.NetworkClient;
|
||||
import com.facebook.fresco.vito.litho.FrescoVitoImage2;
|
||||
import com.facebook.litho.ClickEvent;
|
||||
import com.facebook.litho.Column;
|
||||
import com.facebook.litho.Component;
|
||||
@@ -22,7 +22,6 @@ import com.facebook.litho.annotations.OnCreateLayout;
|
||||
import com.facebook.litho.annotations.OnEvent;
|
||||
import com.facebook.litho.annotations.OnUpdateState;
|
||||
import com.facebook.litho.annotations.State;
|
||||
import com.facebook.litho.fresco.FrescoImage;
|
||||
import com.facebook.litho.widget.Text;
|
||||
import com.facebook.litho.widget.VerticalScroll;
|
||||
import com.facebook.yoga.YogaEdge;
|
||||
@@ -32,9 +31,6 @@ public class RootComponentSpec {
|
||||
|
||||
@OnCreateLayout
|
||||
static Component onCreateLayout(final ComponentContext c, @State boolean displayImage) {
|
||||
final DraweeController controller =
|
||||
Fresco.newDraweeControllerBuilder().setUri("https://fbflipper.com/img/icon.png").build();
|
||||
|
||||
Column col =
|
||||
Column.create(c)
|
||||
.child(
|
||||
@@ -109,24 +105,33 @@ public class RootComponentSpec {
|
||||
.clickHandler(RootComponent.openAnimationsActivity(c)))
|
||||
.child(
|
||||
Text.create(c)
|
||||
.text("Navigate to increment activity")
|
||||
.text("Navigate to buttons activity")
|
||||
.key("11")
|
||||
.marginDip(YogaEdge.ALL, 10)
|
||||
.textSizeSp(20)
|
||||
.clickHandler(RootComponent.openIncrementActivity(c)))
|
||||
.child(
|
||||
Text.create(c)
|
||||
.text("Crash this app")
|
||||
.text("Navigate to Jetpack Compose activity")
|
||||
.key("12")
|
||||
.marginDip(YogaEdge.ALL, 10)
|
||||
.textSizeSp(20)
|
||||
.clickHandler(RootComponent.openJetpackComposeActivity(c)))
|
||||
.child(
|
||||
Text.create(c)
|
||||
.text("Crash this app")
|
||||
.key("13")
|
||||
.marginDip(YogaEdge.ALL, 10)
|
||||
.textSizeSp(20)
|
||||
.clickHandler(RootComponent.triggerCrash(c)))
|
||||
.child(
|
||||
FrescoImage.create(c)
|
||||
.controller(controller)
|
||||
.marginDip(YogaEdge.ALL, 10)
|
||||
.widthDip(150)
|
||||
.heightDip(150))
|
||||
displayImage
|
||||
? FrescoVitoImage2.create(c)
|
||||
.uri(Uri.parse("https://fbflipper.com/img/icon.png"))
|
||||
.marginDip(YogaEdge.ALL, 10)
|
||||
.widthDip(150)
|
||||
.heightDip(150)
|
||||
: null)
|
||||
.build();
|
||||
|
||||
return VerticalScroll.create(c).childComponent(col).build();
|
||||
@@ -200,7 +205,13 @@ public class RootComponentSpec {
|
||||
|
||||
@OnEvent(ClickEvent.class)
|
||||
static void openIncrementActivity(final ComponentContext c) {
|
||||
final Intent intent = new Intent(c.getAndroidContext(), IncrementActivity.class);
|
||||
final Intent intent = new Intent(c.getAndroidContext(), ButtonsActivity.class);
|
||||
c.getAndroidContext().startActivity(intent);
|
||||
}
|
||||
|
||||
@OnEvent(ClickEvent.class)
|
||||
static void openJetpackComposeActivity(final ComponentContext c) {
|
||||
final Intent intent = new Intent(c.getAndroidContext(), JetpackComposeActivity.class);
|
||||
c.getAndroidContext().startActivity(intent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
|
||||
|
||||
42
android/sample/src/main/res/layout/activity_buttons.xml
Normal file
42
android/sample/src/main/res/layout/activity_buttons.xml
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -37,10 +37,10 @@ import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import javax.net.SocketFactory;
|
||||
import javax.net.ssl.KeyManagerFactory;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLParameters;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
import org.java_websocket.client.WebSocketClient;
|
||||
@@ -67,6 +67,22 @@ class FlipperSocketImpl extends WebSocketClient implements FlipperSocket {
|
||||
this.mEventHandler = eventHandler;
|
||||
}
|
||||
|
||||
private void clearEventHandler() {
|
||||
this.mEventHandler =
|
||||
new FlipperSocketEventHandler() {
|
||||
@Override
|
||||
public void onConnectionEvent(SocketEvent event) {}
|
||||
|
||||
@Override
|
||||
public void onMessageReceived(String message) {}
|
||||
|
||||
@Override
|
||||
public FlipperObject onAuthenticationChallengeReceived() {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flipperConnect() {
|
||||
if ((this.isOpen())) {
|
||||
@@ -79,6 +95,7 @@ class FlipperSocketImpl extends WebSocketClient implements FlipperSocket {
|
||||
* certificate exchange.
|
||||
*/
|
||||
FlipperObject authenticationObject = this.mEventHandler.onAuthenticationChallengeReceived();
|
||||
SocketFactory socketFactory;
|
||||
if (authenticationObject.contains("certificates_client_path")
|
||||
&& authenticationObject.contains("certificates_client_pass")) {
|
||||
|
||||
@@ -100,21 +117,23 @@ class FlipperSocketImpl extends WebSocketClient implements FlipperSocket {
|
||||
sslContext.init(
|
||||
kmf.getKeyManagers(), new TrustManager[] {new FlipperTrustManager(cert_ca_path)}, null);
|
||||
|
||||
SSLSocketFactory factory = sslContext.getSocketFactory();
|
||||
|
||||
this.setSocketFactory(
|
||||
new DelegatingSocketFactory(factory) {
|
||||
@Override
|
||||
protected Socket configureSocket(Socket socket) {
|
||||
TrafficStats.setThreadStatsTag(SOCKET_TAG);
|
||||
return socket;
|
||||
}
|
||||
});
|
||||
socketFactory = sslContext.getSocketFactory();
|
||||
} else {
|
||||
socketFactory = SocketFactory.getDefault();
|
||||
}
|
||||
|
||||
this.setSocketFactory(
|
||||
new DelegatingSocketFactory(socketFactory) {
|
||||
@Override
|
||||
protected Socket configureSocket(Socket socket) {
|
||||
TrafficStats.setThreadStatsTag(SOCKET_TAG);
|
||||
return socket;
|
||||
}
|
||||
});
|
||||
|
||||
this.connect();
|
||||
} catch (Exception e) {
|
||||
Log.e("Flipper", "Failed to initialize the socket before connect. " + e.getMessage());
|
||||
Log.e("flipper", "Failed to initialize the socket before connect. Error: " + e.getMessage());
|
||||
this.mEventHandler.onConnectionEvent(FlipperSocketEventHandler.SocketEvent.ERROR);
|
||||
}
|
||||
}
|
||||
@@ -137,6 +156,9 @@ class FlipperSocketImpl extends WebSocketClient implements FlipperSocket {
|
||||
@Override
|
||||
public void onClose(int code, String reason, boolean remote) {
|
||||
this.mEventHandler.onConnectionEvent(FlipperSocketEventHandler.SocketEvent.CLOSE);
|
||||
// Clear the existing event handler as to ensure no other events are processed after the close
|
||||
// is handled.
|
||||
this.clearEventHandler();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -148,6 +170,7 @@ class FlipperSocketImpl extends WebSocketClient implements FlipperSocket {
|
||||
*/
|
||||
@Override
|
||||
public void onError(Exception ex) {
|
||||
|
||||
// Check the exception for OpenSSL error and change the event type.
|
||||
// Required for Flipper as the current implementation treats these errors differently.
|
||||
if (ex instanceof javax.net.ssl.SSLHandshakeException) {
|
||||
@@ -155,24 +178,14 @@ class FlipperSocketImpl extends WebSocketClient implements FlipperSocket {
|
||||
} else {
|
||||
this.mEventHandler.onConnectionEvent(FlipperSocketEventHandler.SocketEvent.ERROR);
|
||||
}
|
||||
// Clear the existing event handler as to ensure no other events are processed after the close
|
||||
// is handled.
|
||||
this.clearEventHandler();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flipperDisconnect() {
|
||||
this.mEventHandler.onConnectionEvent(FlipperSocketEventHandler.SocketEvent.CLOSE);
|
||||
this.mEventHandler =
|
||||
new FlipperSocketEventHandler() {
|
||||
@Override
|
||||
public void onConnectionEvent(SocketEvent event) {}
|
||||
|
||||
@Override
|
||||
public void onMessageReceived(String message) {}
|
||||
|
||||
@Override
|
||||
public FlipperObject onAuthenticationChallengeReceived() {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
this.clearEventHandler();
|
||||
super.close();
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -24,6 +24,8 @@ public interface FlipperClient {
|
||||
|
||||
void stop();
|
||||
|
||||
boolean isConnected();
|
||||
|
||||
void subscribeForUpdates(FlipperStateUpdateListener stateListener);
|
||||
|
||||
void unsubscribe();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -9,10 +9,8 @@ package com.facebook.flipper.plugins.uidebugger.commands
|
||||
|
||||
import com.facebook.flipper.core.FlipperConnection
|
||||
|
||||
sealed class CommandRegister {
|
||||
companion object {
|
||||
fun <T> register(connection: FlipperConnection, cmd: T) where T : Command {
|
||||
connection.receive(cmd.identifier(), cmd.receiver())
|
||||
}
|
||||
object CommandRegister {
|
||||
fun <T> register(connection: FlipperConnection, cmd: T) where T : Command {
|
||||
connection.receive(cmd.identifier(), cmd.receiver())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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]
|
||||
?: throw UIDebuggerException(
|
||||
"Could not convert string ${key} to enum value, possible values ${mapping.entries} ")
|
||||
return value
|
||||
return mapping[key]
|
||||
?: throw UIDebuggerException(
|
||||
"Could not convert string $key to enum value, possible values ${mapping.entries} ")
|
||||
}
|
||||
|
||||
fun toInspectable(value: T, mutable: Boolean): InspectableValue.Enum {
|
||||
return InspectableValue.Enum(EnumData(mapping.keys, getStringRepresentation(value)), mutable)
|
||||
fun getInspectableValues(): Set<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)
|
||||
}
|
||||
|
||||
@@ -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) {}
|
||||
}
|
||||
}
|
||||
@@ -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) {} }
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user