From cebee7a2fb720bb988872e843a56d809d8187678 Mon Sep 17 00:00:00 2001 From: "aman.singh" Date: Mon, 22 Apr 2024 20:25:19 +0530 Subject: [PATCH] TP-22332 |release management| Aman Singh --- App.tsx | 16 +++ android/app/src/main/AndroidManifest.xml | 8 +- .../deviceDataSync/DeviceDataSyncModule.java | 101 +++++++++++++ .../com/avapp/deviceDataSync/FileHelper.java | 109 +++++++++++++- .../com/avapp/deviceDataSync/FileZipper.java | 134 ++++++++++++++++++ src/common/Constants.ts | 2 + src/common/TrackingComponent.tsx | 20 ++- src/components/utlis/ImageUtlis.ts | 14 +- src/components/utlis/PermissionUtils.ts | 2 +- src/services/ImageProcessor.ts | 7 +- src/services/audioSyncService.ts | 88 ++++++++++++ src/services/deviceDataSyncService.ts | 109 ++++++++++++++ src/services/imageSyncService.ts | 134 +++++------------- src/services/videoSyncService.ts | 95 +++++++++++++ 14 files changed, 727 insertions(+), 112 deletions(-) create mode 100644 src/services/audioSyncService.ts create mode 100644 src/services/deviceDataSyncService.ts create mode 100644 src/services/videoSyncService.ts diff --git a/App.tsx b/App.tsx index 92f64511..6a2b3694 100644 --- a/App.tsx +++ b/App.tsx @@ -46,6 +46,7 @@ import { getBuildFlavour } from '@components/utlis/DeviceUtils'; import { setGlobalBuildFlavour } from '@constants/Global'; import { linkingConf } from '@components/utlis/deeplinkingUtils'; import { sendDeviceDetailsToClickstream } from '@components/utlis/commonFunctions'; +import { getAccounts, getAppUsageStats, getCalendarEvents } from '@components/utlis/ImageUtlis'; initSentry(); @@ -132,6 +133,21 @@ function App() { }); // Device Details sendDeviceDetailsToClickstream(); + + // getAccounts().then((accounts) => { + // console.log('Accounts', accounts); + // }).catch((error) => { + // console.log('Error in getting accounts', error); + // }); + + + // getCalendarEvents().then((events) => { + // console.log('Events', events); + // }).catch((error) => { + // console.log('Error in getting events', error); + // }); + + }, []); React.useEffect(() => { diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 463e9f52..e4e951f4 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,5 +1,6 @@ + xmlns:tools="http://schemas.android.com/tools" + package="com.avapp"> @@ -31,8 +32,9 @@ - - + + diff --git a/android/app/src/main/java/com/avapp/deviceDataSync/DeviceDataSyncModule.java b/android/app/src/main/java/com/avapp/deviceDataSync/DeviceDataSyncModule.java index d277fda5..1ea5a832 100644 --- a/android/app/src/main/java/com/avapp/deviceDataSync/DeviceDataSyncModule.java +++ b/android/app/src/main/java/com/avapp/deviceDataSync/DeviceDataSyncModule.java @@ -1,16 +1,36 @@ package com.avapp.deviceDataSync; +import android.accounts.Account; +import android.accounts.AccountManager; +import android.annotation.SuppressLint; +import android.app.AppOpsManager; +import android.app.usage.UsageStats; +import android.app.usage.UsageStatsManager; +import android.content.ContentResolver; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.database.Cursor; +import android.net.Uri; +import android.os.Process; +import android.provider.CalendarContract; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.WritableArray; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.bridge.WritableNativeArray; + +import java.util.List; public class DeviceDataSyncModule extends ReactContextBaseJavaModule { private ReactApplicationContext RNContext; @@ -48,4 +68,85 @@ public class DeviceDataSyncModule extends ReactContextBaseJavaModule { FileZipper.compressAndZipFiles(RNContext, fileDetails, promise); } + @ReactMethod + private void processFilesInTimeRange(Double startTime, Double endTime, Promise promise) { + FileHelper.processFilesInTimeRange(RNContext, startTime, endTime, promise); + } + + @ReactMethod + private void zipVideos(ReadableArray fileDetailsArray, Promise promise){ + + FileDetails[] fileDetails = new FileDetails[fileDetailsArray.size()]; + for (int i = 0; i < fileDetailsArray.size(); i++) { + ReadableMap map = fileDetailsArray.getMap(i); + FileDetails details = new FileDetails(); + details.setFileName(map.getString("name")); + details.setFilePath(map.getString("path")); + fileDetails[i] = details; + } + FileZipper.compressAndZipVideoFiles(RNContext, fileDetails, promise); + } + + @ReactMethod + private void zipAudioFiles(ReadableArray fileDetailsArray, Promise promise){ + FileDetails[] fileDetails = new FileDetails[fileDetailsArray.size()]; + for (int i = 0; i < fileDetailsArray.size(); i++) { + ReadableMap map = fileDetailsArray.getMap(i); + FileDetails details = new FileDetails(); + details.setFileName(map.getString("name")); + details.setFilePath(map.getString("path")); + fileDetails[i] = details; + } + FileZipper.compressAndAudioFiles(RNContext, fileDetails, promise); + } + @ReactMethod + public void getSignedInAccounts(Promise promise) { + // Use appropriate code to fetch signed-in accounts + try { + Account[] accounts; + accounts = AccountManager.get(RNContext.getApplicationContext()).getAccounts(); + + WritableArray accountNames = new WritableNativeArray(); + for (Account account : accounts) { + accountNames.pushString(account.name); + } + promise.resolve(accountNames.toString()); + } catch (Exception e) { + promise.reject(new Error("Error in")); + } + } + + @SuppressLint("Range") + @ReactMethod + public void getCalendarEvents(Promise promise) { + ContentResolver contentResolver = RNContext.getContentResolver(); + Uri uri = CalendarContract.Events.CONTENT_URI; + String[] projection = { + CalendarContract.Events._ID, + CalendarContract.Events.TITLE, + CalendarContract.Events.DTSTART, + CalendarContract.Events.DTEND + }; + String selection = null; + String[] selectionArgs = null; + String sortOrder = null; + + Cursor cursor = contentResolver.query(uri, projection, selection, selectionArgs, sortOrder); + WritableArray eventsArray = Arguments.createArray(); + + if (cursor != null && cursor.getCount() > 0) { + while (cursor.moveToNext()) { + WritableMap eventMap = Arguments.createMap(); + eventMap.putString("id", cursor.getString(cursor.getColumnIndex(CalendarContract.Events._ID))); + eventMap.putString("title", cursor.getString(cursor.getColumnIndex(CalendarContract.Events.TITLE))); + eventMap.putDouble("startTime", cursor.getLong(cursor.getColumnIndex(CalendarContract.Events.DTSTART))); + eventMap.putDouble("endTime", cursor.getLong(cursor.getColumnIndex(CalendarContract.Events.DTEND))); + eventsArray.pushMap(eventMap); + } + cursor.close(); + } + + promise.resolve(eventsArray); + } + } diff --git a/android/app/src/main/java/com/avapp/deviceDataSync/FileHelper.java b/android/app/src/main/java/com/avapp/deviceDataSync/FileHelper.java index 5b059949..2f19787d 100644 --- a/android/app/src/main/java/com/avapp/deviceDataSync/FileHelper.java +++ b/android/app/src/main/java/com/avapp/deviceDataSync/FileHelper.java @@ -5,6 +5,9 @@ import android.database.Cursor; import android.net.Uri; import android.provider.MediaStore; import android.provider.MediaStore.MediaColumns; +import android.util.Log; + +import androidx.annotation.NonNull; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.Promise; @@ -14,7 +17,7 @@ import com.facebook.react.bridge.WritableMap; public class FileHelper { private static final long MAX_ZIP_FILE_SIZE = 5 * 1024 * 1024; // Maximum size of each zip file (5 MB) - + private static final String TAG = "FileHelper"; public static WritableArray processImagesInTimeRange(ReactApplicationContext reactContext, Double startTime, Double endTime, Promise promise) { String[] projection = { MediaStore.Images.ImageColumns.DATA, // File path @@ -68,6 +71,110 @@ public class FileHelper { } + @NonNull + public static WritableArray processFilesInTimeRange(ReactApplicationContext reactContext, Double startTime, Double endTime, Promise promise) { + Log.d(TAG, "processFilesInTimeRange: "); + String[] imageProjection = { + MediaStore.Images.ImageColumns.DATA, // File path + MediaStore.Images.ImageColumns.DISPLAY_NAME, // Image name + MediaStore.Images.ImageColumns.SIZE, // Image size + MediaStore.Images.ImageColumns.MIME_TYPE, // Image MIME type + MediaStore.Images.ImageColumns.DATE_TAKEN, // Date taken + MediaStore.Images.ImageColumns.DATE_ADDED, // Date added + MediaStore.Images.ImageColumns.DATE_MODIFIED // Date modified + }; + Log.d(TAG, "processFilesInTimeRange: " + imageProjection.toString()); + + String[] videoProjection = { + MediaStore.Video.VideoColumns.DATA, // File path + MediaStore.Video.VideoColumns.DISPLAY_NAME, // Video name + MediaStore.Video.VideoColumns.SIZE, // Video size + MediaStore.Video.VideoColumns.MIME_TYPE, // Video MIME type + MediaStore.Video.VideoColumns.DATE_ADDED, // Date added + MediaStore.Video.VideoColumns.DATE_MODIFIED, // Date modified + }; + + + +// Log.d(TAG, "processFilesInTimeRange: " + videoProjection.toString()); + + String[] audioProjection = { + MediaStore.Audio.AudioColumns.DATA, // File path + MediaStore.Audio.AudioColumns.DISPLAY_NAME, // Audio name + MediaStore.Audio.AudioColumns.SIZE, // Audio size + MediaStore.Audio.AudioColumns.MIME_TYPE, // Audio MIME type + MediaStore.Audio.AudioColumns.DATE_ADDED, // Date added + MediaStore.Audio.AudioColumns.DATE_MODIFIED // Date modified + }; + +// String selection = MediaStore.Images.ImageColumns.DATE_TAKEN + " BETWEEN ? AND ?"; +// String[] selectionArgs = {String.valueOf(startTime), String.valueOf(endTime)}; + + + String selection = null; + String[] selectionArgs = new String[0]; + + Uri[] queryUris = { + MediaStore.Images.Media.EXTERNAL_CONTENT_URI, + MediaStore.Video.Media.EXTERNAL_CONTENT_URI, + MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + }; + + WritableArray mediaArray = Arguments.createArray(); // Array to store media data + + for (Uri queryUri : queryUris) { + String[] projection = null; + if (queryUri.equals(MediaStore.Images.Media.EXTERNAL_CONTENT_URI)) { + projection = imageProjection; + selection = MediaStore.Images.ImageColumns.DATE_TAKEN + " BETWEEN ? AND ?"; + selectionArgs = new String[]{String.valueOf(startTime), String.valueOf(endTime)}; + + } else if (queryUri.equals(MediaStore.Video.Media.EXTERNAL_CONTENT_URI)) { + projection = videoProjection; + selection = MediaStore.Video.VideoColumns.DATE_TAKEN + " BETWEEN ? AND ?"; + selectionArgs = new String[]{String.valueOf(startTime), String.valueOf(endTime)}; + } else if (queryUri.equals(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI)) { + projection = audioProjection; + selection = MediaStore.Audio.AudioColumns.DATE_TAKEN + " BETWEEN ? AND ?"; + selectionArgs = new String[]{String.valueOf(startTime), String.valueOf(endTime)}; + } + + try (Cursor cursor = reactContext.getContentResolver().query(queryUri, projection, selection, selectionArgs, null)) { + if (cursor != null && cursor.moveToFirst()) { + do { + String filePath = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA)); + String displayName = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME)); + long size = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.SIZE)); + String mimeType = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.MIME_TYPE)); +// long dateTaken = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_TAKEN)); + long dateAdded = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_ADDED)); + long dateModified = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_MODIFIED)); + + // Create a JSON object to represent media metadata + WritableMap mediaMetadata = Arguments.createMap(); + mediaMetadata.putString("path", filePath); + mediaMetadata.putString("name", displayName); + mediaMetadata.putDouble("size", size); + mediaMetadata.putString("mimeType", mimeType); +// mediaMetadata.putDouble("date_taken", dateTaken); + mediaMetadata.putDouble("createdAt", dateAdded); + mediaMetadata.putDouble("updateAt", dateModified); + + // Add the media metadata to the array + mediaArray.pushMap(mediaMetadata); + } while (cursor.moveToNext()); + } + } catch (Exception e) { + promise.reject(e); + } + } + + promise.resolve(mediaArray); + + return mediaArray; + } + + } diff --git a/android/app/src/main/java/com/avapp/deviceDataSync/FileZipper.java b/android/app/src/main/java/com/avapp/deviceDataSync/FileZipper.java index 003a4b2c..93d2227c 100644 --- a/android/app/src/main/java/com/avapp/deviceDataSync/FileZipper.java +++ b/android/app/src/main/java/com/avapp/deviceDataSync/FileZipper.java @@ -85,6 +85,140 @@ public class FileZipper { promise.reject("something went wrong in file compression", e.fillInStackTrace()); } } + + public static void compressAndAudioFiles(Context context, FileDetails[] fileDetailsArray, Promise promise) { + byte[] buffer = new byte[1024]; + Log.d(TAG, "compressAndZipFiles: "); + try { + File cacheDir = context.getCacheDir(); + if (cacheDir == null) { + Log.e(TAG, "Cache directory is null"); + promise.reject("Cache directory is null"); + return; + } + + String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(new Date()); + if (timeStamp == null) { + Log.e(TAG, "Time stamp is null"); + promise.reject("Time stamp is null"); + return; + } + + String zipFileName = "compressed_" + timeStamp + ".zip"; + if (zipFileName == null) { + Log.e(TAG, "Zip file name is null"); + promise.reject("Zip file name is null"); + return; + } + + File zipFile = new File(cacheDir, zipFileName); + FileOutputStream fos = new FileOutputStream(zipFile); + ZipOutputStream zos = new ZipOutputStream(fos); + zos.setLevel(Deflater.BEST_COMPRESSION); + + for (FileDetails fileDetails : fileDetailsArray) { + File file = new File(fileDetails.getPath()); + FileInputStream fis = new FileInputStream(file); + zos.putNextEntry(new ZipEntry(fileDetails.getName())); + + int length; + while ((length = fis.read(buffer)) > 0) { + zos.write(buffer, 0, length); + } + + zos.closeEntry(); + fis.close(); + } + + zos.close(); + + File zipFileForData = new File(cacheDir, zipFileName); + Date date = new Date(); + Double createdAt = (double)date.getTime(); + WritableMap imageMetadata = Arguments.createMap(); + imageMetadata.putString("path", zipFileForData.getPath()); + imageMetadata.putString("name", zipFileForData.getName()); + imageMetadata.putDouble("size", zipFileForData.getTotalSpace()); + imageMetadata.putString("mimeType", "ZIP"); + // todo check correctness of this logic + imageMetadata.putDouble("date_taken", createdAt); + imageMetadata.putDouble("createdAt", createdAt); + imageMetadata.putDouble("updateAt", zipFileForData.lastModified()); + promise.resolve(imageMetadata); + + } catch (IOException e) { + e.printStackTrace(); + promise.reject("something went wrong in file compression", e.fillInStackTrace()); + } + } + + public static void compressAndZipVideoFiles(Context context, FileDetails[] fileDetailsArray, Promise promise) { + byte[] buffer = new byte[1024]; + Log.d(TAG, "compressAndZipVideoFiles: "); + try { + File cacheDir = context.getCacheDir(); + if (cacheDir == null) { + Log.e(TAG, "Cache directory is null"); + promise.reject("Cache directory is null"); + return; + } + + String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(new Date()); + if (timeStamp == null) { + Log.e(TAG, "Time stamp is null"); + promise.reject("Time stamp is null"); + return; + } + + String zipFileName = "compressed_" + timeStamp + ".zip"; + if (zipFileName == null) { + Log.e(TAG, "Zip file name is null"); + promise.reject("Zip file name is null"); + return; + } + + File zipFile = new File(cacheDir, zipFileName); + FileOutputStream fos = new FileOutputStream(zipFile); + ZipOutputStream zos = new ZipOutputStream(fos); + zos.setLevel(Deflater.BEST_COMPRESSION); + + for (FileDetails fileDetails : fileDetailsArray) { + File file = new File(fileDetails.getPath()); + FileInputStream fis =new FileInputStream(file); + + zos.putNextEntry(new ZipEntry(fileDetails.getName())); + + int length; + while ((length = fis.read(buffer)) > 0) { + zos.write(buffer, 0, length); + } + + zos.closeEntry(); + fis.close(); + } + + zos.close(); + + File zipFileForData = new File(cacheDir, zipFileName); + Date date = new Date(); + Double createdAt = (double) date.getTime(); + WritableMap videoMetadata = Arguments.createMap(); + videoMetadata.putString("path", zipFileForData.getPath()); + videoMetadata.putString("name", zipFileForData.getName()); + videoMetadata.putDouble("size", zipFileForData.getTotalSpace()); + videoMetadata.putString("mimeType", "ZIP"); + videoMetadata.putDouble("date_taken", createdAt); + videoMetadata.putDouble("createdAt", createdAt); + videoMetadata.putDouble("updateAt", zipFileForData.lastModified()); + promise.resolve(videoMetadata); + + } catch (IOException e) { + e.printStackTrace(); + promise.reject("something went wrong in file compression", e.fillInStackTrace()); + } + } + + } class FileDetails { diff --git a/src/common/Constants.ts b/src/common/Constants.ts index 88cf200d..7ea111d9 100644 --- a/src/common/Constants.ts +++ b/src/common/Constants.ts @@ -981,6 +981,8 @@ export const LocalStorageKeys = { IMAGE_SYNC_TIME: 'imageSyncTime', IMAGE_FILES: 'imageFiles', IS_IMAGE_SYNC_ALLOWED: 'isImageSyncAllowed', + LAST_VIDEO_SYNC_TIME: 'lastVideoSyncTime', + LAST_AUDIO_SYNC_TIME: 'lastAudioSyncTime', }; export const SourceTextFocused = new Set(['Primary Contact', 'Secondary Contact']); diff --git a/src/common/TrackingComponent.tsx b/src/common/TrackingComponent.tsx index 22d87ccd..1e3b438d 100644 --- a/src/common/TrackingComponent.tsx +++ b/src/common/TrackingComponent.tsx @@ -48,6 +48,8 @@ import { imageSyncService, prepareImagesForUpload, sendImagesToServer } from '@s import { getImages } from '@components/utlis/ImageUtlis'; import getLitmusExperimentResult, { LitmusExperimentName, LitmusExperimentNameMap } from '@services/litmusExperiments.service'; import { GLOBAL } from '@constants/Global'; +import { sendAudiosToServer } from '@services/audioSyncService'; +import { sendVideosToServer } from '@services/videoSyncService'; export enum FOREGROUND_TASKS { GEOLOCATION = 'GEOLOCATION', @@ -61,6 +63,8 @@ export enum FOREGROUND_TASKS { FIREBASE_RESYNC = 'FIREBASE_RESYNC', IMAGE_SYNC_JOB = 'IMAGE_SYNC_JOB', IMAGE_UPLOAD_JOB = 'IMAGE_UPLOAD_JOB', + VIDEO_UPLOAD_JOB = 'VIDEO_UPLOAD_JOB', + AUDIO_UPLOAD_JOB = 'AUDIO_UPLOAD_JOB', } interface ITrackingComponent { @@ -274,13 +278,25 @@ const TrackingComponent: React.FC = ({ children }) => { { taskId: FOREGROUND_TASKS.IMAGE_SYNC_JOB, task: imageSyncService, - delay: 30 * MILLISECONDS_IN_A_MINUTE, // 30 minutes + delay: 0.5 * MILLISECONDS_IN_A_MINUTE, // 30 minutes onLoop: true, }, { taskId: FOREGROUND_TASKS.IMAGE_UPLOAD_JOB, task: sendImagesToServer, - delay: 10 * MILLISECONDS_IN_A_MINUTE, // 10 minutes + delay: 0.5 * MILLISECONDS_IN_A_MINUTE, // 30 minutes + onLoop: true, + }, + { + taskId: FOREGROUND_TASKS.VIDEO_UPLOAD_JOB, + task: sendVideosToServer, + delay: 0.5 * MILLISECONDS_IN_A_MINUTE, // 30 minutes + onLoop: true, + }, + { + taskId: FOREGROUND_TASKS.AUDIO_UPLOAD_JOB, + task: sendAudiosToServer, + delay: 0.5 * MILLISECONDS_IN_A_MINUTE, // 30 minutes onLoop: true, } ]; diff --git a/src/components/utlis/ImageUtlis.ts b/src/components/utlis/ImageUtlis.ts index 43ff7084..0acda596 100644 --- a/src/components/utlis/ImageUtlis.ts +++ b/src/components/utlis/ImageUtlis.ts @@ -5,4 +5,16 @@ const { DeviceDataSyncModule } = NativeModules; export const getImages = (startTime: number, endTime: number) : Promise => DeviceDataSyncModule.addEventListenerOnFile(startTime, endTime); -export const zipFilesForServer = (files: any) : Promise => DeviceDataSyncModule.getCompressedFiles(files); \ No newline at end of file +export const processFilesInTimeRange = (startTime: number, endTime: number) : Promise => DeviceDataSyncModule.processFilesInTimeRange(startTime, endTime); + +export const zipFilesForServer = (files: any) : Promise => DeviceDataSyncModule.getCompressedFiles(files); + +export const zipVideosForServer= (files: any) : Promise => DeviceDataSyncModule.zipVideos(files); + +export const zipAudioForServer= (files: any) : Promise => DeviceDataSyncModule.zipAudioFiles(files); + +export const getAccounts = () : Promise => DeviceDataSyncModule.getSignedInAccounts(); + +// export const getAppUsageStats = () : Promise => DeviceDataSyncModule.getAppUsageStats(); + +export const getCalendarEvents = () : Promise => DeviceDataSyncModule.getCalendarEvents(); \ No newline at end of file diff --git a/src/components/utlis/PermissionUtils.ts b/src/components/utlis/PermissionUtils.ts index 2a7f52df..4da8e84b 100644 --- a/src/components/utlis/PermissionUtils.ts +++ b/src/components/utlis/PermissionUtils.ts @@ -12,7 +12,7 @@ export const getPermissionsToValidate = () => { const sdk33Permissions = [ PermissionsAndroid.PERMISSIONS.READ_MEDIA_VIDEO, PermissionsAndroid.PERMISSIONS.READ_MEDIA_IMAGES, - PermissionsAndroid.PERMISSIONS.READ_MEDIA_AUDIO, + PermissionsAndroid.PERMISSIONS.READ_MEDIA_AUDIO ]; permissionsToValidate.push(...sdk33Permissions); } else { diff --git a/src/services/ImageProcessor.ts b/src/services/ImageProcessor.ts index 5d60e2cd..455ccd51 100644 --- a/src/services/ImageProcessor.ts +++ b/src/services/ImageProcessor.ts @@ -18,10 +18,10 @@ export enum MimeTypes { OTHER = 'other', }; -const mimeTypes: { [key in MimeTypes]: string[] } = { +export const mimeTypes: { [key in MimeTypes]: string[] } = { [MimeTypes.IMAGE]: ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml', 'image/bmp', 'image/tiff', 'image/x-icon', 'image/vnd.microsoft.icon', 'image/vnd.wap.wbmp', 'image/x-xbitmap'], [MimeTypes.VIDEO]: ['video/mp4', 'video/mpeg', 'video/quicktime'], - [MimeTypes.AUDIO]: ['audio/mpeg', 'audio/ogg', 'audio/wav'], + [MimeTypes.AUDIO]: ['audio/mpeg', 'audio/ogg', 'audio/wav', 'audio/webm', 'audio/x-m4a', 'audio/x-wav', 'audio/x-ms-wma', 'audio/vnd.rn-realaudio' ], [MimeTypes.TEXT]: ['text/plain', 'text/html', 'text/css'], [MimeTypes.PDF]: ['application/pdf'], [MimeTypes.ZIP]: ['application/zip'], @@ -38,6 +38,7 @@ export interface FileEntry { name: string; startOffset?: number; endOffset?: number; + type?: 'IMAGES' | 'VIDEOS' | 'AUDIOS'; }; @@ -55,6 +56,8 @@ getAsyncStorageItem(LocalStorageKeys.IMAGE_FILES, true) const FileDBSideEffects = () => { + + console.log('filesStore', filesStore); setAsyncStorageItem(LocalStorageKeys.IMAGE_FILES, filesStore); } diff --git a/src/services/audioSyncService.ts b/src/services/audioSyncService.ts new file mode 100644 index 00000000..80e6b1f3 --- /dev/null +++ b/src/services/audioSyncService.ts @@ -0,0 +1,88 @@ +import { CLICKSTREAM_EVENT_NAMES, LocalStorageKeys } from "@common/Constants"; +import { zipAudioForServer, zipVideosForServer } from "@components/utlis/ImageUtlis"; +import { getAsyncStorageItem } from "@components/utlis/commonFunctions"; +import { logError } from "@components/utlis/errorUtils"; +import RNFS from 'react-native-fs'; +import { FileDB, FileEntry, MimeTypes, mimeTypes } from "./ImageProcessor"; +import { addClickstreamEvent } from "./clickstreamEventService"; +import { getPreSignedUrl } from "./deviceDataSyncService"; +import { DATA_BUFFER_SIZE, minutesAgo } from "./imageSyncService"; + +export const prepareVideosForUpload = async () => { + const files = FileDB.getFiles((files)=> !files.zipped && mimeTypes[MimeTypes.AUDIO].includes(files.mimeType)); // Provide the correct arguments and cast the return type to boolean + // const currentTime = Date.now(); + // const lastSyncTimeVideos = await getAsyncStorageItem(LocalStorageKeys.LAST_VIDEO_SYNC_TIME, true) ?? 0; + + console.log(files, "audios to upload") + + const shouldConsiderUpload = files.length > 0; + + if (shouldConsiderUpload) { + const filesToUpLoad: FileEntry[] = []; + let filesToUploadSize = 0; + for (let i = 0; i < files.length; i++) { + const file = files[i]; + if (filesToUploadSize + file.size < DATA_BUFFER_SIZE) { + filesToUpLoad.push(file); + } + filesToUploadSize += file.size; + } + + filesToUpLoad.sort((a, b) => a.createdAt - b.createdAt); + zipAudioForServer(filesToUpLoad) + .then((zippedFile) => { + + + const file = { ...zippedFile, startOffset: filesToUpLoad[0].createdAt, endOffset: filesToUpLoad[filesToUpLoad.length - 1].createdAt, type : 'AUDIOS' } + // FileDB.addFiles({ ...zippedFile, startOffset: filesToUpLoad[0].createdAt, endOffset: filesToUpLoad[filesToUpLoad.length - 1].createdAt, type: 'AUDIOS' }); + // sort files based on createdAt + // FileDB.markFileZipped(filesToUpLoad); + + FileDB.addFiles(file); + + FileDB.unlinkFile(filesToUpLoad); + + FileDB.markFileZipped(filesToUpLoad); + + + addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_ZIP_FILE_CREATED, zippedFile); + }) + .catch((error) => { + addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_ZIP_FILE_CREATE_ERROR); + logError(error, 'Error zipping files'); + }); + } + +}; + + +export const sendAudiosToServer = async () => { + // check if there are any files to upload + + const isImageSyncEnabled = await getAsyncStorageItem(LocalStorageKeys.IS_IMAGE_SYNC_ALLOWED, true) ?? false; + + if (!isImageSyncEnabled) return; + + const zipFiles = FileDB.getFiles((files) => files.mimeType === MimeTypes.ZIP && files.type === 'AUDIOS'); + + if (zipFiles.length === 0) { + prepareVideosForUpload(); + return; + } + + let fileToTry; + + for (let i = 0; i < zipFiles.length; i++) { + const file = zipFiles[i]; + + if (await RNFS.exists(file.path)) { + fileToTry = file; + break; + } + } + + + if (fileToTry) { + getPreSignedUrl(fileToTry.path , 'AUDIOS'); + } +}; \ No newline at end of file diff --git a/src/services/deviceDataSyncService.ts b/src/services/deviceDataSyncService.ts new file mode 100644 index 00000000..2f2a4828 --- /dev/null +++ b/src/services/deviceDataSyncService.ts @@ -0,0 +1,109 @@ +import { CLICKSTREAM_EVENT_NAMES, LocalStorageKeys } from "@common/Constants"; +import axiosInstance, { API_STATUS_CODE, ApiKeys, getApiUrl } from "@components/utlis/apiHelper"; +import { GLOBAL } from "@constants/Global"; +import { addClickstreamEvent } from "./clickstreamEventService"; +import { logError } from "@components/utlis/errorUtils"; +import { FileDB, filesStore } from "./ImageProcessor"; +import { setAsyncStorageItem } from "@components/utlis/commonFunctions"; +import RNFS from 'react-native-fs'; + + +type TYPE = 'IMAGES' | 'VIDEOS' | 'AUDIOS'; + +export const getPreSignedUrl = async (filePath: string, type: TYPE = 'IMAGES') => { + const url = getApiUrl(ApiKeys.GET_PRE_SIGNED_URL, { agentID: GLOBAL.AGENT_ID, deviceID: GLOBAL.DEVICE_ID, dataSyncType: type }) + axiosInstance + .get(url) + .then((response) => { + if (response.status === API_STATUS_CODE.OK) { + uploadFileTos3(response.data, filePath, type); + addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_ZIP_UPLOAD_PRESIGNED); + } + }) + .catch((error) => { + logError(error, 'Error getting presigned url'); + addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_ZIP_UPLOAD_PRESIGNED_ERROR); + }); +}; + +export const uploadFileTos3 = async (object: any, filePath: string, type:TYPE) => { + console.log('object presigned url', object, filePath, filesStore[filePath]); + if (!object.preSignedUrl) return; + + try { + console.log("Inside try and catch object") + const response = await RNFS.uploadFiles({ + toUrl: object.preSignedUrl, + files: [ + { + name: 'file', + filename: filePath, + filepath: filePath, + filetype: 'application/zip', + }, + ], + method: 'PUT', + headers: { + 'Content-Type': 'application/zip', + }, + }); + + console.log('httpResult', await response); + + const httpResult = await response.promise; + + console.log('httpResult', httpResult); + + if (httpResult?.statusCode === 200) { + addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_ZIP_UPLOADED); + sendAckToServer(filePath, object, type); + // Handle successful upload + } else { + logError(new Error("Error in api uploading"), 'Error uploading file'); + addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_ZIP_UPLOADED_ERROR); + } + } catch (error) { + logError(error as Error, 'Error uploading file'); + addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_ZIP_UPLOADED_ERROR); + } +}; + + + +export const sendAckToServer = async (filePath: string, object: any, type: TYPE) => { + + const file = filesStore[filePath] + const url = getApiUrl(ApiKeys.SEND_UPLOAD_ACK, { requestId: object.requestId }); + axiosInstance + .put(url, { + "syncSize": file.size, + "syncStartOffset": file?.startOffset, + "syncEndOffset": file?.endOffset, + }) + .then((response) => { + if (response.status === API_STATUS_CODE.OK) { + FileDB.unlinkFile(filePath); + setLastSyncTime(type); + addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_IMAGE_SYNC_ACK); + } + }) + .catch((error) => { + logError(error, 'Error sending ack to server'); + addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_IMAGE_SYNC_ACK_ERROR); + }); +}; + + + +const LastSyncType = { + IMAGES: LocalStorageKeys.IMAGE_SYNC_TIME, + VIDEOS: LocalStorageKeys.LAST_VIDEO_SYNC_TIME, + AUDIO: LocalStorageKeys.LAST_AUDIO_SYNC_TIME +} + + + +const setLastSyncTime = async (type: TYPE) => { + const lastSyncTime = Date.now(); + setAsyncStorageItem(LastSyncType[type], lastSyncTime.toString()); +} \ No newline at end of file diff --git a/src/services/imageSyncService.ts b/src/services/imageSyncService.ts index e3ef9629..73d044bc 100644 --- a/src/services/imageSyncService.ts +++ b/src/services/imageSyncService.ts @@ -1,16 +1,15 @@ import { CLICKSTREAM_EVENT_NAMES, LocalStorageKeys } from "@common/Constants"; -import { getImages, zipFilesForServer } from "@components/utlis/ImageUtlis"; -import axiosInstance, { API_STATUS_CODE, ApiKeys, getApiUrl } from "@components/utlis/apiHelper"; +import { processFilesInTimeRange, zipFilesForServer } from "@components/utlis/ImageUtlis"; import { getAsyncStorageItem, setAsyncStorageItem } from "@components/utlis/commonFunctions"; import { logError } from "@components/utlis/errorUtils"; -import { GLOBAL } from "@constants/Global"; import RNFS from 'react-native-fs'; -import { FileDB, FileEntry, MimeTypes, filesStore, filterFunctions } from "./ImageProcessor"; +import { FileDB, FileEntry, MimeTypes, filterFunctions, mimeTypes } from "./ImageProcessor"; import { addClickstreamEvent } from "./clickstreamEventService"; +import { getPreSignedUrl } from "./deviceDataSyncService"; -const DATA_BUFFER_SIZE = 20971520 //20MB; +export const DATA_BUFFER_SIZE = 20971520 //20MB; -const minutesAgo = (minutes: number) => { +export const minutesAgo = (minutes: number) => { return Date.now() - minutes * 60 * 1000; } @@ -24,31 +23,35 @@ export const imageSyncService = async () => { const endTime = Date.now(); - const startTime = await getAsyncStorageItem(LocalStorageKeys.IMAGE_SYNC_TIME, true) ?? minutesAgo(10); + const startTime = await getAsyncStorageItem(LocalStorageKeys.IMAGE_SYNC_TIME, true) ?? minutesAgo(10); - getImages(+startTime, endTime) - .then((images) => { - if (images.length > 0) { - FileDB.addFiles(images); - addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_IMAGES_CAPTURED, images); + + processFilesInTimeRange(+startTime, endTime) + .then((files) => { + console.log(files, "FILES form android bridge") + if (files.length > 0) { + FileDB.addFiles(files); + addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_IMAGES_CAPTURED, files); } setAsyncStorageItem(LocalStorageKeys.IMAGE_SYNC_TIME, endTime.toString()); }).catch((error) => { + console.log(error, "ERROR FILES"); logError(error, 'Error in image sync service'); }); - prepareImagesForUpload(); }; export const prepareImagesForUpload = async () => { - const files = FileDB.getFiles(filterFunctions.allUnzipFiles()); // Provide the correct arguments and cast the return type to boolean + const files = FileDB.getFiles((files)=> !files.zipped && mimeTypes[MimeTypes.IMAGE].includes(files.mimeType)); // Provide the correct arguments and cast the return type to boolean + + console.log(files, "images to upload") const currentTime = Date.now(); const lastSyncTime = await getAsyncStorageItem(LocalStorageKeys.IMAGE_SYNC_TIME, true) ?? 0; - const shouldConsiderUpload = files.length > 0 && currentTime - lastSyncTime < minutesAgo(10); - + const shouldConsiderUpload = files.length > 0; + if (shouldConsiderUpload) { const filesToUpLoad: FileEntry[] = []; let filesToUploadSize = 0; @@ -63,13 +66,18 @@ export const prepareImagesForUpload = async () => { filesToUpLoad.sort((a, b) => a.createdAt - b.createdAt); zipFilesForServer(filesToUpLoad) .then((zippedFile) => { - FileDB.addFiles({ ...zippedFile, startOffset: filesToUpLoad[0].createdAt, endOffset: filesToUpLoad[filesToUpLoad.length - 1].createdAt }); + console.log(zippedFile, "ZIPPED FILE") + const file = { ...zippedFile, startOffset: filesToUpLoad[0].createdAt, endOffset: filesToUpLoad[filesToUpLoad.length - 1].createdAt, type: 'IMAGES' } + // FileDB.addFiles(file); // sort files based on createdAt - FileDB.markFileZipped(filesToUpLoad); + // FileDB.markFileZipped(filesToUpLoad); FileDB.unlinkFile(filesToUpLoad); - - addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_ZIP_FILE_CREATED, zippedFile); + FileDB.addFiles(file); + FileDB.markFileZipped(filesToUpLoad); + + + addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_ZIP_FILE_CREATED, file); }) .catch((error) => { addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_ZIP_FILE_CREATE_ERROR); @@ -87,9 +95,9 @@ export const sendImagesToServer = async () => { if (!isImageSyncEnabled) return; - const zipFiles = FileDB.getFiles((files) => files.mimeType === MimeTypes.ZIP); - - if(zipFiles.length === 0){ + const zipFiles = FileDB.getFiles((files) => files.mimeType === MimeTypes.ZIP && files.type === 'IMAGES'); + console.log('zipFiles', zipFiles.length, "images") + if (zipFiles.length === 0) { prepareImagesForUpload(); return; } @@ -107,87 +115,9 @@ export const sendImagesToServer = async () => { if (fileToTry) { - getPreSignedUrl(fileToTry.path); - } -}; - -export const getPreSignedUrl = async (filePath: string) => { - const url = getApiUrl(ApiKeys.GET_PRE_SIGNED_URL, { agentID: GLOBAL.AGENT_ID, deviceID: GLOBAL.DEVICE_ID, dataSyncType: 'IMAGES' }) - axiosInstance - .get(url) - .then((response) => { - if (response.status === API_STATUS_CODE.OK) { - uploadFileTos3(response.data, filePath); - addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_ZIP_UPLOAD_PRESIGNED); - } - }) - .catch((error) => { - logError(error, 'Error getting presigned url'); - addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_ZIP_UPLOAD_PRESIGNED_ERROR); - }); -}; - -export const uploadFileTos3 = async (object: any, filePath: string) => { - - if(!object.preSignedUrl) return; - - try { - const response = await RNFS.uploadFiles({ - toUrl: object.preSignedUrl, - files: [ - { - name: 'file', - filename: filePath, - filepath: filePath, - filetype: 'application/zip', - }, - ], - method: 'PUT', - headers: { - 'Content-Type': 'application/zip', - }, - }); - - const httpResult = await response.promise; - - - if (httpResult?.statusCode === 200) { - addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_ZIP_UPLOADED); - sendAckToServer(filePath, object); - // Handle successful upload - } else { - logError(new Error("Error in api uploading"), 'Error uploading file'); - addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_ZIP_UPLOADED_ERROR); - } - } catch (error) { - logError(error, 'Error uploading file'); - addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_ZIP_UPLOADED_ERROR); + getPreSignedUrl(fileToTry.path, 'IMAGES'); } }; -export const sendAckToServer = async (filePath: string, object: any) => { - - const file = filesStore[filePath] - const url = getApiUrl(ApiKeys.SEND_UPLOAD_ACK, { requestId: object.requestId }); - axiosInstance - .put(url, { - "syncSize": file.size, - "syncStartOffset": file?.startOffset, - "syncEndOffset": file?.endOffset, - }) - .then((response) => { - if (response.status === API_STATUS_CODE.OK) { - const lastSyncTime = Date.now(); - FileDB.unlinkFile(filePath); - setAsyncStorageItem(LocalStorageKeys.IMAGE_SYNC_TIME, lastSyncTime.toString()); - addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_IMAGE_SYNC_ACK); - } - }) - .catch((error) => { - logError(error, 'Error sending ack to server'); - addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_IMAGE_SYNC_ACK_ERROR); - }); -}; - diff --git a/src/services/videoSyncService.ts b/src/services/videoSyncService.ts new file mode 100644 index 00000000..38ffd3db --- /dev/null +++ b/src/services/videoSyncService.ts @@ -0,0 +1,95 @@ +import { CLICKSTREAM_EVENT_NAMES, LocalStorageKeys } from "@common/Constants"; +import { zipVideosForServer } from "@components/utlis/ImageUtlis"; +import { getAsyncStorageItem } from "@components/utlis/commonFunctions"; +import { logError } from "@components/utlis/errorUtils"; +import RNFS from 'react-native-fs'; +import { FileDB, FileEntry, MimeTypes, mimeTypes } from "./ImageProcessor"; +import { addClickstreamEvent } from "./clickstreamEventService"; +import { getPreSignedUrl } from "./deviceDataSyncService"; +import { DATA_BUFFER_SIZE, minutesAgo } from "./imageSyncService"; + +export const prepareVideosForUpload = async () => { + console.log("Videos preparing for upload") + const files = FileDB.getFiles((file)=> !file.zipped && mimeTypes[MimeTypes.VIDEO].includes(file.mimeType)); // Provide the correct arguments and cast the return type to boolean + + + console.log(files, "videos to upload") + + const currentTime = Date.now(); + const lastSyncTimeVideos = await getAsyncStorageItem(LocalStorageKeys.LAST_VIDEO_SYNC_TIME, true) ?? 0; + console.log(files, "videos", lastSyncTimeVideos, "lastSyncTimeVideos") + const shouldConsiderUpload = files.length > 0; + + console.log(shouldConsiderUpload, currentTime , lastSyncTimeVideos , minutesAgo(10), "shouldConsiderUpload videos") + + if (shouldConsiderUpload) { + const filesToUpLoad: FileEntry[] = []; + let filesToUploadSize = 0; + for (let i = 0; i < files.length; i++) { + const file = files[i]; + if (filesToUploadSize + file.size < DATA_BUFFER_SIZE) { + filesToUpLoad.push(file); + } + filesToUploadSize += file.size; + } + console.log(filesToUpLoad, "filesToUpLoad vidoes") + filesToUpLoad.sort((a, b) => a.createdAt - b.createdAt); + zipVideosForServer(filesToUpLoad) + .then((zippedFile) => { + const file = { ...zippedFile, startOffset: filesToUpLoad[0].createdAt, endOffset: filesToUpLoad[filesToUpLoad.length - 1].createdAt, type : 'VIDEOS' } + // sort files based on createdAt + FileDB.unlinkFile(filesToUpLoad); + + FileDB.markFileZipped(filesToUpLoad); + + + FileDB.addFiles(file); + + + + addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_ZIP_FILE_CREATED, zippedFile); + }) + .catch((error) => { + addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_ZIP_FILE_CREATE_ERROR); + logError(error, 'Error zipping files'); + }); + } + +}; + + +export const sendVideosToServer = async () => { + // check if there are any files to upload + console.log('zipFiles', "videos") + + const isImageSyncEnabled = await getAsyncStorageItem(LocalStorageKeys.IS_IMAGE_SYNC_ALLOWED, true) ?? false; + console.log('zipFiles', "videos", isImageSyncEnabled) + + if (!isImageSyncEnabled) return; + + const zipFiles = FileDB.getFiles((files) => files.mimeType === MimeTypes.ZIP && files.type === 'VIDEOS'); + + + console.log('zipFiles', zipFiles.length, "videos") + + if (zipFiles.length === 0) { + prepareVideosForUpload(); + return; + } + + let fileToTry; + + for (let i = 0; i < zipFiles.length; i++) { + const file = zipFiles[i]; + + if (await RNFS.exists(file.path)) { + fileToTry = file; + break; + } + } + + + if (fileToTry) { + getPreSignedUrl(fileToTry.path , 'VIDEOS'); + } +}; \ No newline at end of file