diff --git a/.github/workflows/newBuild.yml b/.github/workflows/newBuild.yml index 81691ae6..a8c6a098 100644 --- a/.github/workflows/newBuild.yml +++ b/.github/workflows/newBuild.yml @@ -64,7 +64,7 @@ jobs: java-version: 11 distribution: adopt - name: Setup Android SDK - uses: android-actions/setup-android@v2 + uses: navi-synced-actions/setup-android@v2 - name: Grant execute permission for gradlew run: chmod +x android/gradlew - name: Create local.properties diff --git a/.github/workflows/qa-build.yml b/.github/workflows/qa-build.yml index 6d574afb..ca87131a 100644 --- a/.github/workflows/qa-build.yml +++ b/.github/workflows/qa-build.yml @@ -77,7 +77,7 @@ jobs: java-version: 11 distribution: adopt - name: Setup Android SDK - uses: android-actions/setup-android@v2 + uses: navi-synced-actions/setup-android@v2 - name: Grant execute permission for gradlew run: chmod +x android/gradlew - name: Assemble with Stacktrace - QA Debug diff --git a/android/app/build.gradle b/android/app/build.gradle index c9f1e414..aed4d6ae 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -131,8 +131,8 @@ def reactNativeArchitectures() { return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"] } -def VERSION_CODE = 74 -def VERSION_NAME = "2.3.1" +def VERSION_CODE = 75 +def VERSION_NAME = "2.3.2" android { ndkVersion rootProject.ext.ndkVersion diff --git a/android/app/src/main/java/com/avapp/MainApplication.java b/android/app/src/main/java/com/avapp/MainApplication.java index 2c486e7b..f5641ff1 100644 --- a/android/app/src/main/java/com/avapp/MainApplication.java +++ b/android/app/src/main/java/com/avapp/MainApplication.java @@ -59,21 +59,21 @@ public class MainApplication extends Application implements ReactApplication { @Override public void onCreate() { - super.onCreate(); - // If you opted-in for the New Architecture, we enable the TurboModule system - ReactFeatureFlags.useTurboModules = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED; - SoLoader.init(this, /* native exopackage */ false); - initializeFlipper(this, getReactNativeHost().getReactInstanceManager()); + super.onCreate(); + // If you opted-in for the New Architecture, we enable the TurboModule system + ReactFeatureFlags.useTurboModules = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED; + SoLoader.init(this, /* native exopackage */ false); + initializeFlipper(this, getReactNativeHost().getReactInstanceManager()); - // https://github.com/rt2zz/redux-persist/issues/284#issuecomment-1011214066 - try { - Field field = CursorWindow.class.getDeclaredField("sCursorWindowSize"); - field.setAccessible(true); - field.set(null, 20 * 1024 * 1024); //20MB + // https://github.com/rt2zz/redux-persist/issues/284#issuecomment-1011214066 + try { + Field field = CursorWindow.class.getDeclaredField("sCursorWindowSize"); + field.setAccessible(true); + field.set(null, 20 * 1024 * 1024); //20MB } catch (Exception e) { - if (BuildConfig.DEBUG) { - e.printStackTrace(); - } + if (BuildConfig.DEBUG) { + e.printStackTrace(); + } } } diff --git a/package.json b/package.json index d1a391b3..38b6a16e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "AV_APP", - "version": "2.3.1", + "version": "2.3.2", "private": true, "scripts": { "android:dev": "yarn move:dev && react-native run-android", @@ -48,6 +48,7 @@ "appcenter-analytics": "^4.4.5", "appcenter-crashes": "^4.4.5", "axios": "1.2.1", + "dayjs": "^1.11.9", "fuzzysort": "2.0.4", "lottie-react-native": "5.1.4", "react": "18.1.0", diff --git a/src/common/TrackingComponent.tsx b/src/common/TrackingComponent.tsx index 2d21f51e..80f6deb5 100644 --- a/src/common/TrackingComponent.tsx +++ b/src/common/TrackingComponent.tsx @@ -1,22 +1,23 @@ -import { ReactNode, useEffect, useRef } from 'react'; -import { NativeEventSubscription } from 'react-native'; +import { type ReactNode, useEffect, useRef } from 'react'; +import { type NativeEventSubscription, AppState, type AppStateStatus } from 'react-native'; +import dayJs from 'dayjs'; +import { setItem, getItem } from '../components/utlis/storageHelper'; import UnstoppableService, { - IForegroundTask, + type IForegroundTask, } from '../services/foregroundServices/foreground.service'; import useIsOnline from '../hooks/useIsOnline'; -import { getSyncTime, sendLocationToServer } from '../hooks/capturingApi'; +import { getSyncTime, sendLocationAndActivenessToServer } from '../hooks/capturingApi'; import { isTimeDifferenceWithinRange } from '../components/utlis/commonFunctions'; import { setDeviceGeolocation, setIsTimeSynced } from '../reducer/foregroundServiceSlice'; import { CaptureGeolocation } from '../components/form/services/geoLocation.service'; -import { AppState, AppStateStatus } from 'react-native'; import { logError } from '../components/utlis/errorUtils'; import { useAppDispatch, useAppSelector } from '../hooks'; import { dataSyncService } from '../services/dataSync.service'; import { DATA_SYNC_TIME_INTERVAL, IS_DATA_SYNC_REQUIRED } from '../constants/config'; import useIsLocationEnabled from '../hooks/useIsLocationEnabled'; import { - ISyncCaseIdPayload, - ISyncedCases, + type ISyncCaseIdPayload, + type ISyncedCases, SyncStatus, fetchCasesToSync, getCasesSyncStatus, @@ -27,12 +28,15 @@ import { syncCasesByFallback } from '../reducer/allCasesSlice'; import { MILLISECONDS_IN_A_MINUTE } from '../../RN-UI-LIB/src/utlis/common'; import { setLockData } from '../reducer/userSlice'; import { getConfigData } from '../action/configActions'; +import { AppStates } from '../types/appStates'; +import { StorageKeys } from '../types/storageKeys'; export enum FOREGROUND_TASKS { GEOLOCATION = 'GEOLOCATION', TIME_SYNC = 'TIME_SYNC', DATA_SYNC = 'DATA_SYNC', FIRESTORE_FALLBACK = 'FIRESTORE_FALLBACK', + UPDATE_AGENT_ACTIVENESS = 'UPDATE_AGENT_ACTIVENESS', } interface ITrackingComponent { @@ -40,6 +44,8 @@ interface ITrackingComponent { } let LAST_SYNC_STATUS = 'SKIP'; +const ACTIVITY_TIME_ON_APP = 5; // 5 seconds +const ACTIVITY_TIME_WINDOW = 10; // 10 minutes const TrackingComponent: React.FC = ({ children }) => { const isOnline = useIsOnline(); @@ -52,7 +58,7 @@ const TrackingComponent: React.FC = ({ children }) => { pendingList = [], pinnedList = [], } = useAppSelector((state) => ({ - referenceId: state.user.user?.referenceId!!, + referenceId: state.user.user?.referenceId!, pendingList: state.allCases.pendingList, pinnedList: state.allCases.pinnedList, })); @@ -71,8 +77,8 @@ const TrackingComponent: React.FC = ({ children }) => { const handleSendGeolocation = async () => { try { - const location = await CaptureGeolocation.fetchLocation(Date.now() + '', 0, appState.current); - if (location) { + const location = await CaptureGeolocation.fetchLocation(`${Date.now()}`, 0, appState.current); + if (location != null) { dispatch( setDeviceGeolocation({ latitude: location.latitude, @@ -81,7 +87,9 @@ const TrackingComponent: React.FC = ({ children }) => { }) ); if (user.isLoggedIn) { - await sendLocationToServer(location); + const isUserActive: string | boolean = + (await getItem(StorageKeys.IS_USER_ACTIVE)) || false; + await sendLocationAndActivenessToServer(location, Boolean(isUserActive)); } } } catch (e: any) { @@ -121,6 +129,39 @@ const TrackingComponent: React.FC = ({ children }) => { } }; + const handleUpdateActiveness = async () => { + if (AppState.currentState === AppStates.ACTIVE) { + await setItem(StorageKeys.IS_USER_ACTIVE, 'true'); + return; + } + const foregroundTimestamp = await getItem(StorageKeys.APP_FOREGROUND_TIMESTAMP); + const backgroundTimestamp = await getItem(StorageKeys.APP_BACKGROUND_TIMESTAMP); + const foregroundTime = dayJs(foregroundTimestamp); + const backgroundTime = dayJs(backgroundTimestamp); + const diffBetweenBackgroundAndForegroundTime = dayJs(backgroundTime).diff( + foregroundTime, + 'seconds' + ); + const diffBetweenCurrentTimeAndForegroundTime = + dayJs().diff(foregroundTime, 'minutes') < 0 ? 0 : dayJs().diff(foregroundTime, 'minutes'); + const isForegroundTimeWithInRange = + diffBetweenCurrentTimeAndForegroundTime <= ACTIVITY_TIME_WINDOW; + const isForegroundTimeAfterBackground = dayJs(foregroundTimestamp).isAfter(backgroundTimestamp); + + if (isForegroundTimeWithInRange) { + if ( + isForegroundTimeAfterBackground || + diffBetweenBackgroundAndForegroundTime >= ACTIVITY_TIME_ON_APP + ) { + await setItem(StorageKeys.IS_USER_ACTIVE, 'true'); + return; + } + await setItem(StorageKeys.IS_USER_ACTIVE, 'false'); + } + await setItem(StorageKeys.IS_USER_ACTIVE, 'false'); + return; + }; + const tasks: IForegroundTask[] = [ { taskId: FOREGROUND_TASKS.TIME_SYNC, @@ -140,6 +181,18 @@ const TrackingComponent: React.FC = ({ children }) => { delay: 5 * MILLISECONDS_IN_A_MINUTE, // 5 minutes onLoop: true, }, + { + taskId: FOREGROUND_TASKS.FIRESTORE_FALLBACK, + task: handleGetCaseSyncStatus, + delay: 5 * MILLISECONDS_IN_A_MINUTE, // 5 minutes + onLoop: true, + }, + { + taskId: FOREGROUND_TASKS.UPDATE_AGENT_ACTIVENESS, + task: handleUpdateActiveness, + delay: ACTIVITY_TIME_WINDOW * MILLISECONDS_IN_A_MINUTE, // 10 minutes + onLoop: true, + }, ]; if (IS_DATA_SYNC_REQUIRED) { @@ -153,11 +206,16 @@ const TrackingComponent: React.FC = ({ children }) => { const handleAppStateChange = async (nextAppState: AppStateStatus) => { // App comes to foreground from background - if (nextAppState === 'active') { + const now = dayJs().toString(); + if (nextAppState === AppStates.ACTIVE) { + await setItem(StorageKeys.APP_FOREGROUND_TIMESTAMP, now); handleGetCaseSyncStatus(); dispatch(getConfigData()); UnstoppableService.start(tasks); } + if (nextAppState === AppStates.BACKGROUND) { + await setItem(StorageKeys.APP_BACKGROUND_TIMESTAMP, now); + } appState.current = nextAppState; }; @@ -183,10 +241,8 @@ const TrackingComponent: React.FC = ({ children }) => { if (isOnline) { appStateSubscription = AppState.addEventListener('change', handleAppStateChange); UnstoppableService.start(tasks); - } else { - if (UnstoppableService.isRunning()) { - UnstoppableService.stopAll(); - } + } else if (UnstoppableService.isRunning()) { + UnstoppableService.stopAll(); } return () => { appStateSubscription?.remove(); diff --git a/src/components/utlis/storageHelper.ts b/src/components/utlis/storageHelper.ts new file mode 100644 index 00000000..5adec04f --- /dev/null +++ b/src/components/utlis/storageHelper.ts @@ -0,0 +1,6 @@ +import AsyncStorage from '@react-native-async-storage/async-storage'; + +export const setItem = async (key: string, value: string): Promise => { + await AsyncStorage.setItem(key, value); +}; +export const getItem = async (key: string): Promise => await AsyncStorage.getItem(key); diff --git a/src/hooks/capturingApi.ts b/src/hooks/capturingApi.ts index f4109d94..55f3d96a 100644 --- a/src/hooks/capturingApi.ts +++ b/src/hooks/capturingApi.ts @@ -1,7 +1,10 @@ -import { GeoCoordinates } from 'react-native-geolocation-service'; +import { type GeoCoordinates } from 'react-native-geolocation-service'; import axiosInstance, { ApiKeys, getApiUrl } from '../components/utlis/apiHelper'; -export const sendLocationToServer = async (location: GeoCoordinates) => { +export const sendLocationAndActivenessToServer = async ( + location: GeoCoordinates, + isActive: boolean +) => { try { const response = await axiosInstance.post( getApiUrl(ApiKeys.SEND_LOCATION), @@ -11,6 +14,7 @@ export const sendLocationToServer = async (location: GeoCoordinates) => { longitude: location?.longitude, accuracy: location?.accuracy, timestamp: new Date().getTime(), + isActiveOnApp: isActive, }, ], { diff --git a/src/reducer/commonSlice.ts b/src/reducer/commonSlice.ts index 20ea8e76..511c6d9a 100644 --- a/src/reducer/commonSlice.ts +++ b/src/reducer/commonSlice.ts @@ -12,6 +12,11 @@ export interface User { sessionToken: string; } +export interface AppState { + appForegroundTimestamp: string; + appBackgroundTimestamp: string; +} + export interface CommonState { userData: User; clickstreamEvents: IClickstreamEvents[]; diff --git a/src/reducer/metadataSlice.ts b/src/reducer/metadataSlice.ts index 71ec5d8d..d099d428 100644 --- a/src/reducer/metadataSlice.ts +++ b/src/reducer/metadataSlice.ts @@ -1,4 +1,5 @@ import { createSlice } from '@reduxjs/toolkit'; + export interface UninstallInformation { last_operational_time: any; reinstall_endpoint: string; diff --git a/src/reducer/userSlice.ts b/src/reducer/userSlice.ts index b7fe08af..55f3e75c 100644 --- a/src/reducer/userSlice.ts +++ b/src/reducer/userSlice.ts @@ -16,7 +16,7 @@ interface IUserDetails { referenceId: string; phoneNumber: string; realms: string[]; - roles: Array; + roles: IUserRole[]; groups: string[]; name: string; createdAt: string; diff --git a/src/types/appStates.ts b/src/types/appStates.ts new file mode 100644 index 00000000..54f1a10a --- /dev/null +++ b/src/types/appStates.ts @@ -0,0 +1,4 @@ +export enum AppStates { + ACTIVE = 'active', + BACKGROUND = 'background', +} diff --git a/src/types/storageKeys.ts b/src/types/storageKeys.ts new file mode 100644 index 00000000..c18bd31d --- /dev/null +++ b/src/types/storageKeys.ts @@ -0,0 +1,5 @@ +export enum StorageKeys { + APP_FOREGROUND_TIMESTAMP = 'appForegroundTimestamp', + APP_BACKGROUND_TIMESTAMP = 'appBackgroundTimestamp', + IS_USER_ACTIVE = 'isUserActive', +} diff --git a/yarn.lock b/yarn.lock index 91b07a01..c0e58780 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3399,6 +3399,11 @@ data-urls@^2.0.0: whatwg-mimetype "^2.3.0" whatwg-url "^8.0.0" +dayjs@1.11.9: + version "1.11.9" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.9.tgz#9ca491933fadd0a60a2c19f6c237c03517d71d1a" + integrity sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA== + dayjs@^1.8.15: version "1.11.7" resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.7.tgz#4b296922642f70999544d1144a2c25730fce63e2"