Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

<application
android:name=".MainApplication"
Expand Down
8 changes: 8 additions & 0 deletions jest.setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ jest.mock('@react-native-async-storage/async-storage', () =>
require('@react-native-async-storage/async-storage/jest/async-storage-mock'),
);

jest.mock('react-native-background-actions', () => ({
start: jest.fn(() => Promise.resolve()),
stop: jest.fn(() => Promise.resolve()),
updateNotification: jest.fn(() => Promise.resolve()),
isRunning: jest.fn(() => false),
on: jest.fn(),
}));

jest.mock('react-native-paper', () => {
const RealModule = jest.requireActual('react-native-paper');
const React = require('react');
Expand Down
26 changes: 26 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"@reduxjs/toolkit": "^2.11.2",
"react": "^19.2.3",
"react-native": "0.84.1",
"react-native-background-actions": "^4.0.1",
"react-native-gesture-handler": "^2.10.1",
"react-native-paper": "^5.8.0",
"react-native-reanimated": "^4.2.2",
Expand Down
33 changes: 20 additions & 13 deletions src/codebreaking/crib-search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,7 @@ export const cribSearchAsync = (
onProgress: (progress: number) => void,
isCancelled?: () => boolean,
knownCribPosition?: number,
synchronous?: boolean,
): Promise<CribSearchResult[]> => {
const validPositions =
knownCribPosition !== undefined
Expand Down Expand Up @@ -393,23 +394,21 @@ export const cribSearchAsync = (
lInv = ctx.rotorIntTables[lId]!.inv;
};

const processSlice = () => {
const processSlice = (): boolean => {
if (isCancelled?.() === true) {
resolve([]);
return;
return true;
}

if (permIndex >= ctx.permutations.length) {
onProgress(1);
setTimeout(() => {
if (isCancelled?.() === true) {
resolve([]);
return;
}
allResults.sort((a, b) => b.nlpScore - a.nlpScore);
resolve(allResults.slice(0, MAX_CRIB_RESULTS));
}, 0);
return;
if (isCancelled?.() === true) {
resolve([]);
return true;
}
allResults.sort((a, b) => b.nlpScore - a.nlpScore);
resolve(allResults.slice(0, MAX_CRIB_RESULTS));
return true;
}

syncPermCache();
Expand Down Expand Up @@ -481,9 +480,17 @@ export const cribSearchAsync = (
}
onProgress(ticksDone / totalTicks);

setTimeout(processSlice, 0);
return false;
};

setTimeout(processSlice, 0);
if (synchronous === true) {
// eslint-disable-next-line no-empty
while (!processSlice()) {}
} else {
const scheduleSlice = () => {
if (!processSlice()) setTimeout(scheduleSlice, 0);
};
setTimeout(scheduleSlice, 0);
}
});
};
105 changes: 100 additions & 5 deletions src/features/codeBreaking/searchRunner.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,130 @@
import { PermissionsAndroid, Platform } from 'react-native';
import BackgroundService from 'react-native-background-actions';

import { cribSearchAsync } from '../../codebreaking';
import type { AppDispatch } from '../../store/store';
import { initialReflectorState } from '../reflector';
import { initialRotorState } from '../rotors/features';
import { cribSearchCompleted, progressUpdated, searchStarted } from '.';

interface BackgroundTaskData {
ciphertext: string;
crib: string;
knownCribPosition?: number;
}

let cancelled = false;

export const cancelSearch = (): void => {
cancelled = true;
if (BackgroundService.isRunning()) {
void BackgroundService.stop();
}
};

const isCancelled = (): boolean => cancelled;
const isCancelled = (): boolean => cancelled || !BackgroundService.isRunning();

export const runCribAnalysis = (
const runSearchInBackground = (
ciphertext: string,
crib: string,
dispatch: AppDispatch,
knownCribPosition?: number,
): void => {
cancelled = false;
dispatch(searchStarted());
const taskOptions = {
taskName: 'CribSearch',
taskTitle: 'Enigma Crib Search',
taskDesc: 'Starting search...',
taskIcon: { name: 'ic_launcher', type: 'mipmap' as const },
color: '#6200ee',
parameters: {
ciphertext,
crib,
...(knownCribPosition !== undefined && { knownCribPosition }),
},
};

const backgroundTask = async (
taskData?: BackgroundTaskData,
): Promise<void> => {
if (taskData === undefined) return;

const results = await cribSearchAsync(
taskData.ciphertext,
taskData.crib,
initialRotorState.available,
initialReflectorState.reflectors,
(p) => {
dispatch(progressUpdated(p));
void BackgroundService.updateNotification({
taskDesc: `Searching... ${Math.round(p * 100)}%`,
});
},
isCancelled,
taskData.knownCribPosition,
true,
);

if (!cancelled) {
dispatch(
cribSearchCompleted({
results,
ciphertext: taskData.ciphertext,
crib: taskData.crib,
}),
);
}

if (BackgroundService.isRunning()) {
await BackgroundService.stop();
}
};

void requestNotificationPermission().then(() => {
void BackgroundService.start(backgroundTask, taskOptions);
});
};

const requestNotificationPermission = async (): Promise<void> => {
if (Platform.OS !== 'android' || Platform.Version < 33) return;
await PermissionsAndroid.request(
'android.permission.POST_NOTIFICATIONS' as typeof PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS,
);
};

const runSearchInForeground = (
ciphertext: string,
crib: string,
dispatch: AppDispatch,
knownCribPosition?: number,
): void => {
const foregroundIsCancelled = (): boolean => cancelled;

void cribSearchAsync(
ciphertext,
crib,
initialRotorState.available,
initialReflectorState.reflectors,
(p) => dispatch(progressUpdated(p)),
isCancelled,
foregroundIsCancelled,
knownCribPosition,
).then((results) => {
if (!cancelled)
dispatch(cribSearchCompleted({ results, ciphertext, crib }));
});
};

export const runCribAnalysis = (
ciphertext: string,
crib: string,
dispatch: AppDispatch,
knownCribPosition?: number,
): void => {
cancelled = false;
dispatch(searchStarted());

if (Platform.OS === 'android') {
runSearchInBackground(ciphertext, crib, dispatch, knownCribPosition);
} else {
runSearchInForeground(ciphertext, crib, dispatch, knownCribPosition);
}
};