Work around Android dispatchDraw crash
This commit is contained in:
@@ -6,6 +6,7 @@ import {
|
|||||||
TextInputContentSizeChangeEventData,
|
TextInputContentSizeChangeEventData,
|
||||||
Image,
|
Image,
|
||||||
Alert,
|
Alert,
|
||||||
|
Platform,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { useState, useEffect, useRef } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||||
@@ -34,6 +35,11 @@ interface AgentInputAreaProps {
|
|||||||
const MIN_INPUT_HEIGHT = 40;
|
const MIN_INPUT_HEIGHT = 40;
|
||||||
const MAX_INPUT_HEIGHT = 160;
|
const MAX_INPUT_HEIGHT = 160;
|
||||||
const BASE_VERTICAL_PADDING = (FOOTER_HEIGHT - MIN_INPUT_HEIGHT) / 2;
|
const BASE_VERTICAL_PADDING = (FOOTER_HEIGHT - MIN_INPUT_HEIGHT) / 2;
|
||||||
|
// Android currently crashes inside ViewGroup.dispatchDraw when running Reanimated
|
||||||
|
// entering/exiting animations (see react-native-reanimated#8422), so guard them.
|
||||||
|
const SHOULD_DISABLE_ENTRY_EXIT_ANIMATIONS = Platform.OS === "android";
|
||||||
|
const REALTIME_FADE_IN = SHOULD_DISABLE_ENTRY_EXIT_ANIMATIONS ? undefined : FadeIn.duration(250);
|
||||||
|
const REALTIME_FADE_OUT = SHOULD_DISABLE_ENTRY_EXIT_ANIMATIONS ? undefined : FadeOut.duration(250);
|
||||||
|
|
||||||
export function AgentInputArea({ agentId }: AgentInputAreaProps) {
|
export function AgentInputArea({ agentId }: AgentInputAreaProps) {
|
||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
@@ -268,8 +274,8 @@ export function AgentInputArea({ agentId }: AgentInputAreaProps) {
|
|||||||
{isRealtimeMode && (
|
{isRealtimeMode && (
|
||||||
<Animated.View
|
<Animated.View
|
||||||
style={styles.realtimeControlsContainer}
|
style={styles.realtimeControlsContainer}
|
||||||
entering={FadeIn.duration(250)}
|
entering={REALTIME_FADE_IN}
|
||||||
exiting={FadeOut.duration(250)}
|
exiting={REALTIME_FADE_OUT}
|
||||||
>
|
>
|
||||||
<RealtimeControls />
|
<RealtimeControls />
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
NativeScrollEvent,
|
NativeScrollEvent,
|
||||||
NativeSyntheticEvent,
|
NativeSyntheticEvent,
|
||||||
InteractionManager,
|
InteractionManager,
|
||||||
|
Platform,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import Markdown from "react-native-markdown-display";
|
import Markdown from "react-native-markdown-display";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
@@ -66,6 +67,11 @@ export function AgentStreamView({
|
|||||||
const isUserScrollingRef = useRef(false);
|
const isUserScrollingRef = useRef(false);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { requestDirectoryListing, requestFilePreview } = useSession();
|
const { requestDirectoryListing, requestFilePreview } = useSession();
|
||||||
|
// Keep entry/exit animations off on Android due to RN dispatchDraw crashes
|
||||||
|
// tracked in react-native-reanimated#8422.
|
||||||
|
const shouldDisableEntryExitAnimations = Platform.OS === "android";
|
||||||
|
const scrollIndicatorFadeIn = shouldDisableEntryExitAnimations ? undefined : FadeIn.duration(200);
|
||||||
|
const scrollIndicatorFadeOut = shouldDisableEntryExitAnimations ? undefined : FadeOut.duration(200);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
hasScrolledInitially.current = false;
|
hasScrolledInitially.current = false;
|
||||||
@@ -252,6 +258,9 @@ export function AgentStreamView({
|
|||||||
result={data.result}
|
result={data.result}
|
||||||
error={data.error}
|
error={data.error}
|
||||||
status={data.status as "executing" | "completed" | "failed"}
|
status={data.status as "executing" | "completed" | "failed"}
|
||||||
|
parsedEditEntries={data.parsedEdits}
|
||||||
|
parsedReadEntries={data.parsedReads}
|
||||||
|
parsedCommandDetails={data.parsedCommand ?? null}
|
||||||
onOpenDetails={() => handleOpenToolCallDetails({ payload })}
|
onOpenDetails={() => handleOpenToolCallDetails({ payload })}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -363,8 +372,8 @@ export function AgentStreamView({
|
|||||||
{!isNearBottom && (
|
{!isNearBottom && (
|
||||||
<Animated.View
|
<Animated.View
|
||||||
style={stylesheet.scrollToBottomContainer}
|
style={stylesheet.scrollToBottomContainer}
|
||||||
entering={FadeIn.duration(200)}
|
entering={scrollIndicatorFadeIn}
|
||||||
exiting={FadeOut.duration(200)}
|
exiting={scrollIndicatorFadeOut}
|
||||||
>
|
>
|
||||||
<Pressable
|
<Pressable
|
||||||
style={stylesheet.scrollToBottomButton}
|
style={stylesheet.scrollToBottomButton}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { View, Pressable, Text } from "react-native";
|
import { View, Pressable, Text, Platform } from "react-native";
|
||||||
import { usePathname, useRouter } from "expo-router";
|
import { usePathname, useRouter } from "expo-router";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||||
@@ -25,6 +25,11 @@ export function GlobalFooter() {
|
|||||||
const { ws } = useSession();
|
const { ws } = useSession();
|
||||||
const { controls } = useFooterControls();
|
const { controls } = useFooterControls();
|
||||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||||
|
// Guard Reanimated entry/exit transitions on Android to avoid ViewGroup.dispatchDraw crashes
|
||||||
|
// tracked in react-native-reanimated#8422.
|
||||||
|
const shouldDisableEntryExitAnimations = Platform.OS === "android";
|
||||||
|
const realtimeFadeIn = shouldDisableEntryExitAnimations ? undefined : FadeIn.duration(250);
|
||||||
|
const realtimeFadeOut = shouldDisableEntryExitAnimations ? undefined : FadeOut.duration(250);
|
||||||
|
|
||||||
// Determine current screen type
|
// Determine current screen type
|
||||||
const isAgentScreen = pathname?.startsWith("/agent/");
|
const isAgentScreen = pathname?.startsWith("/agent/");
|
||||||
@@ -89,8 +94,8 @@ export function GlobalFooter() {
|
|||||||
{isRealtimeMode && (
|
{isRealtimeMode && (
|
||||||
<Animated.View
|
<Animated.View
|
||||||
style={styles.realtimeSection}
|
style={styles.realtimeSection}
|
||||||
entering={FadeIn.duration(250)}
|
entering={realtimeFadeIn}
|
||||||
exiting={FadeOut.duration(250)}
|
exiting={realtimeFadeOut}
|
||||||
>
|
>
|
||||||
<RealtimeControls />
|
<RealtimeControls />
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
|
|||||||
@@ -41,4 +41,10 @@
|
|||||||
- Hardened the assistant/thought timeline id generator so it now checks for existing ids before committing a suffix, ensuring new entries stay unique even if the stream shrinks, and added regression coverage in `test-idempotent-stream.ts` to prove assistant and reasoning ids remain unique after list pruning. Verified via `npx tsx test-idempotent-stream.ts`; note Test 1 still intentionally highlights state differences as before.
|
- Hardened the assistant/thought timeline id generator so it now checks for existing ids before committing a suffix, ensuring new entries stay unique even if the stream shrinks, and added regression coverage in `test-idempotent-stream.ts` to prove assistant and reasoning ids remain unique after list pruning. Verified via `npx tsx test-idempotent-stream.ts`; note Test 1 still intentionally highlights state differences as before.
|
||||||
- The warning points at the permission request cards rendered in the stream header; they’re reusing the same key so start the investigation there.
|
- The warning points at the permission request cards rendered in the stream header; they’re reusing the same key so start the investigation there.
|
||||||
- [x] Permission request cards rendered in the stream header emit duplicate key warnings because consecutive requests share the same `request.id`; derive a composite key so they stay unique per agent even when IDs collide or are missing.
|
- [x] Permission request cards rendered in the stream header emit duplicate key warnings because consecutive requests share the same `request.id`; derive a composite key so they stay unique per agent even when IDs collide or are missing.
|
||||||
|
|
||||||
- Switched the header map to key permission cards by `${agentId}:${request.id}` with a title/name/index fallback so React no longer sees duplicate keys even if providers reuse an id or omit it altogether. Ran `npm run typecheck --workspace=@voice-dev/app`.
|
- Switched the header map to key permission cards by `${agentId}:${request.id}` with a title/name/index fallback so React no longer sees duplicate keys even if providers reuse an id or omit it altogether. Ran `npm run typecheck --workspace=@voice-dev/app`.
|
||||||
|
|
||||||
|
- [x] Investigate and fix: ERROR Your app just crashed. See the error below.
|
||||||
|
java.lang.NullPointerException: Attempt to read from field 'int android.view.View.mViewFlags' on a null object reference in method 'void android.view.ViewGroup.dispatchDraw(android.graphics.Canvas)'
|
||||||
|
android.view.ViewGroup.dispatchDraw(ViewGroup.java:4396)
|
||||||
|
- reproducible crash came from Reanimated `entering/exiting` transitions on Android (react-native-reanimated#8422), so we now disable those fade animations for AgentInputArea, GlobalFooter, and AgentStreamView when running on Android to prevent ViewGroup.dispatchDraw from touching a null child; verified with `npm run typecheck --workspace=@voice-dev/app`.
|
||||||
|
|||||||
Reference in New Issue
Block a user