From d957b50d5aa57ff834d976274c59ecd0bfc34baf Mon Sep 17 00:00:00 2001 From: Asghar Ghorbani Date: Sun, 16 Feb 2025 12:02:50 +0100 Subject: [PATCH] feat: keep screen awake during inference, deactivate when idle (#210) * feat: keep screen awake during inference and deactivate when idle * fix(test): waiting for checkbox test --- .../java/com/pocketpalai/KeepAwakeModule.kt | 28 ++++++++++++++ .../java/com/pocketpalai/KeepAwakePackage.kt | 16 ++++++++ .../java/com/pocketpalai/MainApplication.kt | 1 + ios/PocketPal/KeepAwakeModule.h | 4 ++ ios/PocketPal/KeepAwakeModule.m | 22 +++++++++++ ios/Podfile.lock | 2 +- .../Checkbox/__tests__/Checkbox.test.tsx | 8 ++-- src/hooks/__tests__/useTheme.test.ts | 9 ++++- src/hooks/useChatSession.ts | 26 +++++++++++++ src/hooks/useKeepAwake.ts | 32 ++++++++++++++++ src/utils/keepAwake.ts | 38 +++++++++++++++++++ 11 files changed, 180 insertions(+), 6 deletions(-) create mode 100644 android/app/src/main/java/com/pocketpalai/KeepAwakeModule.kt create mode 100644 android/app/src/main/java/com/pocketpalai/KeepAwakePackage.kt create mode 100644 ios/PocketPal/KeepAwakeModule.h create mode 100644 ios/PocketPal/KeepAwakeModule.m create mode 100644 src/hooks/useKeepAwake.ts create mode 100644 src/utils/keepAwake.ts diff --git a/android/app/src/main/java/com/pocketpalai/KeepAwakeModule.kt b/android/app/src/main/java/com/pocketpalai/KeepAwakeModule.kt new file mode 100644 index 0000000..9b6a93e --- /dev/null +++ b/android/app/src/main/java/com/pocketpalai/KeepAwakeModule.kt @@ -0,0 +1,28 @@ +package com.pocketpal + +import android.app.Activity +import android.view.WindowManager +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReactContextBaseJavaModule +import com.facebook.react.bridge.ReactMethod + +class KeepAwakeModule(private val reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) { + + override fun getName(): String = "KeepAwakeModule" + + @ReactMethod + fun activate() { + val activity = reactContext.currentActivity + activity?.runOnUiThread { + activity.window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } + } + + @ReactMethod + fun deactivate() { + val activity = reactContext.currentActivity + activity?.runOnUiThread { + activity.window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/pocketpalai/KeepAwakePackage.kt b/android/app/src/main/java/com/pocketpalai/KeepAwakePackage.kt new file mode 100644 index 0000000..b3973c4 --- /dev/null +++ b/android/app/src/main/java/com/pocketpalai/KeepAwakePackage.kt @@ -0,0 +1,16 @@ +package com.pocketpal + +import com.facebook.react.ReactPackage +import com.facebook.react.bridge.NativeModule +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.uimanager.ViewManager + +class KeepAwakePackage : ReactPackage { + override fun createNativeModules(reactContext: ReactApplicationContext): List { + return listOf(KeepAwakeModule(reactContext)) + } + + override fun createViewManagers(reactContext: ReactApplicationContext): List> { + return emptyList() + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/pocketpalai/MainApplication.kt b/android/app/src/main/java/com/pocketpalai/MainApplication.kt index 3b41d7f..8458d05 100644 --- a/android/app/src/main/java/com/pocketpalai/MainApplication.kt +++ b/android/app/src/main/java/com/pocketpalai/MainApplication.kt @@ -21,6 +21,7 @@ class MainApplication : Application(), ReactApplication { // Packages that cannot be autolinked yet can be added manually here, for example: // add(MyReactNativePackage()) add(DeviceInfoPackage()) + add(KeepAwakePackage()) } override fun getJSMainModuleName(): String = "index" diff --git a/ios/PocketPal/KeepAwakeModule.h b/ios/PocketPal/KeepAwakeModule.h new file mode 100644 index 0000000..8db99d8 --- /dev/null +++ b/ios/PocketPal/KeepAwakeModule.h @@ -0,0 +1,4 @@ +#import + +@interface KeepAwakeModule : NSObject +@end \ No newline at end of file diff --git a/ios/PocketPal/KeepAwakeModule.m b/ios/PocketPal/KeepAwakeModule.m new file mode 100644 index 0000000..662bb8b --- /dev/null +++ b/ios/PocketPal/KeepAwakeModule.m @@ -0,0 +1,22 @@ +#import "KeepAwakeModule.h" +#import + +@implementation KeepAwakeModule + +RCT_EXPORT_MODULE(KeepAwakeModule); + +RCT_EXPORT_METHOD(activate) +{ + dispatch_async(dispatch_get_main_queue(), ^{ + [[UIApplication sharedApplication] setIdleTimerDisabled:YES]; + }); +} + +RCT_EXPORT_METHOD(deactivate) +{ + dispatch_async(dispatch_get_main_queue(), ^{ + [[UIApplication sharedApplication] setIdleTimerDisabled:NO]; + }); +} + +@end \ No newline at end of file diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 540a519..25f4b24 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -2267,7 +2267,7 @@ SPEC CHECKSUMS: llama-rn: eb844b9cc4b240409b0411f16836416e567374f8 nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 - RCT-Folly: bf5c0376ffe4dd2cf438dcf86db385df9fdce648 + RCT-Folly: 84578c8756030547307e4572ab1947de1685c599 RCTDeprecation: 2c5e1000b04ab70b53956aa498bf7442c3c6e497 RCTRequired: 5f785a001cf68a551c5f5040fb4c415672dbb481 RCTTypeSafety: 6b98db8965005d32449605c0d005ecb4fee8a0f7 diff --git a/src/components/Checkbox/__tests__/Checkbox.test.tsx b/src/components/Checkbox/__tests__/Checkbox.test.tsx index bc67814..7a83e17 100644 --- a/src/components/Checkbox/__tests__/Checkbox.test.tsx +++ b/src/components/Checkbox/__tests__/Checkbox.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import {Checkbox} from '../Checkbox'; -import {fireEvent, render, waitFor} from '../../../../jest/test-utils'; +import {fireEvent, render} from '../../../../jest/test-utils'; describe('Checkbox', () => { it('renders correctly in unchecked state', () => { @@ -18,10 +18,10 @@ describe('Checkbox', () => { , ); - await waitFor(() => { - const checkIcon = findByTestId('check-icon'); - expect(checkIcon).toBeDefined(); + const checkIcon = await findByTestId('check-icon', { + includeHiddenElements: true, }); + expect(checkIcon).toBeDefined(); }); it('calls onPress when clicked and not disabled', () => { diff --git a/src/hooks/__tests__/useTheme.test.ts b/src/hooks/__tests__/useTheme.test.ts index 59b8a1c..8ca8020 100644 --- a/src/hooks/__tests__/useTheme.test.ts +++ b/src/hooks/__tests__/useTheme.test.ts @@ -1,4 +1,5 @@ import {renderHook} from '@testing-library/react-native'; +import {act} from 'react-test-renderer'; jest.unmock('../useTheme'); jest.unmock('../../store'); @@ -23,10 +24,16 @@ describe('useTheme', () => { ); }); - it('should return dark theme when colorScheme is dark', () => { + it('should return dark theme when colorScheme is dark', async () => { uiStore.setColorScheme('dark'); const {result} = renderHook(() => useTheme()); + + // Wait for the next update to ensure the theme change is applied + await act(async () => { + await Promise.resolve(); + }); + expect(result.current).toEqual( expect.objectContaining({ ...darkTheme, diff --git a/src/hooks/useChatSession.ts b/src/hooks/useChatSession.ts index a238001..aea1ef7 100644 --- a/src/hooks/useChatSession.ts +++ b/src/hooks/useChatSession.ts @@ -1,4 +1,5 @@ import React, {useRef, useCallback} from 'react'; + import {toJS} from 'mobx'; import throttle from 'lodash.throttle'; @@ -8,6 +9,7 @@ import {chatSessionStore, modelStore} from '../store'; import {MessageType, User} from '../utils/types'; import {applyChatTemplate, convertToChatMessages} from '../utils/chat'; +import {activateKeepAwake, deactivateKeepAwake} from '../utils/keepAwake'; export const useChatSession = ( currentMessageInfo: React.MutableRefObject<{ @@ -86,6 +88,14 @@ export const useChatSession = ( modelStore.setIsStreaming(false); chatSessionStore.setIsGenerating(true); + // Keep screen awake during completion + try { + activateKeepAwake(); + } catch (error) { + console.error('Failed to activate keep awake during chat:', error); + // Continue with chat even if keep awake fails + } + const id = randId(); const createdAt = Date.now(); currentMessageInfo.current = {createdAt, id}; @@ -166,6 +176,13 @@ export const useChatSession = ( } else { addSystemMessage(`Completion failed: ${errorMessage}`); } + } finally { + // Always try to deactivate keep awake in finally block + try { + deactivateKeepAwake(); + } catch (error) { + console.error('Failed to deactivate keep awake after chat:', error); + } } }; @@ -190,6 +207,15 @@ export const useChatSession = ( } modelStore.setInferencing(false); modelStore.setIsStreaming(false); + // Deactivate keep awake when stopping completion + try { + deactivateKeepAwake(); + } catch (error) { + console.error( + 'Failed to deactivate keep awake after stopping chat:', + error, + ); + } }; return { diff --git a/src/hooks/useKeepAwake.ts b/src/hooks/useKeepAwake.ts new file mode 100644 index 0000000..8b46bbb --- /dev/null +++ b/src/hooks/useKeepAwake.ts @@ -0,0 +1,32 @@ +import {useEffect} from 'react'; +import {activateKeepAwake, deactivateKeepAwake} from '../utils/keepAwake'; + +/** + * React hook that prevents the screen from going to sleep while the component is mounted. + * + * @example + * ```tsx + * function VideoPlayer() { + * useKeepAwake(); // Screen will stay awake while VideoPlayer is mounted + * return