react-native-ensys-camera-x 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CustomCamera.podspec +20 -0
- package/LICENSE +20 -0
- package/README.md +833 -0
- package/android/build.gradle +135 -0
- package/android/consumer-rules.pro +47 -0
- package/android/src/main/AndroidManifest.xml +72 -0
- package/android/src/main/java/android/print/PdfPrint.java +51 -0
- package/android/src/main/java/com/customcamera/AnimatedImageViewManager.kt +199 -0
- package/android/src/main/java/com/customcamera/AudioPlayerView.kt +322 -0
- package/android/src/main/java/com/customcamera/AudioRecorder.kt +506 -0
- package/android/src/main/java/com/customcamera/BarcodeScannerActivity.kt +361 -0
- package/android/src/main/java/com/customcamera/BlurBackgroundView.kt +199 -0
- package/android/src/main/java/com/customcamera/CacheCleanupManager.kt +386 -0
- package/android/src/main/java/com/customcamera/CameraAspectRatioMode.kt +77 -0
- package/android/src/main/java/com/customcamera/CameraAsset.kt +211 -0
- package/android/src/main/java/com/customcamera/CameraTheme.kt +479 -0
- package/android/src/main/java/com/customcamera/CompressionCacheStore.kt +504 -0
- package/android/src/main/java/com/customcamera/CompressionFileNameUtils.kt +7 -0
- package/android/src/main/java/com/customcamera/CompressionProfile.kt +195 -0
- package/android/src/main/java/com/customcamera/CustomCameraActivity.kt +7291 -0
- package/android/src/main/java/com/customcamera/CustomCameraFileProvider.kt +9 -0
- package/android/src/main/java/com/customcamera/CustomCameraModule.kt +2479 -0
- package/android/src/main/java/com/customcamera/CustomCameraPackage.kt +53 -0
- package/android/src/main/java/com/customcamera/DisplayUtils.kt +9 -0
- package/android/src/main/java/com/customcamera/DocumentPreviewView.kt +846 -0
- package/android/src/main/java/com/customcamera/DrawingView.kt +214 -0
- package/android/src/main/java/com/customcamera/EmojiPickerData.kt +228 -0
- package/android/src/main/java/com/customcamera/FilterStripAdapter.kt +158 -0
- package/android/src/main/java/com/customcamera/GifSupport.kt +215 -0
- package/android/src/main/java/com/customcamera/GlobalAudioPlayer.kt +320 -0
- package/android/src/main/java/com/customcamera/ImageExporter.kt +1394 -0
- package/android/src/main/java/com/customcamera/ImageFormatSupport.kt +163 -0
- package/android/src/main/java/com/customcamera/Localization.kt +206 -0
- package/android/src/main/java/com/customcamera/MediaAdapters.kt +758 -0
- package/android/src/main/java/com/customcamera/MediaCacheManager.kt +782 -0
- package/android/src/main/java/com/customcamera/MediaExportFormats.kt +201 -0
- package/android/src/main/java/com/customcamera/MediaFileIo.kt +310 -0
- package/android/src/main/java/com/customcamera/MediaMetadataStore.kt +494 -0
- package/android/src/main/java/com/customcamera/MediaPreviewEditor.kt +14865 -0
- package/android/src/main/java/com/customcamera/MediaPublisher.kt +423 -0
- package/android/src/main/java/com/customcamera/MediaStorageWorkflow.kt +722 -0
- package/android/src/main/java/com/customcamera/MediaStoreRepository.kt +378 -0
- package/android/src/main/java/com/customcamera/MediaThumbnailView.kt +286 -0
- package/android/src/main/java/com/customcamera/MediaUtils.kt +37 -0
- package/android/src/main/java/com/customcamera/NativeVideoViewManager.kt +845 -0
- package/android/src/main/java/com/customcamera/OfficeDocumentConverter.kt +2791 -0
- package/android/src/main/java/com/customcamera/PendingCaptureCrops.kt +88 -0
- package/android/src/main/java/com/customcamera/PowerPointPreview.kt +4077 -0
- package/android/src/main/java/com/customcamera/RecentPickerStore.kt +74 -0
- package/android/src/main/java/com/customcamera/SecureFileEraser.kt +411 -0
- package/android/src/main/java/com/customcamera/SecureKeyModule.kt +88 -0
- package/android/src/main/java/com/customcamera/SecureMediaDeleter.kt +493 -0
- package/android/src/main/java/com/customcamera/StickerPickerData.kt +107 -0
- package/android/src/main/java/com/customcamera/SvgDrawable.kt +175 -0
- package/android/src/main/java/com/customcamera/SvgIconViewManager.kt +77 -0
- package/android/src/main/java/com/customcamera/TextEntryController.kt +553 -0
- package/android/src/main/java/com/customcamera/ThumbnailLoader.kt +455 -0
- package/android/src/main/java/com/customcamera/UploadService.kt +909 -0
- package/android/src/main/java/com/customcamera/VideoEditUtils.kt +210 -0
- package/android/src/main/java/com/customcamera/VideoRotationTranscoder.kt +1992 -0
- package/android/src/main/java/com/customcamera/gif/AnimatedGifEncoder.kt +473 -0
- package/android/src/main/java/com/customcamera/gif/GifDecoder.kt +690 -0
- package/android/src/main/java/com/customcamera/gif/GifFramesDrawable.kt +160 -0
- package/android/src/main/java/com/customcamera/gif/GifHttp.kt +112 -0
- package/android/src/main/java/com/customcamera/gif/GifImageLoader.kt +186 -0
- package/android/src/main/java/com/customcamera/gif/GifModels.kt +74 -0
- package/android/src/main/java/com/customcamera/gif/GifProvider.kt +49 -0
- package/android/src/main/java/com/customcamera/gif/GifProviders.kt +27 -0
- package/android/src/main/java/com/customcamera/gif/GifRepository.kt +124 -0
- package/android/src/main/java/com/customcamera/gif/GifSafety.kt +33 -0
- package/android/src/main/java/com/customcamera/gif/GiphyGifProvider.kt +146 -0
- package/android/src/main/java/com/customcamera/gif/LZWEncoder.kt +258 -0
- package/android/src/main/java/com/customcamera/gif/NeuQuant.kt +435 -0
- package/android/src/main/java/com/customcamera/gif/TenorGifProvider.kt +147 -0
- package/android/src/main/res/drawable/bg_brush_selected.xml +5 -0
- package/android/src/main/res/drawable/bg_color_swatch.xml +5 -0
- package/android/src/main/res/drawable/bg_editor_toolbar.xml +6 -0
- package/android/src/main/res/drawable/bg_icon_circle.xml +5 -0
- package/android/src/main/res/drawable/bg_icon_circle_light.xml +5 -0
- package/android/src/main/res/drawable/bg_mode_tab_active.xml +6 -0
- package/android/src/main/res/drawable/bg_pill.xml +6 -0
- package/android/src/main/res/drawable/bg_recording_dot.xml +6 -0
- package/android/src/main/res/drawable/bg_selection_badge.xml +8 -0
- package/android/src/main/res/drawable/bg_send_button.xml +5 -0
- package/android/src/main/res/drawable/bg_shutter_inner.xml +5 -0
- package/android/src/main/res/drawable/bg_shutter_recording.xml +6 -0
- package/android/src/main/res/drawable/bg_shutter_ring.xml +8 -0
- package/android/src/main/res/drawable/bg_shutter_video_idle.xml +5 -0
- package/android/src/main/res/drawable/bg_thumbnail.xml +9 -0
- package/android/src/main/res/drawable/ic_aspect_ratio.xml +17 -0
- package/android/src/main/res/drawable/ic_audio.xml +10 -0
- package/android/src/main/res/drawable/ic_back.xml +10 -0
- package/android/src/main/res/drawable/ic_brush_medium.xml +12 -0
- package/android/src/main/res/drawable/ic_brush_thick.xml +12 -0
- package/android/src/main/res/drawable/ic_brush_thin.xml +12 -0
- package/android/src/main/res/drawable/ic_check.xml +17 -0
- package/android/src/main/res/drawable/ic_chevron_down.xml +10 -0
- package/android/src/main/res/drawable/ic_chevron_up.xml +13 -0
- package/android/src/main/res/drawable/ic_clear_drawing.xml +10 -0
- package/android/src/main/res/drawable/ic_close.xml +10 -0
- package/android/src/main/res/drawable/ic_cover.xml +9 -0
- package/android/src/main/res/drawable/ic_crop.xml +10 -0
- package/android/src/main/res/drawable/ic_delete.xml +9 -0
- package/android/src/main/res/drawable/ic_document.xml +10 -0
- package/android/src/main/res/drawable/ic_done.xml +10 -0
- package/android/src/main/res/drawable/ic_download.xml +10 -0
- package/android/src/main/res/drawable/ic_emoji.xml +10 -0
- package/android/src/main/res/drawable/ic_filter.xml +10 -0
- package/android/src/main/res/drawable/ic_flash_auto.xml +7 -0
- package/android/src/main/res/drawable/ic_flash_off.xml +23 -0
- package/android/src/main/res/drawable/ic_flash_on.xml +16 -0
- package/android/src/main/res/drawable/ic_flip.xml +15 -0
- package/android/src/main/res/drawable/ic_gallery.xml +10 -0
- package/android/src/main/res/drawable/ic_gif.xml +24 -0
- package/android/src/main/res/drawable/ic_lock.xml +19 -0
- package/android/src/main/res/drawable/ic_lock_open.xml +22 -0
- package/android/src/main/res/drawable/ic_marker.xml +10 -0
- package/android/src/main/res/drawable/ic_more_vert.xml +18 -0
- package/android/src/main/res/drawable/ic_overlay_close.xml +19 -0
- package/android/src/main/res/drawable/ic_overlay_resize.xml +18 -0
- package/android/src/main/res/drawable/ic_pause.xml +10 -0
- package/android/src/main/res/drawable/ic_photo_mode.xml +10 -0
- package/android/src/main/res/drawable/ic_play.xml +10 -0
- package/android/src/main/res/drawable/ic_redo.xml +10 -0
- package/android/src/main/res/drawable/ic_rotate.xml +9 -0
- package/android/src/main/res/drawable/ic_send.xml +10 -0
- package/android/src/main/res/drawable/ic_share.xml +10 -0
- package/android/src/main/res/drawable/ic_stat_upload.xml +10 -0
- package/android/src/main/res/drawable/ic_sticker.xml +24 -0
- package/android/src/main/res/drawable/ic_text.xml +10 -0
- package/android/src/main/res/drawable/ic_text_align_center.xml +11 -0
- package/android/src/main/res/drawable/ic_text_align_left.xml +11 -0
- package/android/src/main/res/drawable/ic_text_align_right.xml +11 -0
- package/android/src/main/res/drawable/ic_text_bg.xml +11 -0
- package/android/src/main/res/drawable/ic_trim.xml +23 -0
- package/android/src/main/res/drawable/ic_undo.xml +10 -0
- package/android/src/main/res/drawable/ic_video_mode.xml +10 -0
- package/android/src/main/res/drawable/ic_video_note.xml +13 -0
- package/android/src/main/res/drawable/ic_volume_off.xml +9 -0
- package/android/src/main/res/drawable/ic_volume_on.xml +9 -0
- package/android/src/main/res/drawable/sticker_heart.xml +9 -0
- package/android/src/main/res/drawable/sticker_smile.xml +9 -0
- package/android/src/main/res/drawable/sticker_star.xml +9 -0
- package/android/src/main/res/drawable/sticker_thumbsup.xml +9 -0
- package/android/src/main/res/values/colors.xml +34 -0
- package/android/src/main/res/values/ids.xml +15 -0
- package/android/src/main/res/values/strings.xml +15 -0
- package/android/src/main/res/values/styles.xml +20 -0
- package/android/src/main/res/xml/file_paths.xml +34 -0
- package/ios/CustomCamera.h +5 -0
- package/ios/CustomCamera.mm +214 -0
- package/lib/module/AudioPlayerContext.js +233 -0
- package/lib/module/AudioPlayerContext.js.map +1 -0
- package/lib/module/CameraPreview.js +304 -0
- package/lib/module/CameraPreview.js.map +1 -0
- package/lib/module/NativeAnimatedImageView.js +41 -0
- package/lib/module/NativeAnimatedImageView.js.map +1 -0
- package/lib/module/NativeAudioPlayerView.js +33 -0
- package/lib/module/NativeAudioPlayerView.js.map +1 -0
- package/lib/module/NativeBlurView.js +35 -0
- package/lib/module/NativeBlurView.js.map +1 -0
- package/lib/module/NativeCustomCamera.js +128 -0
- package/lib/module/NativeCustomCamera.js.map +1 -0
- package/lib/module/NativeCustomCamera.web.js +143 -0
- package/lib/module/NativeCustomCamera.web.js.map +1 -0
- package/lib/module/NativeCustomVideoView.js +5 -0
- package/lib/module/NativeCustomVideoView.js.map +1 -0
- package/lib/module/NativeDocumentPreviewView.js +156 -0
- package/lib/module/NativeDocumentPreviewView.js.map +1 -0
- package/lib/module/NativeMediaThumbnailView.js +110 -0
- package/lib/module/NativeMediaThumbnailView.js.map +1 -0
- package/lib/module/NativeSvgIcon.js +32 -0
- package/lib/module/NativeSvgIcon.js.map +1 -0
- package/lib/module/ResponsivePreviewContainer.js +171 -0
- package/lib/module/ResponsivePreviewContainer.js.map +1 -0
- package/lib/module/accessibility.js +61 -0
- package/lib/module/accessibility.js.map +1 -0
- package/lib/module/config.js +352 -0
- package/lib/module/config.js.map +1 -0
- package/lib/module/cpuOptimization.js +296 -0
- package/lib/module/cpuOptimization.js.map +1 -0
- package/lib/module/db/DatabaseManager.js +191 -0
- package/lib/module/db/DatabaseManager.js.map +1 -0
- package/lib/module/db/MediaMapper.js +284 -0
- package/lib/module/db/MediaMapper.js.map +1 -0
- package/lib/module/db/MediaRepository.js +242 -0
- package/lib/module/db/MediaRepository.js.map +1 -0
- package/lib/module/db/index.js +41 -0
- package/lib/module/db/index.js.map +1 -0
- package/lib/module/db/migrations.js +180 -0
- package/lib/module/db/migrations.js.map +1 -0
- package/lib/module/db/react-native-sqlcipher.d.js +2 -0
- package/lib/module/db/react-native-sqlcipher.d.js.map +1 -0
- package/lib/module/db/secureKey.js +87 -0
- package/lib/module/db/secureKey.js.map +1 -0
- package/lib/module/db/types.js +57 -0
- package/lib/module/db/types.js.map +1 -0
- package/lib/module/db/useMediaDatabase.js +123 -0
- package/lib/module/db/useMediaDatabase.js.map +1 -0
- package/lib/module/defaultIcons.js +2259 -0
- package/lib/module/defaultIcons.js.map +1 -0
- package/lib/module/deviceCapabilities.js +408 -0
- package/lib/module/deviceCapabilities.js.map +1 -0
- package/lib/module/errors.js +267 -0
- package/lib/module/errors.js.map +1 -0
- package/lib/module/iconConfig.js +796 -0
- package/lib/module/iconConfig.js.map +1 -0
- package/lib/module/icons.js +514 -0
- package/lib/module/icons.js.map +1 -0
- package/lib/module/index.js +251 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/jest-globals.d.js +2 -0
- package/lib/module/jest-globals.d.js.map +1 -0
- package/lib/module/library/MediaLibrary.js +107 -0
- package/lib/module/library/MediaLibrary.js.map +1 -0
- package/lib/module/library/MediaLibraryScreen.js +2597 -0
- package/lib/module/library/MediaLibraryScreen.js.map +1 -0
- package/lib/module/library/components/AudioPreview.js +74 -0
- package/lib/module/library/components/AudioPreview.js.map +1 -0
- package/lib/module/library/components/DocumentPreview.js +331 -0
- package/lib/module/library/components/DocumentPreview.js.map +1 -0
- package/lib/module/library/components/ExpandableFab.js +343 -0
- package/lib/module/library/components/ExpandableFab.js.map +1 -0
- package/lib/module/library/components/FileTiles.js +251 -0
- package/lib/module/library/components/FileTiles.js.map +1 -0
- package/lib/module/library/components/ImagePreview.js +70 -0
- package/lib/module/library/components/ImagePreview.js.map +1 -0
- package/lib/module/library/components/MediaMetadataHeader.js +175 -0
- package/lib/module/library/components/MediaMetadataHeader.js.map +1 -0
- package/lib/module/library/components/RecordAudioScreen.js +816 -0
- package/lib/module/library/components/RecordAudioScreen.js.map +1 -0
- package/lib/module/library/components/VideoPreview.js +453 -0
- package/lib/module/library/components/VideoPreview.js.map +1 -0
- package/lib/module/library/components/ViewerCounter.js +55 -0
- package/lib/module/library/components/ViewerCounter.js.map +1 -0
- package/lib/module/library/compressionEstimate.js +507 -0
- package/lib/module/library/compressionEstimate.js.map +1 -0
- package/lib/module/library/fileMetadata.js +196 -0
- package/lib/module/library/fileMetadata.js.map +1 -0
- package/lib/module/library/index.js +42 -0
- package/lib/module/library/index.js.map +1 -0
- package/lib/module/library/libraryIconRegistry.js +69 -0
- package/lib/module/library/libraryIconRegistry.js.map +1 -0
- package/lib/module/library/libraryIcons.js +858 -0
- package/lib/module/library/libraryIcons.js.map +1 -0
- package/lib/module/library/localizedFormat.js +181 -0
- package/lib/module/library/localizedFormat.js.map +1 -0
- package/lib/module/library/palette.js +225 -0
- package/lib/module/library/palette.js.map +1 -0
- package/lib/module/library/responsive.js +53 -0
- package/lib/module/library/responsive.js.map +1 -0
- package/lib/module/locales.js +416 -0
- package/lib/module/locales.js.map +1 -0
- package/lib/module/localization.js +1064 -0
- package/lib/module/localization.js.map +1 -0
- package/lib/module/logger.js +20 -0
- package/lib/module/logger.js.map +1 -0
- package/lib/module/lowMemoryCache.js +206 -0
- package/lib/module/lowMemoryCache.js.map +1 -0
- package/lib/module/mediaHistory.js +33 -0
- package/lib/module/mediaHistory.js.map +1 -0
- package/lib/module/mediaStorageService.js +402 -0
- package/lib/module/mediaStorageService.js.map +1 -0
- package/lib/module/memory.js +79 -0
- package/lib/module/memory.js.map +1 -0
- package/lib/module/nativeActions.js +307 -0
- package/lib/module/nativeActions.js.map +1 -0
- package/lib/module/nativeComponentSupport.js +61 -0
- package/lib/module/nativeComponentSupport.js.map +1 -0
- package/lib/module/networkPerformance.js +24 -0
- package/lib/module/networkPerformance.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/performance.js +104 -0
- package/lib/module/performance.js.map +1 -0
- package/lib/module/performanceMonitor.js +155 -0
- package/lib/module/performanceMonitor.js.map +1 -0
- package/lib/module/permissions.js +370 -0
- package/lib/module/permissions.js.map +1 -0
- package/lib/module/rendering.js +40 -0
- package/lib/module/rendering.js.map +1 -0
- package/lib/module/security.js +18 -0
- package/lib/module/security.js.map +1 -0
- package/lib/module/state.js +266 -0
- package/lib/module/state.js.map +1 -0
- package/lib/module/storage.js +400 -0
- package/lib/module/storage.js.map +1 -0
- package/lib/module/theme.js +390 -0
- package/lib/module/theme.js.map +1 -0
- package/lib/module/types.js +4 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/utils.js +115 -0
- package/lib/module/utils.js.map +1 -0
- package/lib/module/validation.js +288 -0
- package/lib/module/validation.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/AudioPlayerContext.d.ts +27 -0
- package/lib/typescript/src/AudioPlayerContext.d.ts.map +1 -0
- package/lib/typescript/src/CameraPreview.d.ts +13 -0
- package/lib/typescript/src/CameraPreview.d.ts.map +1 -0
- package/lib/typescript/src/NativeAnimatedImageView.d.ts +33 -0
- package/lib/typescript/src/NativeAnimatedImageView.d.ts.map +1 -0
- package/lib/typescript/src/NativeAudioPlayerView.d.ts +12 -0
- package/lib/typescript/src/NativeAudioPlayerView.d.ts.map +1 -0
- package/lib/typescript/src/NativeBlurView.d.ts +12 -0
- package/lib/typescript/src/NativeBlurView.d.ts.map +1 -0
- package/lib/typescript/src/NativeCustomCamera.d.ts +167 -0
- package/lib/typescript/src/NativeCustomCamera.d.ts.map +1 -0
- package/lib/typescript/src/NativeCustomCamera.web.d.ts +47 -0
- package/lib/typescript/src/NativeCustomCamera.web.d.ts.map +1 -0
- package/lib/typescript/src/NativeCustomVideoView.d.ts +30 -0
- package/lib/typescript/src/NativeCustomVideoView.d.ts.map +1 -0
- package/lib/typescript/src/NativeDocumentPreviewView.d.ts +48 -0
- package/lib/typescript/src/NativeDocumentPreviewView.d.ts.map +1 -0
- package/lib/typescript/src/NativeMediaThumbnailView.d.ts +54 -0
- package/lib/typescript/src/NativeMediaThumbnailView.d.ts.map +1 -0
- package/lib/typescript/src/NativeSvgIcon.d.ts +25 -0
- package/lib/typescript/src/NativeSvgIcon.d.ts.map +1 -0
- package/lib/typescript/src/ResponsivePreviewContainer.d.ts +92 -0
- package/lib/typescript/src/ResponsivePreviewContainer.d.ts.map +1 -0
- package/lib/typescript/src/accessibility.d.ts +20 -0
- package/lib/typescript/src/accessibility.d.ts.map +1 -0
- package/lib/typescript/src/config.d.ts +145 -0
- package/lib/typescript/src/config.d.ts.map +1 -0
- package/lib/typescript/src/cpuOptimization.d.ts +63 -0
- package/lib/typescript/src/cpuOptimization.d.ts.map +1 -0
- package/lib/typescript/src/db/DatabaseManager.d.ts +48 -0
- package/lib/typescript/src/db/DatabaseManager.d.ts.map +1 -0
- package/lib/typescript/src/db/MediaMapper.d.ts +77 -0
- package/lib/typescript/src/db/MediaMapper.d.ts.map +1 -0
- package/lib/typescript/src/db/MediaRepository.d.ts +66 -0
- package/lib/typescript/src/db/MediaRepository.d.ts.map +1 -0
- package/lib/typescript/src/db/index.d.ts +23 -0
- package/lib/typescript/src/db/index.d.ts.map +1 -0
- package/lib/typescript/src/db/migrations.d.ts +31 -0
- package/lib/typescript/src/db/migrations.d.ts.map +1 -0
- package/lib/typescript/src/db/secureKey.d.ts +10 -0
- package/lib/typescript/src/db/secureKey.d.ts.map +1 -0
- package/lib/typescript/src/db/types.d.ts +115 -0
- package/lib/typescript/src/db/types.d.ts.map +1 -0
- package/lib/typescript/src/db/useMediaDatabase.d.ts +41 -0
- package/lib/typescript/src/db/useMediaDatabase.d.ts.map +1 -0
- package/lib/typescript/src/defaultIcons.d.ts +139 -0
- package/lib/typescript/src/defaultIcons.d.ts.map +1 -0
- package/lib/typescript/src/deviceCapabilities.d.ts +92 -0
- package/lib/typescript/src/deviceCapabilities.d.ts.map +1 -0
- package/lib/typescript/src/errors.d.ts +95 -0
- package/lib/typescript/src/errors.d.ts.map +1 -0
- package/lib/typescript/src/iconConfig.d.ts +176 -0
- package/lib/typescript/src/iconConfig.d.ts.map +1 -0
- package/lib/typescript/src/icons.d.ts +217 -0
- package/lib/typescript/src/icons.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +104 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/library/MediaLibrary.d.ts +57 -0
- package/lib/typescript/src/library/MediaLibrary.d.ts.map +1 -0
- package/lib/typescript/src/library/MediaLibraryScreen.d.ts +51 -0
- package/lib/typescript/src/library/MediaLibraryScreen.d.ts.map +1 -0
- package/lib/typescript/src/library/components/AudioPreview.d.ts +6 -0
- package/lib/typescript/src/library/components/AudioPreview.d.ts.map +1 -0
- package/lib/typescript/src/library/components/DocumentPreview.d.ts +10 -0
- package/lib/typescript/src/library/components/DocumentPreview.d.ts.map +1 -0
- package/lib/typescript/src/library/components/ExpandableFab.d.ts +39 -0
- package/lib/typescript/src/library/components/ExpandableFab.d.ts.map +1 -0
- package/lib/typescript/src/library/components/FileTiles.d.ts +8 -0
- package/lib/typescript/src/library/components/FileTiles.d.ts.map +1 -0
- package/lib/typescript/src/library/components/ImagePreview.d.ts +5 -0
- package/lib/typescript/src/library/components/ImagePreview.d.ts.map +1 -0
- package/lib/typescript/src/library/components/MediaMetadataHeader.d.ts +16 -0
- package/lib/typescript/src/library/components/MediaMetadataHeader.d.ts.map +1 -0
- package/lib/typescript/src/library/components/RecordAudioScreen.d.ts +8 -0
- package/lib/typescript/src/library/components/RecordAudioScreen.d.ts.map +1 -0
- package/lib/typescript/src/library/components/VideoPreview.d.ts +20 -0
- package/lib/typescript/src/library/components/VideoPreview.d.ts.map +1 -0
- package/lib/typescript/src/library/components/ViewerCounter.d.ts +13 -0
- package/lib/typescript/src/library/components/ViewerCounter.d.ts.map +1 -0
- package/lib/typescript/src/library/compressionEstimate.d.ts +313 -0
- package/lib/typescript/src/library/compressionEstimate.d.ts.map +1 -0
- package/lib/typescript/src/library/fileMetadata.d.ts +33 -0
- package/lib/typescript/src/library/fileMetadata.d.ts.map +1 -0
- package/lib/typescript/src/library/index.d.ts +40 -0
- package/lib/typescript/src/library/index.d.ts.map +1 -0
- package/lib/typescript/src/library/libraryIconRegistry.d.ts +35 -0
- package/lib/typescript/src/library/libraryIconRegistry.d.ts.map +1 -0
- package/lib/typescript/src/library/libraryIcons.d.ts +85 -0
- package/lib/typescript/src/library/libraryIcons.d.ts.map +1 -0
- package/lib/typescript/src/library/localizedFormat.d.ts +49 -0
- package/lib/typescript/src/library/localizedFormat.d.ts.map +1 -0
- package/lib/typescript/src/library/palette.d.ts +104 -0
- package/lib/typescript/src/library/palette.d.ts.map +1 -0
- package/lib/typescript/src/library/responsive.d.ts +15 -0
- package/lib/typescript/src/library/responsive.d.ts.map +1 -0
- package/lib/typescript/src/locales.d.ts +103 -0
- package/lib/typescript/src/locales.d.ts.map +1 -0
- package/lib/typescript/src/localization.d.ts +1051 -0
- package/lib/typescript/src/localization.d.ts.map +1 -0
- package/lib/typescript/src/logger.d.ts +4 -0
- package/lib/typescript/src/logger.d.ts.map +1 -0
- package/lib/typescript/src/lowMemoryCache.d.ts +64 -0
- package/lib/typescript/src/lowMemoryCache.d.ts.map +1 -0
- package/lib/typescript/src/mediaHistory.d.ts +3 -0
- package/lib/typescript/src/mediaHistory.d.ts.map +1 -0
- package/lib/typescript/src/mediaStorageService.d.ts +155 -0
- package/lib/typescript/src/mediaStorageService.d.ts.map +1 -0
- package/lib/typescript/src/memory.d.ts +21 -0
- package/lib/typescript/src/memory.d.ts.map +1 -0
- package/lib/typescript/src/nativeActions.d.ts +174 -0
- package/lib/typescript/src/nativeActions.d.ts.map +1 -0
- package/lib/typescript/src/nativeComponentSupport.d.ts +30 -0
- package/lib/typescript/src/nativeComponentSupport.d.ts.map +1 -0
- package/lib/typescript/src/networkPerformance.d.ts +12 -0
- package/lib/typescript/src/networkPerformance.d.ts.map +1 -0
- package/lib/typescript/src/performance.d.ts +32 -0
- package/lib/typescript/src/performance.d.ts.map +1 -0
- package/lib/typescript/src/performanceMonitor.d.ts +45 -0
- package/lib/typescript/src/performanceMonitor.d.ts.map +1 -0
- package/lib/typescript/src/permissions.d.ts +132 -0
- package/lib/typescript/src/permissions.d.ts.map +1 -0
- package/lib/typescript/src/rendering.d.ts +9 -0
- package/lib/typescript/src/rendering.d.ts.map +1 -0
- package/lib/typescript/src/security.d.ts +6 -0
- package/lib/typescript/src/security.d.ts.map +1 -0
- package/lib/typescript/src/state.d.ts +102 -0
- package/lib/typescript/src/state.d.ts.map +1 -0
- package/lib/typescript/src/storage.d.ts +117 -0
- package/lib/typescript/src/storage.d.ts.map +1 -0
- package/lib/typescript/src/theme.d.ts +15 -0
- package/lib/typescript/src/theme.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +802 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/lib/typescript/src/utils.d.ts +50 -0
- package/lib/typescript/src/utils.d.ts.map +1 -0
- package/lib/typescript/src/validation.d.ts +52 -0
- package/lib/typescript/src/validation.d.ts.map +1 -0
- package/package.json +204 -0
- package/react-native.config.js +17 -0
- package/src/AudioPlayerContext.tsx +220 -0
- package/src/CameraPreview.tsx +352 -0
- package/src/NativeAnimatedImageView.tsx +71 -0
- package/src/NativeAudioPlayerView.tsx +39 -0
- package/src/NativeBlurView.tsx +40 -0
- package/src/NativeCustomCamera.ts +305 -0
- package/src/NativeCustomCamera.web.ts +192 -0
- package/src/NativeCustomVideoView.ts +30 -0
- package/src/NativeDocumentPreviewView.tsx +196 -0
- package/src/NativeMediaThumbnailView.tsx +167 -0
- package/src/NativeSvgIcon.tsx +46 -0
- package/src/ResponsivePreviewContainer.tsx +227 -0
- package/src/accessibility.ts +80 -0
- package/src/config.ts +406 -0
- package/src/cpuOptimization.ts +365 -0
- package/src/db/DatabaseManager.ts +212 -0
- package/src/db/MediaMapper.ts +377 -0
- package/src/db/MediaRepository.ts +313 -0
- package/src/db/index.ts +73 -0
- package/src/db/migrations.ts +222 -0
- package/src/db/react-native-sqlcipher.d.ts +76 -0
- package/src/db/secureKey.ts +100 -0
- package/src/db/types.ts +123 -0
- package/src/db/useMediaDatabase.ts +191 -0
- package/src/defaultIcons.tsx +2390 -0
- package/src/deviceCapabilities.ts +438 -0
- package/src/errors.ts +350 -0
- package/src/iconConfig.tsx +962 -0
- package/src/icons.tsx +788 -0
- package/src/index.tsx +763 -0
- package/src/jest-globals.d.ts +9 -0
- package/src/library/MediaLibrary.tsx +141 -0
- package/src/library/MediaLibraryScreen.tsx +3003 -0
- package/src/library/components/AudioPreview.tsx +73 -0
- package/src/library/components/DocumentPreview.tsx +361 -0
- package/src/library/components/ExpandableFab.tsx +342 -0
- package/src/library/components/FileTiles.tsx +252 -0
- package/src/library/components/ImagePreview.tsx +72 -0
- package/src/library/components/MediaMetadataHeader.tsx +208 -0
- package/src/library/components/RecordAudioScreen.tsx +845 -0
- package/src/library/components/VideoPreview.tsx +478 -0
- package/src/library/components/ViewerCounter.tsx +71 -0
- package/src/library/compressionEstimate.ts +662 -0
- package/src/library/fileMetadata.ts +212 -0
- package/src/library/index.ts +66 -0
- package/src/library/libraryIconRegistry.tsx +86 -0
- package/src/library/libraryIcons.tsx +928 -0
- package/src/library/localizedFormat.ts +207 -0
- package/src/library/palette.ts +281 -0
- package/src/library/responsive.ts +49 -0
- package/src/locales.ts +381 -0
- package/src/localization.ts +1947 -0
- package/src/logger.ts +20 -0
- package/src/lowMemoryCache.ts +218 -0
- package/src/mediaHistory.ts +42 -0
- package/src/mediaStorageService.ts +546 -0
- package/src/memory.ts +102 -0
- package/src/nativeActions.ts +422 -0
- package/src/nativeComponentSupport.ts +60 -0
- package/src/networkPerformance.ts +30 -0
- package/src/performance.ts +137 -0
- package/src/performanceMonitor.ts +219 -0
- package/src/permissions.ts +403 -0
- package/src/rendering.ts +47 -0
- package/src/security.ts +19 -0
- package/src/state.ts +333 -0
- package/src/storage.ts +453 -0
- package/src/theme.tsx +467 -0
- package/src/types.ts +946 -0
- package/src/utils.ts +177 -0
- package/src/validation.ts +372 -0
|
@@ -0,0 +1,782 @@
|
|
|
1
|
+
package com.customcamera
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.graphics.BitmapFactory
|
|
5
|
+
import android.media.MediaMetadataRetriever
|
|
6
|
+
import android.net.Uri
|
|
7
|
+
import android.os.Build
|
|
8
|
+
import android.provider.MediaStore
|
|
9
|
+
import androidx.core.content.FileProvider
|
|
10
|
+
import android.util.Log
|
|
11
|
+
import java.io.File
|
|
12
|
+
import java.io.IOException
|
|
13
|
+
import java.text.SimpleDateFormat
|
|
14
|
+
import java.util.Date
|
|
15
|
+
import java.util.Locale
|
|
16
|
+
import java.util.TimeZone
|
|
17
|
+
import java.util.concurrent.ConcurrentHashMap
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* MediaCacheManager handles all media caching operations.
|
|
21
|
+
*
|
|
22
|
+
* Key features:
|
|
23
|
+
* - Stores captured/selected media in app cache (not gallery)
|
|
24
|
+
* - Maintains metadata for each cached item
|
|
25
|
+
* - Supports overwriting edited versions (no version history)
|
|
26
|
+
* - Provides cache cleanup and management
|
|
27
|
+
* - Thread-safe operations
|
|
28
|
+
*/
|
|
29
|
+
class MediaCacheManager private constructor(private val context: Context) {
|
|
30
|
+
|
|
31
|
+
companion object {
|
|
32
|
+
private const val TAG = "MediaCacheManager"
|
|
33
|
+
private const val CACHE_DIR_NAME = "camera_media_cache"
|
|
34
|
+
private const val METADATA_FILE_NAME = "cache_metadata.json"
|
|
35
|
+
private const val MAX_CACHE_SIZE_MB = 500L // Maximum cache size in MB
|
|
36
|
+
|
|
37
|
+
@Volatile
|
|
38
|
+
private var instance: MediaCacheManager? = null
|
|
39
|
+
|
|
40
|
+
fun getInstance(context: Context): MediaCacheManager {
|
|
41
|
+
return instance ?: synchronized(this) {
|
|
42
|
+
instance ?: MediaCacheManager(context.applicationContext).also { instance = it }
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Cache directory for storing media files
|
|
48
|
+
private val cacheDir: File by lazy {
|
|
49
|
+
File(context.cacheDir, CACHE_DIR_NAME).apply {
|
|
50
|
+
if (!exists()) {
|
|
51
|
+
mkdirs()
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Metadata file for tracking cached items
|
|
57
|
+
private val metadataFile: File by lazy {
|
|
58
|
+
File(cacheDir, METADATA_FILE_NAME)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// In-memory cache of metadata for fast access
|
|
62
|
+
private val metadataCache = ConcurrentHashMap<String, MediaCacheMetadata>()
|
|
63
|
+
|
|
64
|
+
// Date format for generating unique identifiers
|
|
65
|
+
private val timestampFormat = SimpleDateFormat("yyyyMMdd_HHmmss_SSS", Locale.US).apply {
|
|
66
|
+
timeZone = TimeZone.getTimeZone("UTC")
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Metadata for a cached media item
|
|
71
|
+
*/
|
|
72
|
+
data class MediaCacheMetadata(
|
|
73
|
+
val id: String,
|
|
74
|
+
val fileName: String,
|
|
75
|
+
val cachePath: String,
|
|
76
|
+
val fileType: String, // "photo" or "video"
|
|
77
|
+
val fileSize: Long,
|
|
78
|
+
val width: Int?,
|
|
79
|
+
val height: Int?,
|
|
80
|
+
val duration: Long?, // For videos in milliseconds
|
|
81
|
+
val mimeType: String?,
|
|
82
|
+
val createdAt: Long,
|
|
83
|
+
val lastModified: Long,
|
|
84
|
+
val originalUri: String? = null // Original source URI if from gallery
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
init {
|
|
88
|
+
loadMetadata()
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Load metadata from file into memory
|
|
93
|
+
*/
|
|
94
|
+
private fun loadMetadata() {
|
|
95
|
+
try {
|
|
96
|
+
if (!metadataFile.exists()) {
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
val jsonContent = metadataFile.readText()
|
|
101
|
+
if (jsonContent.isBlank()) {
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Parse JSON and populate metadata cache
|
|
106
|
+
// For simplicity, we'll use a basic JSON parser
|
|
107
|
+
val lines = jsonContent.split("\n")
|
|
108
|
+
for (line in lines) {
|
|
109
|
+
if (line.isNotBlank()) {
|
|
110
|
+
try {
|
|
111
|
+
val metadata = parseMetadataLine(line)
|
|
112
|
+
metadataCache[metadata.id] = metadata
|
|
113
|
+
} catch (e: Exception) {
|
|
114
|
+
Log.w(TAG, "Failed to parse metadata line: $line", e)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
Log.d(TAG, "Loaded ${metadataCache.size} metadata entries")
|
|
120
|
+
} catch (e: Exception) {
|
|
121
|
+
Log.e(TAG, "Failed to load metadata", e)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Save metadata to file
|
|
127
|
+
*/
|
|
128
|
+
private fun saveMetadata() {
|
|
129
|
+
try {
|
|
130
|
+
val jsonContent = StringBuilder()
|
|
131
|
+
for (metadata in metadataCache.values) {
|
|
132
|
+
jsonContent.append(metadataToJsonLine(metadata)).append("\n")
|
|
133
|
+
}
|
|
134
|
+
metadataFile.writeText(jsonContent.toString())
|
|
135
|
+
} catch (e: Exception) {
|
|
136
|
+
Log.e(TAG, "Failed to save metadata", e)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Parse a metadata line from JSON format
|
|
142
|
+
*/
|
|
143
|
+
private fun parseMetadataLine(line: String): MediaCacheMetadata {
|
|
144
|
+
// Simple JSON parsing for our specific format
|
|
145
|
+
val values = mutableMapOf<String, String>()
|
|
146
|
+
|
|
147
|
+
// Extract key-value pairs from JSON-like format
|
|
148
|
+
val pattern = "\"([^\"]+)\":\"?([^\"]+)\"?".toRegex()
|
|
149
|
+
pattern.findAll(line).forEach { match ->
|
|
150
|
+
val key = match.groupValues[1]
|
|
151
|
+
val value = match.groupValues[2].trimEnd(',')
|
|
152
|
+
values[key] = value
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return MediaCacheMetadata(
|
|
156
|
+
id = values["id"] ?: "",
|
|
157
|
+
fileName = values["fileName"] ?: "",
|
|
158
|
+
cachePath = values["cachePath"] ?: "",
|
|
159
|
+
fileType = values["fileType"] ?: "photo",
|
|
160
|
+
fileSize = values["fileSize"]?.toLong() ?: 0L,
|
|
161
|
+
width = values["width"]?.toIntOrNull(),
|
|
162
|
+
height = values["height"]?.toIntOrNull(),
|
|
163
|
+
duration = values["duration"]?.toLongOrNull(),
|
|
164
|
+
mimeType = values["mimeType"],
|
|
165
|
+
createdAt = values["createdAt"]?.toLong() ?: System.currentTimeMillis(),
|
|
166
|
+
lastModified = values["lastModified"]?.toLong() ?: System.currentTimeMillis(),
|
|
167
|
+
originalUri = values["originalUri"]
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Convert metadata to JSON line format
|
|
173
|
+
*/
|
|
174
|
+
private fun metadataToJsonLine(metadata: MediaCacheMetadata): String {
|
|
175
|
+
return """{"id":"${metadata.id}","fileName":"${metadata.fileName}","cachePath":"${metadata.cachePath}","fileType":"${metadata.fileType}","fileSize":${metadata.fileSize},"width":${metadata.width ?: "null"},"height":${metadata.height ?: "null"},"duration":${metadata.duration ?: "null"},"mimeType":"${metadata.mimeType ?: ""}","createdAt":${metadata.createdAt},"lastModified":${metadata.lastModified},"originalUri":"${metadata.originalUri ?: ""}"}"""
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Generate a unique ID based on timestamp
|
|
180
|
+
*/
|
|
181
|
+
fun generateUniqueId(): String {
|
|
182
|
+
return "media_${timestampFormat.format(Date())}"
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Generate a unique filename for cached media.
|
|
187
|
+
*
|
|
188
|
+
* [extension] (with the leading dot) overrides the per-type default. Callers that know the real
|
|
189
|
+
* container — anything copied in from the gallery — pass it so the cache file keeps the source's
|
|
190
|
+
* own format instead of being renamed to .jpg. See [MediaFormat.of].
|
|
191
|
+
*/
|
|
192
|
+
fun generateFileName(fileType: String, id: String? = null, extension: String? = null): String {
|
|
193
|
+
val uniqueId = id ?: generateUniqueId()
|
|
194
|
+
val ext = extension ?: if (fileType == "video") ".mp4" else ".jpg"
|
|
195
|
+
return "${uniqueId}${ext}"
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* The container an incoming file is actually in — extension plus MIME type.
|
|
200
|
+
*
|
|
201
|
+
* Copying media into the cache is a byte-for-byte copy, so the bytes always keep their original
|
|
202
|
+
* format; only the NAME used to be forced to ".jpg" (with a hardcoded "image/jpeg" alongside it)
|
|
203
|
+
* for every non-video. That turned a picked GIF into `media_x.jpg` / image/jpeg the moment it
|
|
204
|
+
* entered the cache, and the lie travelled all the way to the published file and on to JS —
|
|
205
|
+
* consumers then failed to render a "JPEG" whose bytes start with GIF89a. Resolving the format
|
|
206
|
+
* here keeps a GIF a GIF end to end, and does the same for PNG/WebP/HEIC.
|
|
207
|
+
*/
|
|
208
|
+
internal data class MediaFormat(val extension: String, val mimeType: String) {
|
|
209
|
+
companion object {
|
|
210
|
+
/**
|
|
211
|
+
* Resolve the format of [sourceUri] for a [fileType] asset. The MIME type the resolver
|
|
212
|
+
* reports wins (it is derived from the provider's own record); a content:// URI that
|
|
213
|
+
* reports nothing falls back to the extension in its path, then to the type default.
|
|
214
|
+
*
|
|
215
|
+
* The container table lives in [ImageFormatSupport] so that every path that names a file —
|
|
216
|
+
* caching here, publishing in [MediaPublisher], saving from the editor — agrees about what
|
|
217
|
+
* a given format is called. That is what keeps a HEIF a `.heif` and an AVIF an `.avif`
|
|
218
|
+
* instead of quietly becoming `.jpg` on the way into the cache.
|
|
219
|
+
*/
|
|
220
|
+
fun of(context: Context, sourceUri: Uri?, fileType: String): MediaFormat {
|
|
221
|
+
if (fileType == "video") return MediaFormat(".mp4", "video/mp4")
|
|
222
|
+
if (fileType == "audio") return MediaFormat(".m4a", "audio/mp4")
|
|
223
|
+
if (sourceUri == null) return DEFAULT_IMAGE
|
|
224
|
+
|
|
225
|
+
val declaredMime = runCatching { context.contentResolver.getType(sourceUri) }
|
|
226
|
+
.getOrNull()
|
|
227
|
+
?.lowercase(Locale.US)
|
|
228
|
+
ImageFormatSupport.fromMimeType(declaredMime)?.let {
|
|
229
|
+
// Preserve the exact MIME spelling the provider reported (image/heic vs image/heif)
|
|
230
|
+
// rather than normalising it — the bytes are copied verbatim, so the record should be
|
|
231
|
+
// verbatim too.
|
|
232
|
+
return MediaFormat(it.dottedExtension, declaredMime ?: it.mimeType)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
ImageFormatSupport.fromFileName(sourceUri.path)?.let { format ->
|
|
236
|
+
// Keep the extension actually present in the path (.heic stays .heic) and pair it with
|
|
237
|
+
// that container's canonical MIME type.
|
|
238
|
+
val pathExtension = sourceUri.path
|
|
239
|
+
?.substringAfterLast('/', "")
|
|
240
|
+
?.substringAfterLast('.', "")
|
|
241
|
+
?.lowercase(Locale.US)
|
|
242
|
+
?.takeIf { it.isNotEmpty() }
|
|
243
|
+
return MediaFormat(".${pathExtension ?: format.extension}", format.mimeType)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return DEFAULT_IMAGE
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Format of an on-disk [file], read from its extension alone. */
|
|
250
|
+
fun ofFile(file: File, fileType: String): MediaFormat {
|
|
251
|
+
if (fileType == "video") {
|
|
252
|
+
// A video is not automatically an MP4: the preview editor's "Export By" menu can
|
|
253
|
+
// produce a .webm or .3gp, and registering those as video/mp4 would put MP4's name on
|
|
254
|
+
// bytes that are not MP4 — the same class of bug the image branch below exists to
|
|
255
|
+
// prevent. Unrecognised extensions keep the historical MP4 default.
|
|
256
|
+
return MediaExportFormats.videoContainerFromFileName(file.name)
|
|
257
|
+
?.let { MediaFormat(it.dottedExtension, it.mimeType) }
|
|
258
|
+
?: MediaFormat(".mp4", "video/mp4")
|
|
259
|
+
}
|
|
260
|
+
val ext = file.extension.lowercase(Locale.US)
|
|
261
|
+
val format = ImageFormatSupport.fromFileName(file.name) ?: return DEFAULT_IMAGE
|
|
262
|
+
return MediaFormat(".$ext", format.mimeType)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Every camera capture is a JPEG, so that is the fallback for anything unrecognised. */
|
|
266
|
+
private val DEFAULT_IMAGE = MediaFormat(".jpg", "image/jpeg")
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Cache a media file from a source URI
|
|
272
|
+
*
|
|
273
|
+
* @param sourceUri The source URI of the media file
|
|
274
|
+
* @param fileType The type of media ("photo" or "video")
|
|
275
|
+
* @param id Optional custom ID (will generate one if not provided)
|
|
276
|
+
* @param allowMove Set only when the source is a throwaway file this app just wrote and will
|
|
277
|
+
* delete straight after caching — a freshly recorded clip or captured still. It lets the
|
|
278
|
+
* source be MOVED into the cache instead of copied; see [ingestUriToFile].
|
|
279
|
+
* @return MediaCacheMetadata containing cache information
|
|
280
|
+
*/
|
|
281
|
+
fun cacheMedia(
|
|
282
|
+
sourceUri: Uri,
|
|
283
|
+
fileType: String,
|
|
284
|
+
id: String? = null,
|
|
285
|
+
allowMove: Boolean = false
|
|
286
|
+
): MediaCacheMetadata {
|
|
287
|
+
val uniqueId = id ?: generateUniqueId()
|
|
288
|
+
// Keep the source's real container (a picked GIF stays a .gif / image/gif) — the copy below
|
|
289
|
+
// is byte-for-byte, so naming it anything else would misdescribe the bytes on disk.
|
|
290
|
+
val format = MediaFormat.of(context, sourceUri, fileType)
|
|
291
|
+
val fileName = generateFileName(fileType, uniqueId, format.extension)
|
|
292
|
+
val cacheFile = File(cacheDir, fileName)
|
|
293
|
+
|
|
294
|
+
// Bring the file into the cache — moved when the caller owns the source, copied otherwise.
|
|
295
|
+
ingestUriToFile(sourceUri, cacheFile, allowMove)
|
|
296
|
+
|
|
297
|
+
// Extract metadata
|
|
298
|
+
val fileSize = cacheFile.length()
|
|
299
|
+
val (width, height, duration) = extractMediaMetadata(cacheFile, fileType)
|
|
300
|
+
val mimeType = format.mimeType
|
|
301
|
+
|
|
302
|
+
// Create metadata
|
|
303
|
+
val metadata = MediaCacheMetadata(
|
|
304
|
+
id = uniqueId,
|
|
305
|
+
fileName = fileName,
|
|
306
|
+
cachePath = cacheFile.absolutePath,
|
|
307
|
+
fileType = fileType,
|
|
308
|
+
fileSize = fileSize,
|
|
309
|
+
width = width,
|
|
310
|
+
height = height,
|
|
311
|
+
duration = duration,
|
|
312
|
+
mimeType = mimeType,
|
|
313
|
+
createdAt = System.currentTimeMillis(),
|
|
314
|
+
lastModified = System.currentTimeMillis(),
|
|
315
|
+
originalUri = sourceUri.toString()
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
// Store metadata
|
|
319
|
+
metadataCache[uniqueId] = metadata
|
|
320
|
+
saveMetadata()
|
|
321
|
+
|
|
322
|
+
Log.d(TAG, "Cached media: $uniqueId -> ${cacheFile.absolutePath}")
|
|
323
|
+
return metadata
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Cache a media file and return CacheMediaResult for the workflow layer.
|
|
328
|
+
*/
|
|
329
|
+
fun cacheMediaForStorage(
|
|
330
|
+
uri: Uri,
|
|
331
|
+
fileType: String,
|
|
332
|
+
fileName: String? = null,
|
|
333
|
+
allowMove: Boolean = false
|
|
334
|
+
): MediaStorageWorkflow.CacheMediaResult {
|
|
335
|
+
return try {
|
|
336
|
+
val metadata = cacheMedia(
|
|
337
|
+
sourceUri = uri,
|
|
338
|
+
fileType = fileType,
|
|
339
|
+
id = null,
|
|
340
|
+
allowMove = allowMove
|
|
341
|
+
)
|
|
342
|
+
MediaStorageWorkflow.CacheMediaResult(
|
|
343
|
+
isSuccess = true,
|
|
344
|
+
mediaId = metadata.id,
|
|
345
|
+
fileName = metadata.fileName,
|
|
346
|
+
cachePath = metadata.cachePath,
|
|
347
|
+
fileSize = metadata.fileSize,
|
|
348
|
+
width = metadata.width,
|
|
349
|
+
height = metadata.height,
|
|
350
|
+
duration = metadata.duration,
|
|
351
|
+
mimeType = metadata.mimeType
|
|
352
|
+
)
|
|
353
|
+
} catch (e: Exception) {
|
|
354
|
+
Log.e(TAG, "Failed to cache media", e)
|
|
355
|
+
MediaStorageWorkflow.CacheMediaResult(
|
|
356
|
+
isSuccess = false,
|
|
357
|
+
mediaId = "",
|
|
358
|
+
fileName = fileName ?: "",
|
|
359
|
+
cachePath = "",
|
|
360
|
+
error = e.message
|
|
361
|
+
)
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Cache a media file from a byte array
|
|
367
|
+
*
|
|
368
|
+
* @param data The byte array of the media file
|
|
369
|
+
* @param fileType The type of media ("photo" or "video")
|
|
370
|
+
* @param id Optional custom ID (will generate one if not provided)
|
|
371
|
+
* @return MediaCacheMetadata containing cache information
|
|
372
|
+
*/
|
|
373
|
+
fun cacheMediaFromBytes(data: ByteArray, fileType: String, id: String? = null): MediaCacheMetadata {
|
|
374
|
+
val uniqueId = id ?: generateUniqueId()
|
|
375
|
+
// The bytes are written verbatim, so the entry must be named for what they actually are.
|
|
376
|
+
// Unlike cacheMedia there is no source URI to ask the resolver about, so the container is
|
|
377
|
+
// read from the bytes themselves — otherwise a GIF written through this path lands as
|
|
378
|
+
// `media_x.jpg` / image/jpeg and every consumer fails to render it (see MediaFormat).
|
|
379
|
+
val isGif = fileType != "video" && fileType != "audio" && GifSupport.hasGifSignature(data)
|
|
380
|
+
val fileName = generateFileName(fileType, uniqueId, if (isGif) ".gif" else null)
|
|
381
|
+
val cacheFile = File(cacheDir, fileName)
|
|
382
|
+
|
|
383
|
+
// Write bytes to cache file
|
|
384
|
+
cacheFile.writeBytes(data)
|
|
385
|
+
|
|
386
|
+
// Extract metadata
|
|
387
|
+
val fileSize = cacheFile.length()
|
|
388
|
+
val (width, height, duration) = extractMediaMetadata(cacheFile, fileType)
|
|
389
|
+
val mimeType = if (isGif) "image/gif" else getMimeType(fileType)
|
|
390
|
+
|
|
391
|
+
// Create metadata
|
|
392
|
+
val metadata = MediaCacheMetadata(
|
|
393
|
+
id = uniqueId,
|
|
394
|
+
fileName = fileName,
|
|
395
|
+
cachePath = cacheFile.absolutePath,
|
|
396
|
+
fileType = fileType,
|
|
397
|
+
fileSize = fileSize,
|
|
398
|
+
width = width,
|
|
399
|
+
height = height,
|
|
400
|
+
duration = duration,
|
|
401
|
+
mimeType = mimeType,
|
|
402
|
+
createdAt = System.currentTimeMillis(),
|
|
403
|
+
lastModified = System.currentTimeMillis()
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
// Store metadata
|
|
407
|
+
metadataCache[uniqueId] = metadata
|
|
408
|
+
saveMetadata()
|
|
409
|
+
|
|
410
|
+
Log.d(TAG, "Cached media from bytes: $uniqueId -> ${cacheFile.absolutePath}")
|
|
411
|
+
return metadata
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Overwrite an existing cached media file with new data
|
|
416
|
+
* This replaces the existing file without creating a new entry
|
|
417
|
+
*
|
|
418
|
+
* @param id The ID of the media to overwrite
|
|
419
|
+
* @param newData The new byte data
|
|
420
|
+
* @return Updated MediaCacheMetadata
|
|
421
|
+
*/
|
|
422
|
+
fun overwriteCachedMedia(id: String, newData: ByteArray): MediaCacheMetadata {
|
|
423
|
+
val existingMetadata = metadataCache[id]
|
|
424
|
+
?: throw IllegalArgumentException("Media with id $id not found in cache")
|
|
425
|
+
|
|
426
|
+
val cacheFile = File(existingMetadata.cachePath)
|
|
427
|
+
|
|
428
|
+
// Overwrite file with new data
|
|
429
|
+
cacheFile.writeBytes(newData)
|
|
430
|
+
|
|
431
|
+
// Extract new metadata
|
|
432
|
+
val fileSize = cacheFile.length()
|
|
433
|
+
val (width, height, duration) = extractMediaMetadata(cacheFile, existingMetadata.fileType)
|
|
434
|
+
|
|
435
|
+
// Update metadata
|
|
436
|
+
val updatedMetadata = existingMetadata.copy(
|
|
437
|
+
fileSize = fileSize,
|
|
438
|
+
width = width,
|
|
439
|
+
height = height,
|
|
440
|
+
duration = duration,
|
|
441
|
+
lastModified = System.currentTimeMillis()
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
// Store updated metadata
|
|
445
|
+
metadataCache[id] = updatedMetadata
|
|
446
|
+
saveMetadata()
|
|
447
|
+
|
|
448
|
+
Log.d(TAG, "Overwrote cached media: $id")
|
|
449
|
+
return updatedMetadata
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Overwrite an existing cached media file from a source URI
|
|
454
|
+
*
|
|
455
|
+
* @param id The ID of the media to overwrite
|
|
456
|
+
* @param sourceUri The source URI of the new media data
|
|
457
|
+
* @return Updated MediaCacheMetadata
|
|
458
|
+
*/
|
|
459
|
+
fun overwriteCachedMediaFromUri(id: String, sourceUri: Uri): MediaCacheMetadata {
|
|
460
|
+
val existingMetadata = metadataCache[id]
|
|
461
|
+
?: throw IllegalArgumentException("Media with id $id not found in cache")
|
|
462
|
+
|
|
463
|
+
var cacheFile = File(existingMetadata.cachePath)
|
|
464
|
+
|
|
465
|
+
// Copy file to cache, overwriting existing. Never a move: the source of an overwrite is an
|
|
466
|
+
// edit result whose lifetime belongs to the editor, not to us.
|
|
467
|
+
ingestUriToFile(sourceUri, cacheFile, allowMove = false)
|
|
468
|
+
|
|
469
|
+
// The new bytes may be in a DIFFERENT container than the entry they replace: baking edits
|
|
470
|
+
// into a GIF (or PNG/WebP) composites it to a flat JPEG, and leaving the entry named .gif
|
|
471
|
+
// would put JPEG bytes behind a GIF name and MIME type — the same lie, just in the other
|
|
472
|
+
// direction. Re-type the entry to match what was actually written. Same-format overwrites
|
|
473
|
+
// (trim/mute writing .mp4 over .mp4) resolve to the identical name and skip the rename.
|
|
474
|
+
val incoming = MediaFormat.of(context, sourceUri, existingMetadata.fileType)
|
|
475
|
+
var fileName = existingMetadata.fileName
|
|
476
|
+
var mimeType = existingMetadata.mimeType
|
|
477
|
+
if (!fileName.endsWith(incoming.extension, ignoreCase = true)) {
|
|
478
|
+
val retypedName = "${File(fileName).nameWithoutExtension}${incoming.extension}"
|
|
479
|
+
val retypedFile = File(cacheDir, retypedName)
|
|
480
|
+
if (runCatching { cacheFile.renameTo(retypedFile) }.getOrDefault(false)) {
|
|
481
|
+
cacheFile = retypedFile
|
|
482
|
+
fileName = retypedName
|
|
483
|
+
mimeType = incoming.mimeType
|
|
484
|
+
Log.d(TAG, "Re-typed cached media $id to $retypedName after format change")
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Extract new metadata
|
|
489
|
+
val fileSize = cacheFile.length()
|
|
490
|
+
val (width, height, duration) = extractMediaMetadata(cacheFile, existingMetadata.fileType)
|
|
491
|
+
|
|
492
|
+
// Update metadata
|
|
493
|
+
val updatedMetadata = existingMetadata.copy(
|
|
494
|
+
fileName = fileName,
|
|
495
|
+
cachePath = cacheFile.absolutePath,
|
|
496
|
+
mimeType = mimeType,
|
|
497
|
+
fileSize = fileSize,
|
|
498
|
+
width = width,
|
|
499
|
+
height = height,
|
|
500
|
+
duration = duration,
|
|
501
|
+
lastModified = System.currentTimeMillis()
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
// Store updated metadata
|
|
505
|
+
metadataCache[id] = updatedMetadata
|
|
506
|
+
saveMetadata()
|
|
507
|
+
|
|
508
|
+
Log.d(TAG, "Overwrote cached media from URI: $id")
|
|
509
|
+
return updatedMetadata
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Get metadata for a cached media item
|
|
514
|
+
*/
|
|
515
|
+
fun getMetadata(id: String): MediaCacheMetadata? {
|
|
516
|
+
return metadataCache[id]
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Get all cached media metadata
|
|
521
|
+
*/
|
|
522
|
+
fun getAllMetadata(): List<MediaCacheMetadata> {
|
|
523
|
+
return metadataCache.values.sortedByDescending { it.createdAt }
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Get a cached media file
|
|
528
|
+
*/
|
|
529
|
+
fun getCachedFile(id: String): File? {
|
|
530
|
+
val metadata = metadataCache[id] ?: return null
|
|
531
|
+
val file = File(metadata.cachePath)
|
|
532
|
+
return if (file.exists()) file else null
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Get URI for a cached media file
|
|
537
|
+
*/
|
|
538
|
+
fun getCachedUri(id: String): Uri? {
|
|
539
|
+
val file = getCachedFile(id) ?: return null
|
|
540
|
+
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
|
541
|
+
try {
|
|
542
|
+
FileProvider.getUriForFile(
|
|
543
|
+
context,
|
|
544
|
+
"${context.packageName}.camera.fileprovider",
|
|
545
|
+
file
|
|
546
|
+
)
|
|
547
|
+
} catch (e: Exception) {
|
|
548
|
+
Uri.fromFile(file)
|
|
549
|
+
}
|
|
550
|
+
} else {
|
|
551
|
+
Uri.fromFile(file)
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Delete a cached media item
|
|
557
|
+
*/
|
|
558
|
+
fun deleteCachedMedia(id: String): Boolean {
|
|
559
|
+
val metadata = metadataCache[id] ?: return false
|
|
560
|
+
val file = File(metadata.cachePath)
|
|
561
|
+
|
|
562
|
+
val deleted = if (file.exists()) {
|
|
563
|
+
file.delete()
|
|
564
|
+
} else {
|
|
565
|
+
true // File doesn't exist, consider it deleted
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
if (deleted) {
|
|
569
|
+
metadataCache.remove(id)
|
|
570
|
+
saveMetadata()
|
|
571
|
+
Log.d(TAG, "Deleted cached media: $id")
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
return deleted
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Clear all cached media
|
|
579
|
+
*/
|
|
580
|
+
fun clearCache(): Boolean {
|
|
581
|
+
var success = true
|
|
582
|
+
|
|
583
|
+
// Delete all files in cache directory
|
|
584
|
+
cacheDir.listFiles()?.forEach { file ->
|
|
585
|
+
if (file.name != METADATA_FILE_NAME) {
|
|
586
|
+
if (!file.delete()) {
|
|
587
|
+
success = false
|
|
588
|
+
Log.w(TAG, "Failed to delete cache file: ${file.name}")
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Clear metadata
|
|
594
|
+
metadataCache.clear()
|
|
595
|
+
saveMetadata()
|
|
596
|
+
|
|
597
|
+
// Reclaim the persistent compression cache too — its master outputs live in a sibling cache
|
|
598
|
+
// dir (cache/compression_cache/) and its index is a separate encrypted DB, neither of which
|
|
599
|
+
// this loop above touches. Best-effort: a failure here never fails the media-cache clear.
|
|
600
|
+
runCatching { CompressionCacheStore.clearAll(context) }
|
|
601
|
+
|
|
602
|
+
Log.d(TAG, "Cleared cache, success: $success")
|
|
603
|
+
return success
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Get total cache size in bytes
|
|
608
|
+
*/
|
|
609
|
+
fun getCacheSize(): Long {
|
|
610
|
+
return metadataCache.values.sumOf { it.fileSize }
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Get cache size in MB
|
|
615
|
+
*/
|
|
616
|
+
fun getCacheSizeMB(): Double {
|
|
617
|
+
return getCacheSize() / (1024.0 * 1024.0)
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Clean up old cache entries if cache size exceeds limit
|
|
622
|
+
* Removes oldest entries first
|
|
623
|
+
*/
|
|
624
|
+
fun cleanupIfNeeded() {
|
|
625
|
+
val currentSizeMB = getCacheSizeMB()
|
|
626
|
+
|
|
627
|
+
if (currentSizeMB > MAX_CACHE_SIZE_MB) {
|
|
628
|
+
Log.d(TAG, "Cache size ($currentSizeMB MB) exceeds limit ($MAX_CACHE_SIZE_MB MB), cleaning up")
|
|
629
|
+
|
|
630
|
+
// Sort by creation time (oldest first)
|
|
631
|
+
val sortedMetadata = metadataCache.values.sortedBy { it.createdAt }
|
|
632
|
+
var sizeToRemove = currentSizeMB - (MAX_CACHE_SIZE_MB * 0.8) // Remove down to 80% of limit
|
|
633
|
+
|
|
634
|
+
for (metadata in sortedMetadata) {
|
|
635
|
+
if (sizeToRemove <= 0) break
|
|
636
|
+
|
|
637
|
+
val removedSize = metadata.fileSize / (1024.0 * 1024.0)
|
|
638
|
+
if (deleteCachedMedia(metadata.id)) {
|
|
639
|
+
sizeToRemove -= removedSize
|
|
640
|
+
Log.d(TAG, "Removed old cache entry: ${metadata.id} (${removedSize} MB)")
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* Bring the content behind [sourceUri] into [destFile].
|
|
648
|
+
*
|
|
649
|
+
* When [allowMove] is set AND the URI resolves to a plain file this app owns, the file is MOVED
|
|
650
|
+
* (a rename inside the cache volume) instead of copied. This is the difference between a large
|
|
651
|
+
* recording landing in the cache instantly and the app spending a full read+write pass over
|
|
652
|
+
* hundreds of megabytes — while also needing a second copy's worth of free space it does not
|
|
653
|
+
* have. Capture callers immediately delete their temp file afterwards, so the move is exactly
|
|
654
|
+
* what they meant; a deleted-by-us source simply makes their delete a no-op.
|
|
655
|
+
*
|
|
656
|
+
* Everything else (gallery picks, documents, anything behind a content provider) is copied, and
|
|
657
|
+
* a failed copy never leaves a truncated file behind — see [MediaFileIo.copyStream].
|
|
658
|
+
*/
|
|
659
|
+
private fun ingestUriToFile(sourceUri: Uri, destFile: File, allowMove: Boolean) {
|
|
660
|
+
if (allowMove) {
|
|
661
|
+
sourceFileOrNull(sourceUri)?.let { source ->
|
|
662
|
+
if (MediaFileIo.relocate(source, destFile).succeeded) return
|
|
663
|
+
// Fall through to the stream path: the source is still intact either way, so a refused
|
|
664
|
+
// relocation (no room for a cross-volume copy) still gets one honest attempt that either
|
|
665
|
+
// works or throws a reportable IOException.
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
context.contentResolver.openInputStream(sourceUri)?.use { input ->
|
|
669
|
+
MediaFileIo.copyStream(input, destFile)
|
|
670
|
+
} ?: throw IOException("Failed to open input stream for URI: $sourceUri")
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* The on-disk file behind [uri], or null when there is not one we may relocate.
|
|
675
|
+
*
|
|
676
|
+
* Only `file://` URIs qualify. A `content://` URI is deliberately excluded even when it points
|
|
677
|
+
* at our own FileProvider: the provider is the only thing that knows how the authority maps to
|
|
678
|
+
* a path, and guessing wrong would move a file some other component still owns.
|
|
679
|
+
*/
|
|
680
|
+
private fun sourceFileOrNull(uri: Uri): File? {
|
|
681
|
+
if (uri.scheme != null && uri.scheme != "file") return null
|
|
682
|
+
val path = uri.path?.takeIf { it.isNotBlank() } ?: return null
|
|
683
|
+
return File(path).takeIf { it.isFile }
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Extract media metadata (dimensions, duration) from file
|
|
688
|
+
*/
|
|
689
|
+
private fun extractMediaMetadata(file: File, fileType: String): Triple<Int?, Int?, Long?> {
|
|
690
|
+
return if (fileType == "video") {
|
|
691
|
+
extractVideoMetadata(file)
|
|
692
|
+
} else {
|
|
693
|
+
extractImageMetadata(file)
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* Extract image metadata
|
|
699
|
+
*/
|
|
700
|
+
private fun extractImageMetadata(file: File): Triple<Int?, Int?, Long?> {
|
|
701
|
+
return try {
|
|
702
|
+
val options = BitmapFactory.Options().apply {
|
|
703
|
+
inJustDecodeBounds = true
|
|
704
|
+
}
|
|
705
|
+
BitmapFactory.decodeFile(file.absolutePath, options)
|
|
706
|
+
Triple(options.outWidth, options.outHeight, null)
|
|
707
|
+
} catch (e: Exception) {
|
|
708
|
+
Log.w(TAG, "Failed to extract image metadata", e)
|
|
709
|
+
Triple(null, null, null)
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* Extract video metadata
|
|
715
|
+
*/
|
|
716
|
+
private fun extractVideoMetadata(file: File): Triple<Int?, Int?, Long?> {
|
|
717
|
+
return try {
|
|
718
|
+
val retriever = MediaMetadataRetriever()
|
|
719
|
+
// release() MUST run even if setDataSource/extractMetadata throws (corrupt or
|
|
720
|
+
// zero-byte cache file, unsupported codec) — otherwise the native retriever handle
|
|
721
|
+
// leaks. Rapidly caching many videos, some malformed, would accumulate leaked native
|
|
722
|
+
// allocations until MediaMetadataRetriever creation fails and the process crashes.
|
|
723
|
+
// Use try/finally, NOT Kotlin .use{} (MediaMetadataRetriever is only AutoCloseable on
|
|
724
|
+
// API 29+, so .use{} crashes on API 24-28).
|
|
725
|
+
try {
|
|
726
|
+
retriever.setDataSource(file.absolutePath)
|
|
727
|
+
|
|
728
|
+
var width = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)?.toIntOrNull()
|
|
729
|
+
var height = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)?.toIntOrNull()
|
|
730
|
+
val duration = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLongOrNull()
|
|
731
|
+
val rotation = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION)?.toIntOrNull() ?: 0
|
|
732
|
+
|
|
733
|
+
if (rotation == 90 || rotation == 270) {
|
|
734
|
+
val temp = width
|
|
735
|
+
width = height
|
|
736
|
+
height = temp
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
Triple(width, height, duration)
|
|
740
|
+
} finally {
|
|
741
|
+
retriever.release()
|
|
742
|
+
}
|
|
743
|
+
} catch (e: Exception) {
|
|
744
|
+
Log.w(TAG, "Failed to extract video metadata", e)
|
|
745
|
+
Triple(null, null, null)
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* Get MIME type for file type
|
|
751
|
+
*/
|
|
752
|
+
private fun getMimeType(fileType: String): String {
|
|
753
|
+
return when (fileType) {
|
|
754
|
+
"video" -> "video/mp4"
|
|
755
|
+
else -> "image/jpeg"
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* Check if a media item exists in cache
|
|
761
|
+
*/
|
|
762
|
+
fun existsInCache(id: String): Boolean {
|
|
763
|
+
return metadataCache.containsKey(id) && getCachedFile(id)?.exists() == true
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
/**
|
|
767
|
+
* Get cache statistics
|
|
768
|
+
*/
|
|
769
|
+
fun getCacheStats(): Map<String, Any> {
|
|
770
|
+
val allMetadata = getAllMetadata()
|
|
771
|
+
val photoCount = allMetadata.count { it.fileType == "photo" }
|
|
772
|
+
val videoCount = allMetadata.count { it.fileType == "video" }
|
|
773
|
+
|
|
774
|
+
return mapOf(
|
|
775
|
+
"totalItems" to allMetadata.size,
|
|
776
|
+
"photoCount" to photoCount,
|
|
777
|
+
"videoCount" to videoCount,
|
|
778
|
+
"totalSizeMB" to String.format("%.2f", getCacheSizeMB()),
|
|
779
|
+
"maxSizeMB" to MAX_CACHE_SIZE_MB
|
|
780
|
+
)
|
|
781
|
+
}
|
|
782
|
+
}
|