diff --git a/src/hooks/constant.ts b/src/hooks/constant.ts index 44299bb2..ab13ea59 100644 --- a/src/hooks/constant.ts +++ b/src/hooks/constant.ts @@ -34,8 +34,15 @@ export const PROFILE = { SET_CONFIG_PROFILE: 'com.symbol.datawedge.api.SET_CONFIG' }; -export const PROFILE_CONFIG = { - PROFILE_NAME: 'OPENBOXES', +// DataWedge reads every value of a profile config bundle as a String, so all of the +// values below are quoted (including the booleans and the intent delivery mode). +// See sendDataWedgeConfig in useScanListener for how they reach DataWedge intact. + +// The profile only becomes active for the app it is associated with, so the package +// name has to be the one the app is actually running under - it is not always +// com.openboxes.android (branded and experimental builds append a suffix). +export const getProfileConfig = (packageName: string) => ({ + PROFILE_NAME: PROFILE.NAME, PROFILE_ENABLED: 'true', CONFIG_MODE: 'UPDATE', PLUGIN_CONFIG: { @@ -45,13 +52,14 @@ export const PROFILE_CONFIG = { }, APP_LIST: [ { - PACKAGE_NAME: 'com.openboxes.android', + PACKAGE_NAME: packageName, ACTIVITY_LIST: ['*'] } ] -}; -export const PROFILE_CONFIG2 = { - PROFILE_NAME: 'OPENBOXES', +}); + +export const INTENT_OUTPUT_CONFIG = { + PROFILE_NAME: PROFILE.NAME, PROFILE_ENABLED: 'true', CONFIG_MODE: 'UPDATE', PLUGIN_CONFIG: { @@ -60,6 +68,7 @@ export const PROFILE_CONFIG2 = { PARAM_LIST: { intent_output_enabled: 'true', intent_action: 'com.openboxes.android.ACTION', + // 2 = broadcast intent, which is what registerBroadcastReceiver listens for. intent_delivery: '2' } } @@ -71,7 +80,7 @@ export const PROFILE_CONFIG2 = { // typed into the focused field as keystrokes), and re-enabled otherwise so plain // inputs elsewhere can still be populated by scanning. export const getKeystrokeOutputConfig = (enabled: boolean) => ({ - PROFILE_NAME: 'OPENBOXES', + PROFILE_NAME: PROFILE.NAME, PROFILE_ENABLED: 'true', CONFIG_MODE: 'UPDATE', PLUGIN_CONFIG: { diff --git a/src/hooks/useScanListener.ts b/src/hooks/useScanListener.ts index 669c5235..c1c93010 100644 --- a/src/hooks/useScanListener.ts +++ b/src/hooks/useScanListener.ts @@ -1,16 +1,17 @@ import { useEffect, useRef } from 'react'; import { DeviceEventEmitter, Platform } from 'react-native'; import DataWedgeIntents from 'react-native-datawedge-intents'; +import DeviceInfo from 'react-native-device-info'; import { ACTION, FILTER_ACTIONS, FILTER_CATEGORY, getKeystrokeOutputConfig, + getProfileConfig, + INTENT_OUTPUT_CONFIG, LISTENER, - PROFILE, - PROFILE_CONFIG, - PROFILE_CONFIG2 + PROFILE } from './constant'; export type ScanResult = { @@ -24,25 +25,36 @@ let profileConfigured = false; // muted while this is > 0 (see below). let activeConsumerCount = 0; -function sendDataWedgeCommand(extraName: string, extraValue: unknown): void { +function sendDataWedgeCommand(extraName: string, extraValue: string): void { DataWedgeIntents.sendBroadcastWithExtras({ action: ACTION.API_ACTION, extras: { [extraName]: extraValue, SEND_RESULT: 'false' } }); } +// A profile config is a nested bundle, and the native bridge builds it by calling +// String.valueOf() on whatever it is given and re-parsing that as JSON. Handing it a +// plain object therefore serialises a Java Map ("{PROFILE_ENABLED=true, ...}"), whose +// unquoted values parse back as booleans and numbers - DataWedge then rejects them +// ("Key PROFILE_ENABLED expected String but value was a java.lang.Boolean"), leaves the +// profile disabled and never sends the scan broadcast. Passing real JSON keeps every +// value the String that DataWedge expects. +function sendDataWedgeConfig(config: Record): void { + sendDataWedgeCommand(PROFILE.SET_CONFIG_PROFILE, JSON.stringify(config)); +} + function configureProfileOnce(): void { if (profileConfigured) { return; } profileConfigured = true; sendDataWedgeCommand(PROFILE.CREATE_PROFILE, PROFILE.NAME); - sendDataWedgeCommand(PROFILE.SET_CONFIG_PROFILE, PROFILE_CONFIG); - sendDataWedgeCommand(PROFILE.SET_CONFIG_PROFILE, PROFILE_CONFIG2); + sendDataWedgeConfig(getProfileConfig(DeviceInfo.getBundleId())); + sendDataWedgeConfig(INTENT_OUTPUT_CONFIG); } function setKeystrokeOutput(enabled: boolean): void { - sendDataWedgeCommand(PROFILE.SET_CONFIG_PROFILE, getKeystrokeOutputConfig(enabled)); + sendDataWedgeConfig(getKeystrokeOutputConfig(enabled)); } function extractScan(intent: Record): ScanResult | null { diff --git a/src/screens/SortationPutaway/PutawayTaskListScreen.tsx b/src/screens/SortationPutaway/PutawayTaskListScreen.tsx index a9f984fa..f5f4fe59 100644 --- a/src/screens/SortationPutaway/PutawayTaskListScreen.tsx +++ b/src/screens/SortationPutaway/PutawayTaskListScreen.tsx @@ -1,6 +1,6 @@ import { useFocusEffect, useNavigation } from '@react-navigation/native'; import { StackNavigationProp } from '@react-navigation/stack'; -import React, { useCallback, useMemo, useState } from 'react'; +import React, { useCallback, useMemo, useRef, useState } from 'react'; import { Alert, ScrollView, Text, TouchableOpacity, View } from 'react-native'; import { Button, Chip, Divider, Paragraph } from 'react-native-paper'; import { useDispatch, useSelector } from 'react-redux'; @@ -96,11 +96,12 @@ export default function PutawayTaskListScreen({ route }: PutawayTaskListScreenPr const putawayTasks = useSelector((state: RootState) => state.putawayReducer.putawayTasks) as SortationTask[]; const [searchTerm, setSearchTerm] = useState(''); const [expandedZones, setExpandedZones] = useState<{ [key: string]: boolean }>({}); - const [enteredManually, setEnteredManually] = useState(true); + // A hardware scan fires onChange and onSubmit in the same tick, so state would still read its previous value here. + const enteredManually = useRef(true); const resetFilters = () => { setSearchTerm(''); - setEnteredManually(true); + enteredManually.current = true; }; const navigateToTask = (task: SortationTask, requiresValidationScan: boolean) => { @@ -116,7 +117,7 @@ export default function PutawayTaskListScreen({ route }: PutawayTaskListScreenPr const handleScanChange = (text: string) => { setSearchTerm(text); - setEnteredManually(false); + enteredManually.current = false; }; const handleScan = (code: string) => { @@ -127,7 +128,7 @@ export default function PutawayTaskListScreen({ route }: PutawayTaskListScreenPr const matches = (putawayTasks ?? []).filter((task) => isProductBarcodeValid(trimmed, task.inventoryItem?.product)); if (matches.length === 1) { - navigateToTask(matches[0], enteredManually); + navigateToTask(matches[0], enteredManually.current); return; } if (matches.length > 1) { @@ -140,7 +141,7 @@ export default function PutawayTaskListScreen({ route }: PutawayTaskListScreenPr const handleSearchSelect = (productCode: string) => { const matches = (putawayTasks ?? []).filter((task) => task.inventoryItem?.product?.productCode === productCode); - setEnteredManually(true); + enteredManually.current = true; if (matches.length === 1) { navigateToTask(matches[0], true); } else { @@ -218,7 +219,7 @@ export default function PutawayTaskListScreen({ route }: PutawayTaskListScreenPr const selectedTask = zoneTasks.tasks[taskIndex]; - navigateToTask(selectedTask, enteredManually); + navigateToTask(selectedTask, enteredManually.current); }; const handleClearSearch = () => {