From 98427546d632c750223f469b058a38f9ca5c4b76 Mon Sep 17 00:00:00 2001 From: Kadi Kraman Date: Thu, 20 Aug 2026 16:53:43 +0100 Subject: [PATCH 1/4] [docs] Update observe pricing note (#49167) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Why Update docs based on https://expo.dev/changelog/eas-observe-is-now-generally-available # How Remove the now out of date note about Observe being free while in open beta. # Test Plan 👀 # Checklist - [ ] I added a `changelog.md` entry and rebuilt the package sources according to [this short guide](https://github.com/expo/expo/blob/main/CONTRIBUTING.md#-before-submitting) - [ ] This diff will work correctly for `npx expo prebuild` & EAS Build (eg: updated a module plugin). - [ ] Conforms with the [Documentation Writing Style Guide](https://github.com/expo/expo/blob/main/guides/Expo%20Documentation%20Writing%20Style%20Guide.md) --- docs/pages/eas/observe/introduction.mdx | 2 +- docs/pages/versions/unversioned/sdk/observe.mdx | 3 +-- docs/pages/versions/v57.0.0/sdk/observe.mdx | 3 +-- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/docs/pages/eas/observe/introduction.mdx b/docs/pages/eas/observe/introduction.mdx index bdc9e551e5c84a..d557a2b0a2d59c 100644 --- a/docs/pages/eas/observe/introduction.mdx +++ b/docs/pages/eas/observe/introduction.mdx @@ -17,7 +17,7 @@ import { RelatedSkills } from '~/ui/components/RelatedSkills'; import { Terminal } from '~/ui/components/Snippet'; import { VideoBoxLink } from '~/ui/components/VideoBoxLink'; -> **important** **EAS Observe** is in [Open Beta](/more/release-statuses/#beta). The first 10,000 monthly active users are free. For higher usage, contact [sales@expo.dev](mailto:sales@expo.dev). +> **info** EAS Observe includes 10,000 monthly active users on the Free plan and 50,000 on paid plans. Beyond that, pricing is usage-based. See [Pricing](https://expo.dev/pricing) for details. **EAS Observe** is a performance monitoring service from Expo for tracking how your app performs in production. It gives you visibility into real-world startup times, rendering performance, and user experience across different devices, networks, and conditions. diff --git a/docs/pages/versions/unversioned/sdk/observe.mdx b/docs/pages/versions/unversioned/sdk/observe.mdx index 543d30338f11ae..112a8ceda64033 100644 --- a/docs/pages/versions/unversioned/sdk/observe.mdx +++ b/docs/pages/versions/unversioned/sdk/observe.mdx @@ -4,13 +4,12 @@ description: A library that collects app performance metrics and user-defined ev sourceCodeUrl: 'https://github.com/expo/expo/tree/main/packages/expo-observe' packageName: 'expo-observe' platforms: ['android', 'ios', 'tvos'] -isBeta: true --- import APISection from '~/components/plugins/APISection'; import { APIInstallSection } from '~/components/plugins/InstallSection'; -> **important** **EAS Observe** is in [Open Beta](/more/release-statuses/#beta). The first 10,000 monthly active users are free. For higher usage, contact [sales@expo.dev](mailto:sales@expo.dev). +> **info** EAS Observe includes 10,000 monthly active users on the Free plan and 50,000 on paid plans. Beyond that, pricing is usage-based. See [Pricing](https://expo.dev/pricing) for details. `expo-observe` is a library that collects performance metrics and user-defined events from your app and dispatches them to [EAS Observe](/eas/observe/introduction/), a performance monitoring service from Expo, or to your preferred OpenTelemetry (OTEL)-compliant backend. It measures real-world startup performance, such as [Time to First Render (TTR)](/eas/observe/reference/metrics/#time-to-first-render-ttr) and [Time to Interactive (TTI)](/eas/observe/reference/metrics/#time-to-interactive-tti), from apps running in production. diff --git a/docs/pages/versions/v57.0.0/sdk/observe.mdx b/docs/pages/versions/v57.0.0/sdk/observe.mdx index def61758e6afd5..124908f13ddcfa 100644 --- a/docs/pages/versions/v57.0.0/sdk/observe.mdx +++ b/docs/pages/versions/v57.0.0/sdk/observe.mdx @@ -4,13 +4,12 @@ description: A library that collects app performance metrics and user-defined ev sourceCodeUrl: 'https://github.com/expo/expo/tree/sdk-57/packages/expo-observe' packageName: 'expo-observe' platforms: ['android', 'ios', 'tvos'] -isBeta: true --- import APISection from '~/components/plugins/APISection'; import { APIInstallSection } from '~/components/plugins/InstallSection'; -> **important** **EAS Observe** is in [Open Beta](/more/release-statuses/#beta). The first 10,000 monthly active users are free. For higher usage, contact [sales@expo.dev](mailto:sales@expo.dev). +> **info** EAS Observe includes 10,000 monthly active users on the Free plan and 50,000 on paid plans. Beyond that, pricing is usage-based. See [Pricing](https://expo.dev/pricing) for details. `expo-observe` is a library that collects performance metrics and user-defined events from your app and dispatches them to [EAS Observe](/eas/observe/introduction/), a performance monitoring service from Expo, or to your preferred OpenTelemetry (OTEL)-compliant backend. It measures real-world startup performance, such as [Time to First Render (TTR)](/eas/observe/reference/metrics/#time-to-first-render-ttr) and [Time to Interactive (TTI)](/eas/observe/reference/metrics/#time-to-interactive-tti), from apps running in production. From 33aa07d98d8b1e80e8d78e4093f2ce89798c195a Mon Sep 17 00:00:00 2001 From: Brent Vatne Date: Thu, 20 Aug 2026 09:02:52 -0700 Subject: [PATCH 2/4] [widgets][ios] Add @available(iOS 16.1, *) to getLiveActivityEnvironment so XCFramework precompilation succeeds (#49076) --- packages/expo-widgets/CHANGELOG.md | 1 + packages/expo-widgets/ios/Widgets/Utils.swift | 12 ++++++------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/expo-widgets/CHANGELOG.md b/packages/expo-widgets/CHANGELOG.md index aede8a766bf9dd..95625da0b0f8cc 100644 --- a/packages/expo-widgets/CHANGELOG.md +++ b/packages/expo-widgets/CHANGELOG.md @@ -24,6 +24,7 @@ ### 🐛 Bug fixes +- [iOS] Fix XCFramework precompilation failing on the unguarded `ActivityViewContext` parameter of `getLiveActivityEnvironment`, which requires iOS 16.1. ([#49076](https://github.com/expo/expo/pull/49076) by [@brentvatne](https://github.com/brentvatne)) - [iOS] Fix `LiveActivityFactory.getInstances()` returning live activities that belong to other factories or that have already ended. ([#48489](https://github.com/expo/expo/pull/48489) by [@huextrat](https://github.com/huextrat)) - [iOS] Deliver the Live Activity `start()` URL to the Lock Screen banner, and scope it to the activity that set it instead of storing it globally per factory name. ([#48489](https://github.com/expo/expo/pull/48489) by [@huextrat](https://github.com/huextrat)) - [Android] Add 16KB page size support. ([#47135](https://github.com/expo/expo/pull/47135) by [@jakex7](https://github.com/jakex7)) diff --git a/packages/expo-widgets/ios/Widgets/Utils.swift b/packages/expo-widgets/ios/Widgets/Utils.swift index 501e20e5a361ff..89d11099d5b722 100644 --- a/packages/expo-widgets/ios/Widgets/Utils.swift +++ b/packages/expo-widgets/ios/Widgets/Utils.swift @@ -119,20 +119,20 @@ public func getWidgetEnvironment(environment: EnvironmentValues) -> [String: Any return env } +// ActivityViewContext requires iOS 16.1. Without the availability attribute, precompiling +// this package fails: the SwiftPM prebuild targets iOS 16.0 (spm.config.json), unlike the +// podspec (16.4). Both callers live in @available(iOS 16.1, *) views. +@available(iOS 16.1, *) func getLiveActivityEnvironment(for environment: EnvironmentValues, in context: ActivityViewContext) -> [String: Any] { var env: [String: Any] = [ "colorScheme": "\(environment.colorScheme)" ] - if #available(iOS 16.0, *) { - env["isLuminanceReduced"] = environment.isLuminanceReduced - } + env["isLuminanceReduced"] = environment.isLuminanceReduced + env["isActivityFullscreen"] = environment.isActivityFullscreen if #available(iOS 16.2, *) { env["isStale"] = context.isStale } - if #available(iOS 16.1, *) { - env["isActivityFullscreen"] = environment.isActivityFullscreen - } if #available(iOS 18.0, *) { env["isActivityUpdateReduced"] = environment.isActivityUpdateReduced env["activityFamily"] = "\(environment.activityFamily)" From 8c86c70fffe97d3ae19b0d2c3fe3cae8888249bc Mon Sep 17 00:00:00 2001 From: Gabriel Donadel Dall'Agnol Date: Thu, 20 Aug 2026 13:05:37 -0300 Subject: [PATCH 3/4] [expo] Update react-native-keyboard-controller to 1.22.4 (#49127) # Why In order to support react native 0.87 we should bump react-native-keyboard-controller to latest # How Update react-native-keyboard-controller to 1.22.4 # Test Plan run BareExpo locally # Checklist - [ ] I added a `changelog.md` entry and rebuilt the package sources according to [this short guide](https://github.com/expo/expo/blob/main/CONTRIBUTING.md#-before-submitting) - [ ] This diff will work correctly for `npx expo prebuild` & EAS Build (eg: updated a module plugin). - [ ] Conforms with the [Documentation Writing Style Guide](https://github.com/expo/expo/blob/main/guides/Expo%20Documentation%20Writing%20Style%20Guide.md) --- apps/bare-expo/ios/Podfile.lock | 12 ++++++------ apps/bare-expo/package.json | 2 +- apps/expo-go/ios/Podfile.lock | 12 ++++++------ apps/expo-go/package.json | 2 +- apps/native-component-list/package.json | 2 +- packages/expo/bundledNativeModules.json | 2 +- pnpm-lock.yaml | 18 +++++++++--------- 7 files changed, 25 insertions(+), 25 deletions(-) diff --git a/apps/bare-expo/ios/Podfile.lock b/apps/bare-expo/ios/Podfile.lock index dd5b21ac9ab29e..df0eedd85dcffd 100644 --- a/apps/bare-expo/ios/Podfile.lock +++ b/apps/bare-expo/ios/Podfile.lock @@ -2231,7 +2231,7 @@ PODS: - ReactCommon/turbomodule/core - ReactNativeDependencies - Yoga - - react-native-keyboard-controller (1.21.9): + - react-native-keyboard-controller (1.22.4): - hermes-engine - RCTRequired - RCTTypeSafety @@ -2243,7 +2243,7 @@ PODS: - React-graphics - React-ImageManager - React-jsi - - react-native-keyboard-controller/common (= 1.21.9) + - react-native-keyboard-controller/common (= 1.22.4) - React-NativeModulesApple - React-RCTFabric - React-renderercss @@ -2254,7 +2254,7 @@ PODS: - ReactCommon/turbomodule/core - ReactNativeDependencies - Yoga - - react-native-keyboard-controller/common (1.21.9): + - react-native-keyboard-controller/common (1.22.4): - hermes-engine - RCTRequired - RCTTypeSafety @@ -3487,7 +3487,7 @@ DEPENDENCIES: - "React-Mapbuffer (from `../../../node_modules/.pnpm/react-native@0.86.0_@babel+core@7.29.7_@react-native+jest-preset@0.86.0_@babel+core@7.2_05fe3c1b42d0a26b4dba0087fc75e51f/node_modules/react-native/ReactCommon`)" - "React-microtasksnativemodule (from `../../../node_modules/.pnpm/react-native@0.86.0_@babel+core@7.29.7_@react-native+jest-preset@0.86.0_@babel+core@7.2_05fe3c1b42d0a26b4dba0087fc75e51f/node_modules/react-native/ReactCommon/react/nativemodule/microtasks`)" - "React-mutationobservernativemodule (from `../../../node_modules/.pnpm/react-native@0.86.0_@babel+core@7.29.7_@react-native+jest-preset@0.86.0_@babel+core@7.2_05fe3c1b42d0a26b4dba0087fc75e51f/node_modules/react-native/ReactCommon/react/nativemodule/mutationobserver`)" - - "react-native-keyboard-controller (from `../../../node_modules/.pnpm/react-native-keyboard-controller@1.21.9_react-native-reanimated@4.5.1_patch_hash=f1adeb_9a349fcb8493a723edbedfbafcf73f19/node_modules/react-native-keyboard-controller`)" + - "react-native-keyboard-controller (from `../../../node_modules/.pnpm/react-native-keyboard-controller@1.22.4_react-native-reanimated@4.5.1_patch_hash=f1adeb_5fc0483b2b4f942879a1a7d7c7eb1c5a/node_modules/react-native-keyboard-controller`)" - "react-native-netinfo (from `../../../node_modules/.pnpm/@react-native-community+netinfo@12.0.1_patch_hash=ced0cb79848978ecc3e780a4d812d94868434_66433ebdfb3ea787f7fc42b0ed8ee206/node_modules/@react-native-community/netinfo`)" - "react-native-pager-view (from `../../../node_modules/.pnpm/react-native-pager-view@8.0.2_react-native@0.86.0_@babel+core@7.29.7_@react-native+jest_050fee3d9ae7682571a8180ab6e58f1e/node_modules/react-native-pager-view`)" - "react-native-safe-area-context (from `../../../node_modules/.pnpm/react-native-safe-area-context@5.7.0_react-native@0.86.0_@babel+core@7.29.7_@react-nati_c28ea83fe184def3347ae322f545fd20/node_modules/react-native-safe-area-context`)" @@ -3904,7 +3904,7 @@ EXTERNAL SOURCES: React-mutationobservernativemodule: :path: "../../../node_modules/.pnpm/react-native@0.86.0_@babel+core@7.29.7_@react-native+jest-preset@0.86.0_@babel+core@7.2_05fe3c1b42d0a26b4dba0087fc75e51f/node_modules/react-native/ReactCommon/react/nativemodule/mutationobserver" react-native-keyboard-controller: - :path: "../../../node_modules/.pnpm/react-native-keyboard-controller@1.21.9_react-native-reanimated@4.5.1_patch_hash=f1adeb_9a349fcb8493a723edbedfbafcf73f19/node_modules/react-native-keyboard-controller" + :path: "../../../node_modules/.pnpm/react-native-keyboard-controller@1.22.4_react-native-reanimated@4.5.1_patch_hash=f1adeb_5fc0483b2b4f942879a1a7d7c7eb1c5a/node_modules/react-native-keyboard-controller" react-native-netinfo: :path: "../../../node_modules/.pnpm/@react-native-community+netinfo@12.0.1_patch_hash=ced0cb79848978ecc3e780a4d812d94868434_66433ebdfb3ea787f7fc42b0ed8ee206/node_modules/@react-native-community/netinfo" react-native-pager-view: @@ -4156,7 +4156,7 @@ SPEC CHECKSUMS: React-Mapbuffer: ce449ccdf3b80384415b925606be8a4bdcfc65d3 React-microtasksnativemodule: 2eb3f49d0d8e77b5343455eccd057010b8d38b6b React-mutationobservernativemodule: f0a0d5ae9b51caf7becbeabf836d716cfedb6bf2 - react-native-keyboard-controller: d4796248609d4c03cd6b5f73a9fd083d7c5f33d6 + react-native-keyboard-controller: d9d240e9c2cfddc41c6de0f70b881cc82eeaadd6 react-native-netinfo: 2f906d5d9a639090ed1c079135d6485a95f749f1 react-native-pager-view: c9f0c94cc64041ac70f28f79527ea2ac664cf25c react-native-safe-area-context: 6b4966397ada0f7dd481e4486a2ef936834861a1 diff --git a/apps/bare-expo/package.json b/apps/bare-expo/package.json index 939310d494761b..60297b3160c1b4 100644 --- a/apps/bare-expo/package.json +++ b/apps/bare-expo/package.json @@ -62,7 +62,7 @@ "react-dom": "19.2.3", "react-native": "0.86.0", "react-native-gesture-handler": "~3.1.0", - "react-native-keyboard-controller": "^1.21.9", + "react-native-keyboard-controller": "^1.22.4", "react-native-pager-view": "8.0.2", "react-native-reanimated": "4.5.1", "react-native-safe-area-context": "5.7.0", diff --git a/apps/expo-go/ios/Podfile.lock b/apps/expo-go/ios/Podfile.lock index 06073a60e914c7..1e5face53b384e 100644 --- a/apps/expo-go/ios/Podfile.lock +++ b/apps/expo-go/ios/Podfile.lock @@ -2379,7 +2379,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - react-native-keyboard-controller (1.21.9): + - react-native-keyboard-controller (1.22.4): - boost - DoubleConversion - fast_float @@ -2397,7 +2397,7 @@ PODS: - React-graphics - React-ImageManager - React-jsi - - react-native-keyboard-controller/common (= 1.21.9) + - react-native-keyboard-controller/common (= 1.22.4) - React-NativeModulesApple - React-RCTFabric - React-renderercss @@ -2408,7 +2408,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - react-native-keyboard-controller/common (1.21.9): + - react-native-keyboard-controller/common (1.22.4): - boost - DoubleConversion - fast_float @@ -4106,7 +4106,7 @@ DEPENDENCIES: - React-Mapbuffer (from `../../../react-native-lab/react-native/packages/react-native/ReactCommon`) - React-microtasksnativemodule (from `../../../react-native-lab/react-native/packages/react-native/ReactCommon/react/nativemodule/microtasks`) - React-mutationobservernativemodule (from `../../../react-native-lab/react-native/packages/react-native/ReactCommon/react/nativemodule/mutationobserver`) - - "react-native-keyboard-controller (from `../../../node_modules/.pnpm/react-native-keyboard-controller@1.21.9_react-native-reanimated@4.5.1_patch_hash=f1adeb_9a349fcb8493a723edbedfbafcf73f19/node_modules/react-native-keyboard-controller`)" + - "react-native-keyboard-controller (from `../../../node_modules/.pnpm/react-native-keyboard-controller@1.22.4_react-native-reanimated@4.5.1_patch_hash=f1adeb_5fc0483b2b4f942879a1a7d7c7eb1c5a/node_modules/react-native-keyboard-controller`)" - "react-native-maps/Maps (from `../../../node_modules/.pnpm/react-native-maps@1.27.2_react-native-web@0.21.2_encoding@0.1.13_react-dom@19.2.3_react_07670b4144253b002775c6adf1c85a39/node_modules/react-native-maps`)" - "react-native-netinfo (from `../../../node_modules/.pnpm/@react-native-community+netinfo@12.0.1_patch_hash=ced0cb79848978ecc3e780a4d812d94868434_66433ebdfb3ea787f7fc42b0ed8ee206/node_modules/@react-native-community/netinfo`)" - "react-native-pager-view (from `../../../node_modules/.pnpm/react-native-pager-view@8.0.2_react-native@0.86.0_@babel+core@7.29.7_@react-native+jest_050fee3d9ae7682571a8180ab6e58f1e/node_modules/react-native-pager-view`)" @@ -4435,7 +4435,7 @@ EXTERNAL SOURCES: React-mutationobservernativemodule: :path: "../../../react-native-lab/react-native/packages/react-native/ReactCommon/react/nativemodule/mutationobserver" react-native-keyboard-controller: - :path: "../../../node_modules/.pnpm/react-native-keyboard-controller@1.21.9_react-native-reanimated@4.5.1_patch_hash=f1adeb_9a349fcb8493a723edbedfbafcf73f19/node_modules/react-native-keyboard-controller" + :path: "../../../node_modules/.pnpm/react-native-keyboard-controller@1.22.4_react-native-reanimated@4.5.1_patch_hash=f1adeb_5fc0483b2b4f942879a1a7d7c7eb1c5a/node_modules/react-native-keyboard-controller" react-native-maps: :path: "../../../node_modules/.pnpm/react-native-maps@1.27.2_react-native-web@0.21.2_encoding@0.1.13_react-dom@19.2.3_react_07670b4144253b002775c6adf1c85a39/node_modules/react-native-maps" react-native-netinfo: @@ -4683,7 +4683,7 @@ SPEC CHECKSUMS: React-Mapbuffer: 187c03c70cba972bba8786b581897d8e1ae6bdc3 React-microtasksnativemodule: e130d410da41cad9b9776c492e6f8c07ad7b34df React-mutationobservernativemodule: 7f5f4da31e906da2100a432672c55cb2096570e1 - react-native-keyboard-controller: 93e24ebbe21551622cbb0e9c39664dc5539f943c + react-native-keyboard-controller: 1d5ba749e13f7392286934769bfad3ce98e4f519 react-native-maps: b5af575a371e57847c6d8490e726c08036f7c80f react-native-netinfo: a0be8c77f8420a60ddf07eb87be1a36f02a1bd65 react-native-pager-view: 2694b7efa49588b1cc3aecb5f513871135f499cc diff --git a/apps/expo-go/package.json b/apps/expo-go/package.json index de6e12813776ec..16cacd939a4411 100644 --- a/apps/expo-go/package.json +++ b/apps/expo-go/package.json @@ -84,7 +84,7 @@ "react": "19.2.3", "react-native": "0.86.0", "react-native-gesture-handler": "~3.1.0", - "react-native-keyboard-controller": "^1.21.9", + "react-native-keyboard-controller": "^1.22.4", "react-native-maps": "1.27.2", "react-native-pager-view": "8.0.2", "react-native-reanimated": "4.5.1", diff --git a/apps/native-component-list/package.json b/apps/native-component-list/package.json index 5c093e861bbd18..d46df30d64f3ac 100644 --- a/apps/native-component-list/package.json +++ b/apps/native-component-list/package.json @@ -129,7 +129,7 @@ "react-native": "0.86.0", "react-native-dropdown-picker": "^5.3.0", "react-native-gesture-handler": "~3.1.0", - "react-native-keyboard-controller": "^1.21.9", + "react-native-keyboard-controller": "^1.22.4", "react-native-maps": "1.27.2", "react-native-pager-view": "8.0.2", "react-native-reanimated": "4.5.1", diff --git a/packages/expo/bundledNativeModules.json b/packages/expo/bundledNativeModules.json index 9eefb92e000422..877cb82e896a56 100644 --- a/packages/expo/bundledNativeModules.json +++ b/packages/expo/bundledNativeModules.json @@ -103,7 +103,7 @@ "react-native": "0.86.2", "react-native-web": "~0.21.0", "react-native-gesture-handler": "~3.1.0", - "react-native-keyboard-controller": "1.21.9", + "react-native-keyboard-controller": "1.22.4", "react-native-maps": "1.27.2", "react-native-pager-view": "8.0.2", "react-native-worklets": "0.10.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 77d18a0c4d53ca..1bd236d3b98023 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -223,8 +223,8 @@ importers: specifier: ~3.1.0 version: 3.1.0(react-native@0.86.0(@babel/core@7.29.7)(@react-native/jest-preset@0.86.0(@babel/core@7.29.7)(react@19.2.3))(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3) react-native-keyboard-controller: - specifier: ^1.21.9 - version: 1.21.9(react-native-reanimated@4.5.1(patch_hash=f1adeb1c26dfb66311eb34b353a16b2ae27f5a95a65e9d52e683a60af0e8b18e)(react-native-worklets@0.10.1(patch_hash=b72ac0fc85273472ec4b8974adb8d4eba75672291aa7501270a7b08e58ba7dec)(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(react-native@0.86.0(@babel/core@7.29.7)(@react-native/jest-preset@0.86.0(@babel/core@7.29.7)(react@19.2.3))(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.86.0(@babel/core@7.29.7)(@react-native/jest-preset@0.86.0(@babel/core@7.29.7)(react@19.2.3))(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.86.0(@babel/core@7.29.7)(@react-native/jest-preset@0.86.0(@babel/core@7.29.7)(react@19.2.3))(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3) + specifier: ^1.22.4 + version: 1.22.4(react-native-reanimated@4.5.1(patch_hash=f1adeb1c26dfb66311eb34b353a16b2ae27f5a95a65e9d52e683a60af0e8b18e)(react-native-worklets@0.10.1(patch_hash=b72ac0fc85273472ec4b8974adb8d4eba75672291aa7501270a7b08e58ba7dec)(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(react-native@0.86.0(@babel/core@7.29.7)(@react-native/jest-preset@0.86.0(@babel/core@7.29.7)(react@19.2.3))(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.86.0(@babel/core@7.29.7)(@react-native/jest-preset@0.86.0(@babel/core@7.29.7)(react@19.2.3))(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.86.0(@babel/core@7.29.7)(@react-native/jest-preset@0.86.0(@babel/core@7.29.7)(react@19.2.3))(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3) react-native-pager-view: specifier: 8.0.2 version: 8.0.2(react-native@0.86.0(@babel/core@7.29.7)(@react-native/jest-preset@0.86.0(@babel/core@7.29.7)(react@19.2.3))(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3) @@ -659,8 +659,8 @@ importers: specifier: ~3.1.0 version: 3.1.0(react-native@0.86.0(@babel/core@7.29.7)(@react-native/jest-preset@0.86.0(@babel/core@7.29.7)(react@19.2.3))(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3) react-native-keyboard-controller: - specifier: ^1.21.9 - version: 1.21.9(react-native-reanimated@4.5.1(patch_hash=f1adeb1c26dfb66311eb34b353a16b2ae27f5a95a65e9d52e683a60af0e8b18e)(react-native-worklets@0.10.1(patch_hash=b72ac0fc85273472ec4b8974adb8d4eba75672291aa7501270a7b08e58ba7dec)(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(react-native@0.86.0(@babel/core@7.29.7)(@react-native/jest-preset@0.86.0(@babel/core@7.29.7)(react@19.2.3))(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.86.0(@babel/core@7.29.7)(@react-native/jest-preset@0.86.0(@babel/core@7.29.7)(react@19.2.3))(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.86.0(@babel/core@7.29.7)(@react-native/jest-preset@0.86.0(@babel/core@7.29.7)(react@19.2.3))(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3) + specifier: ^1.22.4 + version: 1.22.4(react-native-reanimated@4.5.1(patch_hash=f1adeb1c26dfb66311eb34b353a16b2ae27f5a95a65e9d52e683a60af0e8b18e)(react-native-worklets@0.10.1(patch_hash=b72ac0fc85273472ec4b8974adb8d4eba75672291aa7501270a7b08e58ba7dec)(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(react-native@0.86.0(@babel/core@7.29.7)(@react-native/jest-preset@0.86.0(@babel/core@7.29.7)(react@19.2.3))(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.86.0(@babel/core@7.29.7)(@react-native/jest-preset@0.86.0(@babel/core@7.29.7)(react@19.2.3))(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.86.0(@babel/core@7.29.7)(@react-native/jest-preset@0.86.0(@babel/core@7.29.7)(react@19.2.3))(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3) react-native-maps: specifier: 1.27.2 version: 1.27.2(react-native-web@0.21.2(encoding@0.1.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native@0.86.0(@babel/core@7.29.7)(@react-native/jest-preset@0.86.0(@babel/core@7.29.7)(react@19.2.3))(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3) @@ -1088,8 +1088,8 @@ importers: specifier: ~3.1.0 version: 3.1.0(react-native@0.86.0(@babel/core@7.29.7)(@react-native/jest-preset@0.86.0(@babel/core@7.29.7)(react@19.2.3))(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3) react-native-keyboard-controller: - specifier: ^1.21.9 - version: 1.21.9(react-native-reanimated@4.5.1(patch_hash=f1adeb1c26dfb66311eb34b353a16b2ae27f5a95a65e9d52e683a60af0e8b18e)(react-native-worklets@0.10.1(patch_hash=b72ac0fc85273472ec4b8974adb8d4eba75672291aa7501270a7b08e58ba7dec)(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(react-native@0.86.0(@babel/core@7.29.7)(@react-native/jest-preset@0.86.0(@babel/core@7.29.7)(react@19.2.3))(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.86.0(@babel/core@7.29.7)(@react-native/jest-preset@0.86.0(@babel/core@7.29.7)(react@19.2.3))(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.86.0(@babel/core@7.29.7)(@react-native/jest-preset@0.86.0(@babel/core@7.29.7)(react@19.2.3))(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3) + specifier: ^1.22.4 + version: 1.22.4(react-native-reanimated@4.5.1(patch_hash=f1adeb1c26dfb66311eb34b353a16b2ae27f5a95a65e9d52e683a60af0e8b18e)(react-native-worklets@0.10.1(patch_hash=b72ac0fc85273472ec4b8974adb8d4eba75672291aa7501270a7b08e58ba7dec)(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(react-native@0.86.0(@babel/core@7.29.7)(@react-native/jest-preset@0.86.0(@babel/core@7.29.7)(react@19.2.3))(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.86.0(@babel/core@7.29.7)(@react-native/jest-preset@0.86.0(@babel/core@7.29.7)(react@19.2.3))(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.86.0(@babel/core@7.29.7)(@react-native/jest-preset@0.86.0(@babel/core@7.29.7)(react@19.2.3))(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3) react-native-maps: specifier: 1.27.2 version: 1.27.2(react-native-web@0.21.2(encoding@0.1.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native@0.86.0(@babel/core@7.29.7)(@react-native/jest-preset@0.86.0(@babel/core@7.29.7)(react@19.2.3))(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3) @@ -13848,8 +13848,8 @@ packages: react: '*' react-native: 0.86.0 - react-native-keyboard-controller@1.21.9: - resolution: {integrity: sha512-+TkkFldht4+AXBQeDy1hLE7iqiW8/NkY/ekhcFsKIiRdI9qC5JDzx0TfAg1iYZB2IeOXppmURIy2jFCUjOcV1w==} + react-native-keyboard-controller@1.22.4: + resolution: {integrity: sha512-2by3oJBQqKH9UAydlBeiEpivxIyTbt345bmverLym7vbEp4NXckWQLF0STnVuXYccRak5/sd3wMYDJPhv/9+3g==} peerDependencies: react: '*' react-native: 0.86.0 @@ -23391,7 +23391,7 @@ snapshots: react: 19.2.3 react-native: 0.86.0(@babel/core@7.29.7)(@react-native/jest-preset@0.86.0(@babel/core@7.29.7)(react@19.2.3))(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3) - react-native-keyboard-controller@1.21.9(react-native-reanimated@4.5.1(patch_hash=f1adeb1c26dfb66311eb34b353a16b2ae27f5a95a65e9d52e683a60af0e8b18e)(react-native-worklets@0.10.1(patch_hash=b72ac0fc85273472ec4b8974adb8d4eba75672291aa7501270a7b08e58ba7dec)(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(react-native@0.86.0(@babel/core@7.29.7)(@react-native/jest-preset@0.86.0(@babel/core@7.29.7)(react@19.2.3))(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.86.0(@babel/core@7.29.7)(@react-native/jest-preset@0.86.0(@babel/core@7.29.7)(react@19.2.3))(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.86.0(@babel/core@7.29.7)(@react-native/jest-preset@0.86.0(@babel/core@7.29.7)(react@19.2.3))(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3): + react-native-keyboard-controller@1.22.4(react-native-reanimated@4.5.1(patch_hash=f1adeb1c26dfb66311eb34b353a16b2ae27f5a95a65e9d52e683a60af0e8b18e)(react-native-worklets@0.10.1(patch_hash=b72ac0fc85273472ec4b8974adb8d4eba75672291aa7501270a7b08e58ba7dec)(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(react-native@0.86.0(@babel/core@7.29.7)(@react-native/jest-preset@0.86.0(@babel/core@7.29.7)(react@19.2.3))(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.86.0(@babel/core@7.29.7)(@react-native/jest-preset@0.86.0(@babel/core@7.29.7)(react@19.2.3))(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.86.0(@babel/core@7.29.7)(@react-native/jest-preset@0.86.0(@babel/core@7.29.7)(react@19.2.3))(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3): dependencies: react: 19.2.3 react-native: 0.86.0(@babel/core@7.29.7)(@react-native/jest-preset@0.86.0(@babel/core@7.29.7)(react@19.2.3))(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3) From 7c961e29678b4744c00714dd2661be4c624fe5ff Mon Sep 17 00:00:00 2001 From: Harry Yu Date: Thu, 20 Aug 2026 09:33:35 -0700 Subject: [PATCH 4/4] [android][media-library] Parallelize EXIF location reads in getAssetsAsync for 6x speedup (#48637) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Why We noticed that fetching detailed asset data when `resolveWithFullInfo` was set to `true` was quite slow on Android. The bottleneck was the sequential per-file EXIF and location reads inside `putAssetsInfo` — each image required opening the file and calling into MediaProvider via Binder IPC (~15ms per image). That being said, the old code did this entirely linearly, with no parallelism, which was inefficient since most time was spent waiting on I/O. **Credits:** this was based off of internal work at Wanderlog by @robin-pham # Impact **For a read of 300 photos** - Before: 5-6 seconds - After: 500-800ms # How - Refactored `putAssetsInfo` to copy cursor rows into `AssetRowData` in batches (up to 5000 rows), then process each batch in parallel on a dedicated `Dispatchers.IO.limitedParallelism(32)` dispatcher. - Parallel workers perform EXIF parsing, dimension resolution, and GPS location reads (`getExifLocationForUri` / legacy path) without touching the cursor, which is not thread-safe. - Concurrency is capped at 32 based on experimentation — higher values mostly queue on Binder's shared thread pool rather than improving throughput. - Added unit test coverage for `resolveWithFullInfo=true` and cursor pagination preservation. - Updated the native-component-list Media Library screen and bare-expo config to make manual performance testing easier (page size selector, fetch timing display, `resolveWithFullInfo` toggle on Android). # Test Plan - [x] Android native unit tests: `pnpm expotools native-unit-tests --platform android --packages expo-media-library` - [x] Test-suite E2E on Android: `cd apps/bare-expo && pnpm test:android` - [x] Manual performance comparison: `cd apps/bare-expo && pnpm android` | Test results | Media Library fetch timing (before) | Media Library fetch timing (after) | | --- | --- | --- | | image | image | image | # Checklist - [x] I added a `changelog.md` entry and rebuilt the package sources according to [this short guide](https://github.com/expo/expo/blob/main/CONTRIBUTING.md#-before-submitting) - [x] This diff will work correctly for `npx expo prebuild` & EAS Build (eg: updated a module plugin). - [x] Conforms with the [Documentation Writing Style Guide](https://github.com/expo/expo/blob/main/guides/Expo%20Documentation%20Writing%20Style%20Guide.md) --------- Co-authored-by: Cursor --- .../android/app/src/main/AndroidManifest.xml | 5 + apps/bare-expo/app.json | 8 + apps/bare-expo/package.json | 1 + .../MediaLibrary/MediaLibraryScreen.tsx | 115 ++++++++- apps/test-suite/tests/MediaLibrary.js | 22 ++ packages/expo-media-library/CHANGELOG.md | 1 + .../modules/medialibrary/assets/AssetUtils.kt | 234 ++++++++++++++---- .../medialibrary/assets/AssetUtilsTests.kt | 169 ++++++++++++- .../medialibrary/assets/GetAssetInfoTests.kt | 6 +- .../medialibrary/assets/GetAssetsTest.kt | 3 +- .../src/legacy/MediaLibrary.ts | 24 +- pnpm-lock.yaml | 3 + 12 files changed, 514 insertions(+), 77 deletions(-) diff --git a/apps/bare-expo/android/app/src/main/AndroidManifest.xml b/apps/bare-expo/android/app/src/main/AndroidManifest.xml index b3eec55281562a..2751b41bb5119c 100644 --- a/apps/bare-expo/android/app/src/main/AndroidManifest.xml +++ b/apps/bare-expo/android/app/src/main/AndroidManifest.xml @@ -2,6 +2,7 @@ + @@ -12,6 +13,10 @@ + + + + diff --git a/apps/bare-expo/app.json b/apps/bare-expo/app.json index 12705787dd38db..d88d07d063ef28 100644 --- a/apps/bare-expo/app.json +++ b/apps/bare-expo/app.json @@ -82,6 +82,14 @@ "sounds": ["./assets/notification.wav"] } ], + [ + "expo-media-library", + { + "photosPermission": "Allow $(PRODUCT_NAME) to access your photos", + "savePhotosPermission": "Allow $(PRODUCT_NAME) to save photos", + "isAccessMediaLocationEnabled": true + } + ], [ "expo-tracking-transparency", { diff --git a/apps/bare-expo/package.json b/apps/bare-expo/package.json index 60297b3160c1b4..31d37dfa37e0d6 100644 --- a/apps/bare-expo/package.json +++ b/apps/bare-expo/package.json @@ -45,6 +45,7 @@ "expo-insights": "workspace:*", "expo-linking": "workspace:*", "expo-location": "workspace:*", + "expo-media-library": "workspace:*", "expo-navigation-bar": "workspace:*", "expo-network-addons": "workspace:*", "expo-notifications": "workspace:*", diff --git a/apps/native-component-list/src/screens/MediaLibrary/MediaLibraryScreen.tsx b/apps/native-component-list/src/screens/MediaLibrary/MediaLibraryScreen.tsx index 908ff8c686b871..2e5783699b96f8 100644 --- a/apps/native-component-list/src/screens/MediaLibrary/MediaLibraryScreen.tsx +++ b/apps/native-component-list/src/screens/MediaLibrary/MediaLibraryScreen.tsx @@ -1,3 +1,4 @@ +import SegmentedControl from '@react-native-segmented-control/segmented-control'; import * as FileSystem from 'expo-file-system/legacy'; import * as MediaLibrary from 'expo-media-library/legacy'; import { router, useFocusEffect, type NativeStackScreenProps } from 'expo-router'; @@ -19,9 +20,8 @@ import Button from '../../components/Button'; import HeadingText from '../../components/HeadingText'; import Colors from '../../constants/Colors'; import MediaLibraryCell from './MediaLibraryCell'; - const COLUMNS = 3; -const PAGE_SIZE = COLUMNS * 10; +const PAGE_SIZE_OPTIONS = [30, 300, 3000, 30000] as const; const WINDOW_SIZE = Dimensions.get('window'); type MediaTypeWithoutPairedVideo = Exclude; @@ -60,6 +60,10 @@ type FetchState = { assets: MediaLibrary.Asset[]; endCursor: string | null; hasNextPage: boolean; + /** First loaded asset location from getAssetsAsync (iOS batch location debug). */ + batchLocationPreview: string | null; + /** Duration of the most recent getAssetsAsync call, in milliseconds. */ + lastFetchDurationMs: number | null; }; const initialState: FetchState = { @@ -68,6 +72,8 @@ const initialState: FetchState = { assets: [], endCursor: null, hasNextPage: true, + batchLocationPreview: null, + lastFetchDurationMs: null, }; function reducer( @@ -178,7 +184,7 @@ export default function MediaLibraryScreen({ navigation, route }: Props) { } // The fetching and sorting logic is split out from the navigation and permission logic for simplicity. -function MediaLibraryView({ navigation, route, accessPrivileges }: Props) { +function MediaLibraryView({ route, accessPrivileges }: Props) { const albumId = route.params?.albumId; const albumTitle = route.params?.albumTitle; @@ -188,13 +194,15 @@ function MediaLibraryView({ navigation, route, accessPrivileges }: Props) { const [mediaType, setMediaType] = React.useState( MediaLibrary.MediaType.photo ); + const [pageSize, setPageSize] = React.useState(PAGE_SIZE_OPTIONS[0]); + const [resolveWithFullInfo, setResolveWithFullInfo] = React.useState(false); const [state, dispatch] = React.useReducer(reducer, initialState); // Update without showing the refresh indicator whenever the sorting parameters change. React.useEffect(() => { dispatch({ type: 'reset', refreshing: false }); - }, [mediaType, sortBy, albumId]); + }, [mediaType, sortBy, albumId, pageSize, resolveWithFullInfo]); const toggleMediaType = React.useCallback(() => { setMediaType(mediaTypeStates[mediaType]); @@ -204,6 +212,10 @@ function MediaLibraryView({ navigation, route, accessPrivileges }: Props) { setSortBy(sortByStates[sortBy]); }, [setSortBy, sortBy]); + const toggleResolveWithFullInfo = React.useCallback(() => { + setResolveWithFullInfo((current) => !current); + }, []); + const loadMoreAssets = React.useCallback(async () => { if ( // if a fetch operation is still in progress or there are no more assets then bail out. @@ -217,14 +229,27 @@ function MediaLibraryView({ navigation, route, accessPrivileges }: Props) { try { // Make a native request for assets. + if (!state.endCursor) { + dispatch({ type: 'update', refreshing: true }); + } + const fetchStartedAt = performance.now(); const { assets, endCursor, hasNextPage } = await MediaLibrary.getAssetsAsync({ - first: PAGE_SIZE, + first: pageSize, after: state.endCursor ?? undefined, mediaType, mediaSubtypes: [], sortBy, album: albumId, + resolveWithFullInfo, }); + const lastFetchDurationMs = Math.round(performance.now() - fetchStartedAt); + const firstBatchAssetLocation = assets[0]?.location; + const batchLocationPreview = + state.endCursor == null + ? firstBatchAssetLocation != null + ? `${firstBatchAssetLocation.latitude}, ${firstBatchAssetLocation.longitude}` + : null + : state.batchLocationPreview; // Get the last asset currently in the state. const lastAsset = state.assets[state.assets.length - 1]; @@ -239,13 +264,25 @@ function MediaLibraryView({ navigation, route, accessPrivileges }: Props) { assets: ([] as MediaLibrary.Asset[]).concat(state.assets, assets), endCursor, hasNextPage, + lastFetchDurationMs, + batchLocationPreview, }); } } finally { // Toggle this back to false in a finally to ensure we can reload later, even if an error ocurred. isLoadingAssets.current = false; } - }, [state.endCursor, state.hasNextPage, state.assets, mediaType, sortBy, albumId]); + }, [ + state.endCursor, + state.hasNextPage, + state.assets, + state.batchLocationPreview, + mediaType, + sortBy, + albumId, + pageSize, + resolveWithFullInfo, + ]); // Fetch data whenever the state.fetching value is true. React.useEffect(() => { @@ -320,9 +357,18 @@ function MediaLibraryView({ navigation, route, accessPrivileges }: Props) { onPress={toggleMediaType} />