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,909 @@
|
|
|
1
|
+
package com.customcamera
|
|
2
|
+
|
|
3
|
+
import android.app.Notification
|
|
4
|
+
import android.app.NotificationChannel
|
|
5
|
+
import android.app.NotificationManager
|
|
6
|
+
import android.app.PendingIntent
|
|
7
|
+
import android.app.Service
|
|
8
|
+
import android.content.ContentResolver
|
|
9
|
+
import android.content.Context
|
|
10
|
+
import android.content.Intent
|
|
11
|
+
import android.content.SharedPreferences
|
|
12
|
+
import android.content.pm.ServiceInfo
|
|
13
|
+
import android.net.Uri
|
|
14
|
+
import android.os.Build
|
|
15
|
+
import android.os.IBinder
|
|
16
|
+
import android.provider.MediaStore
|
|
17
|
+
import androidx.core.app.NotificationCompat
|
|
18
|
+
import androidx.core.app.NotificationManagerCompat
|
|
19
|
+
import com.facebook.react.bridge.Arguments
|
|
20
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
21
|
+
import com.facebook.react.bridge.WritableMap
|
|
22
|
+
import com.facebook.react.modules.core.DeviceEventManagerModule
|
|
23
|
+
import com.google.gson.Gson
|
|
24
|
+
import okhttp3.MediaType
|
|
25
|
+
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
|
26
|
+
import okhttp3.MultipartBody
|
|
27
|
+
import okhttp3.OkHttpClient
|
|
28
|
+
import okhttp3.Request
|
|
29
|
+
import okhttp3.RequestBody
|
|
30
|
+
import okio.BufferedSink
|
|
31
|
+
import java.io.FileNotFoundException
|
|
32
|
+
import java.io.IOException
|
|
33
|
+
import java.net.ConnectException
|
|
34
|
+
import java.net.NoRouteToHostException
|
|
35
|
+
import java.net.UnknownHostException
|
|
36
|
+
import java.util.concurrent.ExecutorService
|
|
37
|
+
import java.util.concurrent.Executors
|
|
38
|
+
import java.util.concurrent.TimeUnit
|
|
39
|
+
import java.util.concurrent.atomic.AtomicInteger
|
|
40
|
+
import javax.net.ssl.SSLException
|
|
41
|
+
|
|
42
|
+
class UploadService : Service() {
|
|
43
|
+
|
|
44
|
+
private lateinit var executorService: ExecutorService
|
|
45
|
+
private val client = OkHttpClient.Builder()
|
|
46
|
+
.connectTimeout(30, TimeUnit.SECONDS)
|
|
47
|
+
// Read timeout covers the wait between finishing the request body and receiving
|
|
48
|
+
// the response headers. Endpoints that post-process large uploads (httpbin, for
|
|
49
|
+
// one, echoes the whole file back in its JSON response) legitimately take more
|
|
50
|
+
// than 30s for big videos — the old 30s limit turned those into spurious
|
|
51
|
+
// "timeout" failure notifications. No callTimeout on purpose: an overall cap
|
|
52
|
+
// would abort long-but-progressing uploads.
|
|
53
|
+
.readTimeout(120, TimeUnit.SECONDS)
|
|
54
|
+
.writeTimeout(60, TimeUnit.SECONDS)
|
|
55
|
+
.build()
|
|
56
|
+
|
|
57
|
+
private lateinit var sharedPreferences: SharedPreferences
|
|
58
|
+
private lateinit var gson: Gson
|
|
59
|
+
|
|
60
|
+
// Tracks upload batches that have been accepted but not yet finished. A bare
|
|
61
|
+
// stopSelf() would tear the service down as soon as ONE batch finishes even if
|
|
62
|
+
// a second startUpload() call queued another batch behind it on the (single
|
|
63
|
+
// threaded) executor — killing that batch's upload mid-flight. stopSelf(startId)
|
|
64
|
+
// combined with this counter ensures the service only stops once every accepted
|
|
65
|
+
// batch has actually completed.
|
|
66
|
+
private val activeJobs = AtomicInteger(0)
|
|
67
|
+
|
|
68
|
+
// Latest progress notification posted by the batch currently uploading. When a second
|
|
69
|
+
// batch arrives while one is in flight, onStartCommand must still call startForeground()
|
|
70
|
+
// (mandatory after every startForegroundService()) — reusing this keeps that call from
|
|
71
|
+
// stomping the live progress bar back to "Preparing files… (0%)".
|
|
72
|
+
@Volatile
|
|
73
|
+
private var lastProgressNotification: Notification? = null
|
|
74
|
+
|
|
75
|
+
override fun onCreate() {
|
|
76
|
+
super.onCreate()
|
|
77
|
+
executorService = Executors.newSingleThreadExecutor()
|
|
78
|
+
sharedPreferences = getSharedPreferences("UploadServicePrefs", Context.MODE_PRIVATE)
|
|
79
|
+
gson = Gson()
|
|
80
|
+
if (SYSTEM_NOTIFICATIONS_ENABLED) {
|
|
81
|
+
createNotificationChannel()
|
|
82
|
+
}
|
|
83
|
+
recoverUploadStateIfNeeded()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
|
87
|
+
if (intent == null) {
|
|
88
|
+
stopIfIdle(startId)
|
|
89
|
+
return START_NOT_STICKY
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
val action = intent.action
|
|
93
|
+
if (action == ACTION_START_UPLOAD) {
|
|
94
|
+
val uris = intent.getStringArrayListExtra(EXTRA_URIS)
|
|
95
|
+
val uploadUrl = intent.getStringExtra(EXTRA_UPLOAD_URL)
|
|
96
|
+
|
|
97
|
+
if (uris.isNullOrEmpty() || uploadUrl.isNullOrBlank()) {
|
|
98
|
+
stopIfIdle(startId)
|
|
99
|
+
return START_NOT_STICKY
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
val jobsNowActive = activeJobs.incrementAndGet()
|
|
103
|
+
|
|
104
|
+
// Start foreground immediately. If another batch is already mid-upload, reuse
|
|
105
|
+
// its latest progress notification instead of resetting the bar to 0%.
|
|
106
|
+
// With system notifications disabled the service was launched via a plain
|
|
107
|
+
// startService() (see CustomCameraModule.startUpload), so there is no
|
|
108
|
+
// startForeground() obligation — and none is possible, since startForeground
|
|
109
|
+
// requires posting a notification.
|
|
110
|
+
if (SYSTEM_NOTIFICATIONS_ENABLED) {
|
|
111
|
+
val initialNotification =
|
|
112
|
+
(if (jobsNowActive > 1) lastProgressNotification else null)
|
|
113
|
+
?: buildProgressNotification(0, 0, uris.size)
|
|
114
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
115
|
+
startForeground(
|
|
116
|
+
NOTIFICATION_ID,
|
|
117
|
+
initialNotification,
|
|
118
|
+
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
|
119
|
+
)
|
|
120
|
+
} else {
|
|
121
|
+
startForeground(NOTIFICATION_ID, initialNotification)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Queue the background upload
|
|
126
|
+
executorService.execute {
|
|
127
|
+
performUpload(uris, uploadUrl, startId)
|
|
128
|
+
}
|
|
129
|
+
} else if (action == ACTION_GET_NOTIFICATION_HISTORY) {
|
|
130
|
+
// Handle request for notification history. Callers use a regular startService for
|
|
131
|
+
// this action, so there is no startForeground() obligation here.
|
|
132
|
+
val history = getNotificationHistory()
|
|
133
|
+
val resultIntent = Intent(ACTION_GET_NOTIFICATION_HISTORY).apply {
|
|
134
|
+
putExtra(EXTRA_NOTIFICATION_HISTORY, history)
|
|
135
|
+
}
|
|
136
|
+
sendBroadcast(resultIntent)
|
|
137
|
+
stopIfIdle(startId)
|
|
138
|
+
} else {
|
|
139
|
+
stopIfIdle(startId)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return START_NOT_STICKY
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
override fun onBind(intent: Intent?): IBinder? = null
|
|
146
|
+
|
|
147
|
+
override fun onDestroy() {
|
|
148
|
+
executorService.shutdownNow()
|
|
149
|
+
super.onDestroy()
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Stops the service via [startId] only if no upload batch is currently in flight. */
|
|
153
|
+
private fun stopIfIdle(startId: Int) {
|
|
154
|
+
if (activeJobs.get() <= 0) {
|
|
155
|
+
stopSelf(startId)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Marks one upload batch as finished. Uses [stopSelf] with the batch's own
|
|
161
|
+
* [startId] rather than the no-argument overload so a batch that is still
|
|
162
|
+
* running (queued behind this one on the executor) is never torn down
|
|
163
|
+
* prematurely — the service only actually stops once every accepted batch
|
|
164
|
+
* has completed.
|
|
165
|
+
*/
|
|
166
|
+
private fun completeJob(startId: Int) {
|
|
167
|
+
if (activeJobs.decrementAndGet() <= 0) {
|
|
168
|
+
stopSelf(startId)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private fun createNotificationChannel() {
|
|
173
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
174
|
+
val notificationManager: NotificationManager =
|
|
175
|
+
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
176
|
+
val cfg = notificationConfig
|
|
177
|
+
|
|
178
|
+
// Low-importance channel for the ongoing upload-progress bar: shown silently in
|
|
179
|
+
// the shade with no heads-up or sound, which suits a frequently-updating progress
|
|
180
|
+
// notification.
|
|
181
|
+
val progressChannel = NotificationChannel(
|
|
182
|
+
cfg.progressChannelId,
|
|
183
|
+
cfg.progressChannelName
|
|
184
|
+
?: Localization.getString("uploadProgressTitle", "Upload Progress"),
|
|
185
|
+
NotificationManager.IMPORTANCE_LOW
|
|
186
|
+
).apply {
|
|
187
|
+
description = Localization.getString("uploadProgressDescription", "Displays progress of background uploads")
|
|
188
|
+
}
|
|
189
|
+
notificationManager.createNotificationChannel(progressChannel)
|
|
190
|
+
|
|
191
|
+
// Separate HIGH-importance channel for the terminal success/failure notifications.
|
|
192
|
+
// A channel's importance is fixed once created, so posting these on the silent
|
|
193
|
+
// progress channel permanently clamps them to silent — the user would never see a
|
|
194
|
+
// heads-up (or hear a sound) telling them an upload finished or failed. A dedicated
|
|
195
|
+
// channel lets the result actually surface.
|
|
196
|
+
val resultChannel = NotificationChannel(
|
|
197
|
+
cfg.resultChannelId,
|
|
198
|
+
cfg.resultChannelName
|
|
199
|
+
?: Localization.getString("uploadStatusTitle", "Upload Status"),
|
|
200
|
+
NotificationManager.IMPORTANCE_HIGH
|
|
201
|
+
).apply {
|
|
202
|
+
description = Localization.getString("uploadStatusDescription", "Notifies when a background upload completes or fails")
|
|
203
|
+
}
|
|
204
|
+
notificationManager.createNotificationChannel(resultChannel)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private fun recoverUploadStateIfNeeded() {
|
|
209
|
+
val savedState = sharedPreferences.getString("upload_state", null) ?: return
|
|
210
|
+
try {
|
|
211
|
+
val uploadState = gson.fromJson<UploadState>(savedState, UploadState::class.java)
|
|
212
|
+
if (uploadState != null && !uploadState.completed) {
|
|
213
|
+
// An in-flight state at service creation means the previous process died
|
|
214
|
+
// mid-upload — that upload no longer exists. Re-posting its progress
|
|
215
|
+
// notification here (as this used to do) produced a frozen, setOngoing
|
|
216
|
+
// (unswipeable) "Uploading media" bar that nothing would ever update or
|
|
217
|
+
// remove, re-appearing on EVERY service start — including plain history
|
|
218
|
+
// queries. Cancel any leftover bar and record the interruption in history
|
|
219
|
+
// instead; a new batch posts its own fresh notification right after this.
|
|
220
|
+
NotificationManagerCompat.from(this).cancel(NOTIFICATION_ID)
|
|
221
|
+
addToNotificationHistory(
|
|
222
|
+
NotificationHistoryItem(
|
|
223
|
+
title = Localization.getString("uploadInterruptedTitle", "Upload interrupted"),
|
|
224
|
+
message = Localization.getString("uploadInterruptedMessage", "A previous upload stopped before completing (file {0} of {1}).", uploadState.currentFile, uploadState.totalFiles),
|
|
225
|
+
timestamp = uploadState.timestamp,
|
|
226
|
+
type = "failure"
|
|
227
|
+
)
|
|
228
|
+
)
|
|
229
|
+
}
|
|
230
|
+
} catch (e: Exception) {
|
|
231
|
+
// Corrupt state — fall through and clear it below
|
|
232
|
+
}
|
|
233
|
+
clearUploadState()
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
private fun getUriSize(uri: Uri): Long {
|
|
237
|
+
var size = 0L
|
|
238
|
+
try {
|
|
239
|
+
contentResolver.query(uri, arrayOf(MediaStore.Images.Media.SIZE), null, null, null)?.use { cursor ->
|
|
240
|
+
if (cursor.moveToFirst()) {
|
|
241
|
+
val sizeIndex = cursor.getColumnIndex(MediaStore.Images.Media.SIZE)
|
|
242
|
+
if (sizeIndex != -1) {
|
|
243
|
+
size = cursor.getLong(sizeIndex)
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
} catch (e: Exception) {
|
|
248
|
+
// ignore
|
|
249
|
+
}
|
|
250
|
+
if (size <= 0L) {
|
|
251
|
+
try {
|
|
252
|
+
contentResolver.openAssetFileDescriptor(uri, "r")?.use { afd ->
|
|
253
|
+
size = afd.length
|
|
254
|
+
}
|
|
255
|
+
} catch (e: Exception) {
|
|
256
|
+
// ignore
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return size
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
private fun getMimeType(uri: Uri): String {
|
|
263
|
+
return contentResolver.getType(uri) ?: "image/jpeg"
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private fun performUpload(uriStrings: List<String>, uploadUrl: String, startId: Int) {
|
|
267
|
+
val totalFiles = uriStrings.size
|
|
268
|
+
val uris = uriStrings.map { Uri.parse(it) }
|
|
269
|
+
|
|
270
|
+
// Compute sizes
|
|
271
|
+
val fileSizes = uris.map { getUriSize(it) }
|
|
272
|
+
val totalBytes = fileSizes.sum()
|
|
273
|
+
|
|
274
|
+
// Bytes of every file already fully handled — uploaded OR permanently failed.
|
|
275
|
+
// Failed files advance this too, so the batch bar keeps moving forward through
|
|
276
|
+
// the remaining files instead of stalling at the failure point.
|
|
277
|
+
var bytesProcessed = 0L
|
|
278
|
+
var lastUpdatedProgress = -1
|
|
279
|
+
var lastUpdatedFile = -1
|
|
280
|
+
var lastUpdatedTime = 0L
|
|
281
|
+
|
|
282
|
+
// Save initial upload state
|
|
283
|
+
saveUploadState(UploadState(
|
|
284
|
+
uris = uriStrings,
|
|
285
|
+
uploadUrl = uploadUrl,
|
|
286
|
+
totalFiles = totalFiles,
|
|
287
|
+
currentFile = 0,
|
|
288
|
+
progress = 0,
|
|
289
|
+
completed = false,
|
|
290
|
+
timestamp = System.currentTimeMillis()
|
|
291
|
+
))
|
|
292
|
+
|
|
293
|
+
fun postProgressNotification(notification: Notification) {
|
|
294
|
+
if (!SYSTEM_NOTIFICATIONS_ENABLED) return
|
|
295
|
+
lastProgressNotification = notification
|
|
296
|
+
try {
|
|
297
|
+
NotificationManagerCompat.from(this).notify(NOTIFICATION_ID, notification)
|
|
298
|
+
} catch (e: SecurityException) {
|
|
299
|
+
// POST_NOTIFICATIONS not granted — the upload continues without visible progress
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Helper to update the progress notification. `force` bypasses the throttle for
|
|
304
|
+
// state transitions the user must see immediately (file boundaries, retry notices).
|
|
305
|
+
fun updateProgress(currentProgressBytes: Long, fileIndex: Int, force: Boolean = false) {
|
|
306
|
+
val totalUploadedBytes = bytesProcessed + currentProgressBytes
|
|
307
|
+
val percentage = if (totalBytes > 0) {
|
|
308
|
+
((totalUploadedBytes * 100) / totalBytes).toInt().coerceIn(0, 100)
|
|
309
|
+
} else {
|
|
310
|
+
((fileIndex * 100) / totalFiles).coerceIn(0, 100)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
val currentTime = System.currentTimeMillis()
|
|
314
|
+
// Throttle: post only when something visible changed (percentage or file
|
|
315
|
+
// counter) AND at least MIN_NOTIFY_INTERVAL_MS has passed. The old condition
|
|
316
|
+
// ("percentage changed OR 150ms elapsed") posted on EVERY percentage step with
|
|
317
|
+
// no time floor — a fast upload could enqueue dozens of notify() calls per
|
|
318
|
+
// second, tripping NotificationManager's per-app rate limit, which silently
|
|
319
|
+
// sheds the excess (potentially including the terminal success/failure post).
|
|
320
|
+
val visibleChange = percentage != lastUpdatedProgress || fileIndex != lastUpdatedFile
|
|
321
|
+
if (!force && !(visibleChange && (currentTime - lastUpdatedTime) >= MIN_NOTIFY_INTERVAL_MS)) {
|
|
322
|
+
return
|
|
323
|
+
}
|
|
324
|
+
lastUpdatedProgress = percentage
|
|
325
|
+
lastUpdatedFile = fileIndex
|
|
326
|
+
lastUpdatedTime = currentTime
|
|
327
|
+
|
|
328
|
+
postProgressNotification(buildProgressNotification(percentage, fileIndex + 1, totalFiles))
|
|
329
|
+
emitUploadEvent("progress", percentage, fileIndex + 1, totalFiles)
|
|
330
|
+
|
|
331
|
+
// Save progress state
|
|
332
|
+
saveUploadState(UploadState(
|
|
333
|
+
uris = uriStrings,
|
|
334
|
+
uploadUrl = uploadUrl,
|
|
335
|
+
totalFiles = totalFiles,
|
|
336
|
+
currentFile = fileIndex + 1,
|
|
337
|
+
progress = percentage,
|
|
338
|
+
completed = false,
|
|
339
|
+
timestamp = System.currentTimeMillis()
|
|
340
|
+
))
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
val failures = mutableListOf<String>()
|
|
344
|
+
var abortedAtFile = -1
|
|
345
|
+
|
|
346
|
+
for (i in uris.indices) {
|
|
347
|
+
if (Thread.currentThread().isInterrupted) return // service is being torn down
|
|
348
|
+
|
|
349
|
+
// Flip the notification to "Uploading file X of N (…%)" the moment the file
|
|
350
|
+
// starts: DNS + TLS setup can take seconds before the first body byte (and
|
|
351
|
+
// therefore the first progress callback) — the bar should reflect state now,
|
|
352
|
+
// not once the connection happens to come up.
|
|
353
|
+
updateProgress(0, i, force = true)
|
|
354
|
+
|
|
355
|
+
val failure = uploadFileWithRetry(
|
|
356
|
+
uri = uris[i],
|
|
357
|
+
fileSize = fileSizes[i],
|
|
358
|
+
uploadUrl = uploadUrl,
|
|
359
|
+
onProgress = { bytesWritten -> updateProgress(bytesWritten, i) },
|
|
360
|
+
onRetryScheduled = { attempt ->
|
|
361
|
+
// A state change the user should see immediately, not a throttled tick.
|
|
362
|
+
postProgressNotification(
|
|
363
|
+
buildProgressNotification(
|
|
364
|
+
lastUpdatedProgress.coerceAtLeast(0), i + 1, totalFiles,
|
|
365
|
+
statusText = Localization.getString("uploadRetrying", "Retrying file {0} of {1} (attempt {2} of {3})…", i + 1, totalFiles, attempt + 1, MAX_UPLOAD_ATTEMPTS)
|
|
366
|
+
)
|
|
367
|
+
)
|
|
368
|
+
}
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
// Success or permanent failure, this file has been dealt with — advance the
|
|
372
|
+
// progress base so the bar never runs backwards past it.
|
|
373
|
+
bytesProcessed += fileSizes[i]
|
|
374
|
+
|
|
375
|
+
if (failure != null) {
|
|
376
|
+
failures.add(failure.message)
|
|
377
|
+
if (failure.abortBatch) {
|
|
378
|
+
// Endpoint-level failure (DNS, refused connection, TLS): every
|
|
379
|
+
// remaining file would fail identically — stop now and report
|
|
380
|
+
// instead of burning a full retry cycle per file.
|
|
381
|
+
abortedAtFile = i
|
|
382
|
+
break
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
val attemptedFiles = if (abortedAtFile >= 0) abortedAtFile + 1 else totalFiles
|
|
388
|
+
val uploadedCount = attemptedFiles - failures.size
|
|
389
|
+
|
|
390
|
+
// Remove the ongoing (silent, low-importance) progress notification, then post the
|
|
391
|
+
// terminal result on the dedicated HIGH-importance channel under its OWN id so it
|
|
392
|
+
// surfaces as a fresh heads-up. It is left in the shade (no auto-cancel timer) until
|
|
393
|
+
// the user taps or swipes it away. stopForeground() is service-wide, so drop the
|
|
394
|
+
// foreground state only when NO other batch is still uploading — otherwise the call
|
|
395
|
+
// would rip away the queued batch's live progress notification mid-flight.
|
|
396
|
+
// (No-op when system notifications are disabled: the service never went foreground.)
|
|
397
|
+
if (SYSTEM_NOTIFICATIONS_ENABLED && activeJobs.get() <= 1) {
|
|
398
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
|
399
|
+
stopForeground(STOP_FOREGROUND_REMOVE)
|
|
400
|
+
} else {
|
|
401
|
+
@Suppress("DEPRECATION")
|
|
402
|
+
stopForeground(true)
|
|
403
|
+
}
|
|
404
|
+
lastProgressNotification = null
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
val resultNotification: Notification
|
|
408
|
+
val historyItem: NotificationHistoryItem
|
|
409
|
+
if (failures.isEmpty()) {
|
|
410
|
+
resultNotification = buildSuccessNotification(totalFiles)
|
|
411
|
+
historyItem = NotificationHistoryItem(
|
|
412
|
+
title = Localization.getString("uploadCompletedTitle", "Upload completed"),
|
|
413
|
+
message = Localization.getString("uploadCompletedMessage", "Successfully uploaded {0} file(s).", totalFiles),
|
|
414
|
+
timestamp = System.currentTimeMillis(),
|
|
415
|
+
type = "success"
|
|
416
|
+
)
|
|
417
|
+
} else {
|
|
418
|
+
// Report exactly what happened — counts plus the first real error (e.g.
|
|
419
|
+
// "Server returned error code: 503") — never a sanitized stand-in.
|
|
420
|
+
val message = if (totalFiles > 1) {
|
|
421
|
+
Localization.getString("uploadFailedMessage", "Uploaded {0} of {1} file(s) — {2} failed. {3}", uploadedCount, totalFiles, totalFiles - uploadedCount, failures.first())
|
|
422
|
+
} else {
|
|
423
|
+
failures.first()
|
|
424
|
+
}
|
|
425
|
+
resultNotification = buildFailureNotification(message)
|
|
426
|
+
historyItem = NotificationHistoryItem(
|
|
427
|
+
title = Localization.getString("uploadFailedTitle", "Upload failed"),
|
|
428
|
+
message = message,
|
|
429
|
+
timestamp = System.currentTimeMillis(),
|
|
430
|
+
type = "failure"
|
|
431
|
+
)
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (SYSTEM_NOTIFICATIONS_ENABLED) {
|
|
435
|
+
try {
|
|
436
|
+
NotificationManagerCompat.from(this).notify(RESULT_NOTIFICATION_ID, resultNotification)
|
|
437
|
+
} catch (se: SecurityException) {
|
|
438
|
+
// Ignore
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
emitUploadEvent(
|
|
442
|
+
if (failures.isEmpty()) "success" else "failure",
|
|
443
|
+
100,
|
|
444
|
+
attemptedFiles,
|
|
445
|
+
totalFiles,
|
|
446
|
+
message = historyItem.message
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
// Terminal state for BOTH outcomes: nothing is in flight anymore, so mark the
|
|
450
|
+
// persisted state completed. The old code saved completed=false after a failure
|
|
451
|
+
// (and never cleared it), which made every later service start resurrect a stale
|
|
452
|
+
// frozen progress notification for this long-dead batch.
|
|
453
|
+
saveUploadState(UploadState(
|
|
454
|
+
uris = uriStrings,
|
|
455
|
+
uploadUrl = uploadUrl,
|
|
456
|
+
totalFiles = totalFiles,
|
|
457
|
+
currentFile = attemptedFiles,
|
|
458
|
+
progress = if (totalBytes > 0) ((bytesProcessed * 100) / totalBytes).toInt().coerceIn(0, 100) else 100,
|
|
459
|
+
completed = true,
|
|
460
|
+
timestamp = System.currentTimeMillis()
|
|
461
|
+
))
|
|
462
|
+
|
|
463
|
+
addToNotificationHistory(historyItem)
|
|
464
|
+
|
|
465
|
+
completeJob(startId)
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/** Terminal outcome of one file's upload attempts. */
|
|
469
|
+
private class UploadFailure(val message: String, val abortBatch: Boolean)
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Uploads a single file, retrying transient failures with capped exponential backoff.
|
|
473
|
+
*
|
|
474
|
+
* This is the root-cause fix for batch uploads dying with "Server returned error
|
|
475
|
+
* code: 503": a batch fires one POST per file back-to-back the instant the previous
|
|
476
|
+
* response lands, and burst-limited endpoints (httpbin.org included) shed those
|
|
477
|
+
* immediate follow-up requests with 503 Service Unavailable. The HTTP spec defines
|
|
478
|
+
* 503 as a TRANSIENT condition to be retried after a delay (with an optional
|
|
479
|
+
* Retry-After hint) — but the old code had no retry at all and treated the first
|
|
480
|
+
* 503 as fatal for the entire batch. A single upload is one request and rarely
|
|
481
|
+
* trips the limiter, which is why only multi-file uploads failed.
|
|
482
|
+
*
|
|
483
|
+
* Nothing is masked: a server that keeps failing still surfaces its real status
|
|
484
|
+
* code in the failure notification once [MAX_UPLOAD_ATTEMPTS] is exhausted, and
|
|
485
|
+
* permanent errors (unreadable file, non-transient 4xx) fail immediately.
|
|
486
|
+
*
|
|
487
|
+
* @return null on success, or an [UploadFailure] carrying the final error message
|
|
488
|
+
* and whether the whole batch should stop (endpoint-level failures).
|
|
489
|
+
*/
|
|
490
|
+
private fun uploadFileWithRetry(
|
|
491
|
+
uri: Uri,
|
|
492
|
+
fileSize: Long,
|
|
493
|
+
uploadUrl: String,
|
|
494
|
+
onProgress: (bytesWritten: Long) -> Unit,
|
|
495
|
+
onRetryScheduled: (attempt: Int) -> Unit
|
|
496
|
+
): UploadFailure? {
|
|
497
|
+
val mimeType = getMimeType(uri)
|
|
498
|
+
val fileName = uri.lastPathSegment ?: "media_${System.currentTimeMillis()}"
|
|
499
|
+
|
|
500
|
+
// A file that can't be opened at all (deleted / permission revoked) is a
|
|
501
|
+
// permanent failure — fail fast instead of burning retry cycles on it.
|
|
502
|
+
try {
|
|
503
|
+
contentResolver.openInputStream(uri)?.close()
|
|
504
|
+
?: return UploadFailure(Localization.getString("uploadErrorCouldNotOpenStream", "Could not open the file to upload"), abortBatch = false)
|
|
505
|
+
} catch (e: Exception) {
|
|
506
|
+
return UploadFailure(
|
|
507
|
+
Localization.getString("uploadErrorCouldNotOpenFile", "Could not open file: {0}", e.localizedMessage ?: uri.toString()),
|
|
508
|
+
abortBatch = false
|
|
509
|
+
)
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
var lastErrorMessage = Localization.getString("errorUnknown", "An error occurred")
|
|
513
|
+
var lastWasEndpointUnreachable = false
|
|
514
|
+
|
|
515
|
+
for (attempt in 1..MAX_UPLOAD_ATTEMPTS) {
|
|
516
|
+
var retryDelayMs = -1L
|
|
517
|
+
|
|
518
|
+
try {
|
|
519
|
+
// Replayable body: a FRESH input stream is opened on every write, so this
|
|
520
|
+
// attempt — and OkHttp's own stale-pooled-connection recovery — always
|
|
521
|
+
// sends the complete file, never a previously drained stream.
|
|
522
|
+
val progressBody = ProgressRequestBody(
|
|
523
|
+
contentType = mimeType.toMediaTypeOrNull(),
|
|
524
|
+
contentResolver = contentResolver,
|
|
525
|
+
uri = uri,
|
|
526
|
+
declaredLength = fileSize
|
|
527
|
+
) { bytesWritten, _ ->
|
|
528
|
+
onProgress(bytesWritten)
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
val requestBody = MultipartBody.Builder()
|
|
532
|
+
.setType(MultipartBody.FORM)
|
|
533
|
+
.addFormDataPart("file", fileName, progressBody)
|
|
534
|
+
.build()
|
|
535
|
+
|
|
536
|
+
val request = Request.Builder()
|
|
537
|
+
.url(uploadUrl)
|
|
538
|
+
.post(requestBody)
|
|
539
|
+
.build()
|
|
540
|
+
|
|
541
|
+
client.newCall(request).execute().use { response ->
|
|
542
|
+
if (response.isSuccessful) return null
|
|
543
|
+
|
|
544
|
+
lastErrorMessage = Localization.getString("uploadErrorServerCode", "Server returned error code: {0}", response.code)
|
|
545
|
+
lastWasEndpointUnreachable = false
|
|
546
|
+
if (isTransientHttpCode(response.code) && attempt < MAX_UPLOAD_ATTEMPTS) {
|
|
547
|
+
// Prefer the server's own Retry-After hint (503/429 commonly carry
|
|
548
|
+
// one) over the local backoff curve, within sane bounds.
|
|
549
|
+
retryDelayMs = response.header("Retry-After")?.trim()?.toLongOrNull()
|
|
550
|
+
?.times(1000L)
|
|
551
|
+
?.coerceIn(BASE_RETRY_DELAY_MS, MAX_RETRY_AFTER_MS)
|
|
552
|
+
?: backoffDelayMs(attempt)
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
} catch (e: FileNotFoundException) {
|
|
556
|
+
// The file vanished between attempts — permanent, never retried.
|
|
557
|
+
return UploadFailure(
|
|
558
|
+
Localization.getString("couldNotReadFile", "Could not read file: {0}", e.localizedMessage ?: uri.toString()),
|
|
559
|
+
abortBatch = false
|
|
560
|
+
)
|
|
561
|
+
} catch (e: IOException) {
|
|
562
|
+
// Transport-level failure (timeout, reset, dead pooled connection) —
|
|
563
|
+
// transient by nature; retried on a fresh connection with a full body.
|
|
564
|
+
e.printStackTrace()
|
|
565
|
+
lastErrorMessage = Localization.getString("uploadErrorNetwork", "Network error: {0}", e.localizedMessage ?: e.javaClass.simpleName)
|
|
566
|
+
lastWasEndpointUnreachable = isEndpointUnreachable(e)
|
|
567
|
+
if (attempt < MAX_UPLOAD_ATTEMPTS) {
|
|
568
|
+
retryDelayMs = backoffDelayMs(attempt)
|
|
569
|
+
}
|
|
570
|
+
} catch (e: Exception) {
|
|
571
|
+
// Non-I/O failures are programming errors, never transient — don't retry.
|
|
572
|
+
e.printStackTrace()
|
|
573
|
+
return UploadFailure(e.localizedMessage ?: Localization.getString("errorUnknown", "An error occurred"), abortBatch = false)
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if (retryDelayMs < 0) break // permanent failure or attempts exhausted
|
|
577
|
+
|
|
578
|
+
onRetryScheduled(attempt)
|
|
579
|
+
try {
|
|
580
|
+
Thread.sleep(retryDelayMs)
|
|
581
|
+
} catch (ie: InterruptedException) {
|
|
582
|
+
Thread.currentThread().interrupt() // service tearing down — stop retrying
|
|
583
|
+
break
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
return UploadFailure(lastErrorMessage, abortBatch = lastWasEndpointUnreachable)
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/** 408/429/5xx are transient per the HTTP spec; other 4xx are permanent client errors. */
|
|
591
|
+
private fun isTransientHttpCode(code: Int): Boolean =
|
|
592
|
+
code == 408 || code == 429 || code in 500..599
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Failures every remaining file in the batch would hit identically (DNS resolution,
|
|
596
|
+
* refused connection, TLS handshake) — continuing the batch would only repeat the
|
|
597
|
+
* same retry cycle once per file.
|
|
598
|
+
*/
|
|
599
|
+
private fun isEndpointUnreachable(e: IOException): Boolean =
|
|
600
|
+
e is UnknownHostException || e is ConnectException ||
|
|
601
|
+
e is NoRouteToHostException || e is SSLException
|
|
602
|
+
|
|
603
|
+
/** 1s, 2s, 4s… capped exponential backoff between attempts. */
|
|
604
|
+
private fun backoffDelayMs(attempt: Int): Long =
|
|
605
|
+
(BASE_RETRY_DELAY_MS shl (attempt - 1)).coerceAtMost(MAX_RETRY_DELAY_MS)
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* PendingIntent that reopens the host app when a notification is tapped. There is
|
|
609
|
+
* no navigation stack for this library to target (it doesn't own the app's routing),
|
|
610
|
+
* so the most useful universal behavior is bringing the app itself to the foreground;
|
|
611
|
+
* host apps that want to route further can inspect their own launch state for that.
|
|
612
|
+
* Returns null (leaving the notification tap a no-op) if the OS can't resolve a
|
|
613
|
+
* launch intent for this package.
|
|
614
|
+
*/
|
|
615
|
+
private fun contentPendingIntent(): PendingIntent? {
|
|
616
|
+
val launchIntent = packageManager.getLaunchIntentForPackage(packageName) ?: return null
|
|
617
|
+
launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
|
618
|
+
return PendingIntent.getActivity(
|
|
619
|
+
this,
|
|
620
|
+
NOTIFICATION_ID,
|
|
621
|
+
launchIntent,
|
|
622
|
+
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
|
623
|
+
)
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
private fun saveUploadState(state: UploadState) {
|
|
627
|
+
try {
|
|
628
|
+
val json = gson.toJson(state)
|
|
629
|
+
sharedPreferences.edit().putString("upload_state", json).apply()
|
|
630
|
+
} catch (e: Exception) {
|
|
631
|
+
// Failed to save state
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
private fun clearUploadState() {
|
|
636
|
+
sharedPreferences.edit().remove("upload_state").apply()
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
private fun addToNotificationHistory(item: NotificationHistoryItem) {
|
|
640
|
+
try {
|
|
641
|
+
val historyJson = sharedPreferences.getString("notification_history", null)
|
|
642
|
+
val history = if (!historyJson.isNullOrEmpty()) {
|
|
643
|
+
val array = gson.fromJson(historyJson, Array<NotificationHistoryItem>::class.java)
|
|
644
|
+
array?.toMutableList() ?: mutableListOf()
|
|
645
|
+
} else {
|
|
646
|
+
mutableListOf()
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
history.add(0, item) // Add to beginning to show most recent first
|
|
650
|
+
if (history.size > 50) { // Limit history to 50 items
|
|
651
|
+
history.subList(50, history.size).clear()
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
val updatedJson = gson.toJson(history)
|
|
655
|
+
sharedPreferences.edit().putString("notification_history", updatedJson).apply()
|
|
656
|
+
} catch (e: Exception) {
|
|
657
|
+
// Failed to update history
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
private fun getNotificationHistory(): String {
|
|
662
|
+
return sharedPreferences.getString("notification_history", "[]") ?: "[]"
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
private fun clearNotificationHistory() {
|
|
666
|
+
sharedPreferences.edit().remove("notification_history").apply()
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
@androidx.annotation.Keep
|
|
670
|
+
data class UploadState(
|
|
671
|
+
val uris: List<String>,
|
|
672
|
+
val uploadUrl: String,
|
|
673
|
+
val totalFiles: Int,
|
|
674
|
+
val currentFile: Int,
|
|
675
|
+
val progress: Int,
|
|
676
|
+
val completed: Boolean,
|
|
677
|
+
val timestamp: Long
|
|
678
|
+
)
|
|
679
|
+
|
|
680
|
+
@androidx.annotation.Keep
|
|
681
|
+
data class NotificationHistoryItem(
|
|
682
|
+
val title: String,
|
|
683
|
+
val message: String,
|
|
684
|
+
val timestamp: Long,
|
|
685
|
+
val type: String // "success", "failure", "progress"
|
|
686
|
+
)
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* White-label notification settings loaded from the shared camera config;
|
|
690
|
+
* defaults reproduce the stock channels/icon exactly.
|
|
691
|
+
*/
|
|
692
|
+
private val notificationConfig: CameraConfig.NotificationConfig by lazy {
|
|
693
|
+
CameraTheme.loadConfig(this).notifications
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* Small status icon: a host-app drawable when configured (and resolvable),
|
|
698
|
+
* otherwise the built-in ic_stat_upload. Must be a plain monochrome vector —
|
|
699
|
+
* adaptive/launcher icons are silently dropped by the system.
|
|
700
|
+
*/
|
|
701
|
+
private fun smallIconRes(): Int {
|
|
702
|
+
val name = notificationConfig.smallIconResourceName ?: return R.drawable.ic_stat_upload
|
|
703
|
+
val resolved = resources.getIdentifier(name, "drawable", packageName)
|
|
704
|
+
return if (resolved != 0) resolved else R.drawable.ic_stat_upload
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
private fun NotificationCompat.Builder.applyAccentColor(): NotificationCompat.Builder {
|
|
708
|
+
notificationConfig.accentColor?.let { setColor(it) }
|
|
709
|
+
return this
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
private fun buildProgressNotification(
|
|
713
|
+
progress: Int,
|
|
714
|
+
fileIndex: Int,
|
|
715
|
+
totalFiles: Int,
|
|
716
|
+
statusText: String? = null
|
|
717
|
+
): Notification {
|
|
718
|
+
val contentText = statusText ?: if (fileIndex > 0) {
|
|
719
|
+
Localization.getString("uploadingFileProgress", "Uploading file {0} of {1} ({2}%)", fileIndex, totalFiles, progress)
|
|
720
|
+
} else {
|
|
721
|
+
Localization.getString("preparingFilesProgress", "Preparing files... ({0}%)", progress)
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
return NotificationCompat.Builder(this, notificationConfig.progressChannelId)
|
|
725
|
+
.setContentTitle(Localization.getString("uploadingMediaTitle", "Uploading media"))
|
|
726
|
+
.setContentText(contentText)
|
|
727
|
+
.setSmallIcon(smallIconRes())
|
|
728
|
+
.applyAccentColor()
|
|
729
|
+
.setOngoing(true)
|
|
730
|
+
.setProgress(100, progress, false)
|
|
731
|
+
.setPriority(NotificationCompat.PRIORITY_LOW)
|
|
732
|
+
// Show the foreground-service notification IMMEDIATELY. Since Android 12 the system
|
|
733
|
+
// otherwise defers a foreground-service notification by up to ~10 seconds to avoid
|
|
734
|
+
// spamming for short tasks — so an upload that finishes quickly would post, run and
|
|
735
|
+
// complete without its notification ever becoming visible. This is the main reason
|
|
736
|
+
// "no notification shows up" during uploads.
|
|
737
|
+
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)
|
|
738
|
+
.setContentIntent(contentPendingIntent())
|
|
739
|
+
.build()
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
private fun buildSuccessNotification(totalFiles: Int): Notification {
|
|
743
|
+
return NotificationCompat.Builder(this, notificationConfig.resultChannelId)
|
|
744
|
+
.setContentTitle(Localization.getString("uploadCompletedTitle", "Upload completed"))
|
|
745
|
+
.setContentText(Localization.getString("uploadCompletedMessage", "Successfully uploaded {0} file(s).", totalFiles))
|
|
746
|
+
.setSmallIcon(smallIconRes())
|
|
747
|
+
.applyAccentColor()
|
|
748
|
+
.setOngoing(false) // Let the user swipe it away
|
|
749
|
+
.setAutoCancel(true) // Dismiss on tap
|
|
750
|
+
.setProgress(0, 0, false) // Hide progress bar
|
|
751
|
+
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
|
752
|
+
.setContentIntent(contentPendingIntent())
|
|
753
|
+
.build()
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
private fun buildFailureNotification(errorMessage: String): Notification {
|
|
757
|
+
return NotificationCompat.Builder(this, notificationConfig.resultChannelId)
|
|
758
|
+
.setContentTitle(Localization.getString("uploadFailedTitle", "Upload failed"))
|
|
759
|
+
.setContentText(errorMessage)
|
|
760
|
+
// Batch failure messages ("Uploaded 2 of 5 file(s) — 3 failed. Server returned
|
|
761
|
+
// error code: 503") don't fit one collapsed line; let the shade expand them.
|
|
762
|
+
.setStyle(NotificationCompat.BigTextStyle().bigText(errorMessage))
|
|
763
|
+
.setSmallIcon(smallIconRes())
|
|
764
|
+
.applyAccentColor()
|
|
765
|
+
.setOngoing(false) // Let the user swipe it away
|
|
766
|
+
.setAutoCancel(true) // Dismiss on tap
|
|
767
|
+
.setProgress(0, 0, false) // Hide progress bar
|
|
768
|
+
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
|
769
|
+
.setContentIntent(contentPendingIntent())
|
|
770
|
+
.build()
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* Emits an in-app upload progress event to React Native ("onUploadProgress").
|
|
775
|
+
* This is the replacement surface for the (currently disabled) system
|
|
776
|
+
* notifications: the example app renders a slim progress banner from these.
|
|
777
|
+
* Payload: { status: "progress"|"success"|"failure", progress: 0-100,
|
|
778
|
+
* currentFile, totalFiles, message? }.
|
|
779
|
+
*
|
|
780
|
+
* Emission is best-effort: the service may outlive the React context (app
|
|
781
|
+
* backgrounded/killed) — uploads must never fail because the UI went away.
|
|
782
|
+
*/
|
|
783
|
+
private fun emitUploadEvent(
|
|
784
|
+
status: String,
|
|
785
|
+
progress: Int,
|
|
786
|
+
currentFile: Int,
|
|
787
|
+
totalFiles: Int,
|
|
788
|
+
message: String? = null
|
|
789
|
+
) {
|
|
790
|
+
val context = reactContext ?: return
|
|
791
|
+
if (!context.hasActiveReactInstance()) return
|
|
792
|
+
try {
|
|
793
|
+
val payload: WritableMap = Arguments.createMap().apply {
|
|
794
|
+
putString("status", status)
|
|
795
|
+
putInt("progress", progress)
|
|
796
|
+
putInt("currentFile", currentFile)
|
|
797
|
+
putInt("totalFiles", totalFiles)
|
|
798
|
+
message?.let { putString("message", it) }
|
|
799
|
+
}
|
|
800
|
+
context
|
|
801
|
+
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
802
|
+
.emit("onUploadProgress", payload)
|
|
803
|
+
} catch (e: Exception) {
|
|
804
|
+
// Bridge torn down mid-upload — keep uploading silently.
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
companion object {
|
|
809
|
+
// ── System-notification kill switch ─────────────────────────────────────
|
|
810
|
+
// false = the entire system-notification surface (channels, foreground
|
|
811
|
+
// promotion, progress + result notify() calls) is skipped and upload
|
|
812
|
+
// progress is reported to the app via "onUploadProgress" events instead.
|
|
813
|
+
// The notification implementation below is intentionally left fully
|
|
814
|
+
// intact. To RE-ENABLE system notifications: flip this to true AND
|
|
815
|
+
// restore the POST_NOTIFICATIONS <uses-permission> in the library
|
|
816
|
+
// AndroidManifest.xml (currently commented out) — the permission
|
|
817
|
+
// requests in CustomCameraModule.startUpload / CustomCameraActivity
|
|
818
|
+
// .requiredPermissions and the startForegroundService launch path are
|
|
819
|
+
// gated on this same flag and come back automatically.
|
|
820
|
+
const val SYSTEM_NOTIFICATIONS_ENABLED = false
|
|
821
|
+
|
|
822
|
+
// React context handle for in-app progress events, set by
|
|
823
|
+
// CustomCameraModule.startUpload before the service starts (same pattern
|
|
824
|
+
// as GlobalAudioPlayer.reactContext). The service itself has no React
|
|
825
|
+
// instance; a stale/null context simply means no events are emitted.
|
|
826
|
+
@Volatile
|
|
827
|
+
var reactContext: ReactApplicationContext? = null
|
|
828
|
+
|
|
829
|
+
const val CHANNEL_ID = "UploadServiceChannel"
|
|
830
|
+
// Dedicated HIGH-importance channel + id for the terminal success/failure notice, kept
|
|
831
|
+
// separate from the silent, low-importance progress notification so it can heads-up.
|
|
832
|
+
const val RESULT_CHANNEL_ID = "UploadResultChannel"
|
|
833
|
+
const val NOTIFICATION_ID = 90812
|
|
834
|
+
const val RESULT_NOTIFICATION_ID = 90813
|
|
835
|
+
const val ACTION_START_UPLOAD = "com.customcamera.action.START_UPLOAD"
|
|
836
|
+
const val EXTRA_URIS = "com.customcamera.extra.URIS"
|
|
837
|
+
const val EXTRA_UPLOAD_URL = "com.customcamera.extra.UPLOAD_URL"
|
|
838
|
+
|
|
839
|
+
// Action to get notification history
|
|
840
|
+
const val ACTION_GET_NOTIFICATION_HISTORY = "com.customcamera.action.GET_NOTIFICATION_HISTORY"
|
|
841
|
+
const val EXTRA_NOTIFICATION_HISTORY = "com.customcamera.extra.NOTIFICATION_HISTORY"
|
|
842
|
+
|
|
843
|
+
// ── Transient-failure retry policy ──────────────────────────────────────
|
|
844
|
+
// 4 attempts = 1 initial + 3 retries with waits of 1s/2s/4s (or the server's
|
|
845
|
+
// Retry-After, capped). Worst case adds ~7s per file before the REAL error is
|
|
846
|
+
// reported — long enough to ride out burst throttling (the multi-file 503),
|
|
847
|
+
// short enough that a genuinely down server still fails visibly and promptly.
|
|
848
|
+
private const val MAX_UPLOAD_ATTEMPTS = 4
|
|
849
|
+
private const val BASE_RETRY_DELAY_MS = 1000L
|
|
850
|
+
private const val MAX_RETRY_DELAY_MS = 8000L
|
|
851
|
+
private const val MAX_RETRY_AFTER_MS = 15000L
|
|
852
|
+
|
|
853
|
+
// NotificationManager rate-limits enqueues per package and SILENTLY sheds the
|
|
854
|
+
// excess — which can include the terminal success/failure post. Progress updates
|
|
855
|
+
// are therefore spaced at least this far apart (≤5 posts/second).
|
|
856
|
+
private const val MIN_NOTIFY_INTERVAL_MS = 200L
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/**
|
|
861
|
+
* Streams a content [uri] into the request body while reporting write progress.
|
|
862
|
+
*
|
|
863
|
+
* The body is REPLAYABLE: [writeTo] opens a fresh stream from the resolver on every
|
|
864
|
+
* call instead of capturing one pre-opened InputStream. OkHttp legitimately invokes
|
|
865
|
+
* writeTo() more than once — e.g. when it transparently retries after picking a stale
|
|
866
|
+
* pooled connection (possible only from a batch's second request onwards) — and
|
|
867
|
+
* [UploadService]'s transient-error retries re-send the same body too. The previous
|
|
868
|
+
* single-InputStream design made every such replay transmit an already-drained,
|
|
869
|
+
* closed stream: a truncated request a server can only reject.
|
|
870
|
+
*/
|
|
871
|
+
class ProgressRequestBody(
|
|
872
|
+
private val contentType: MediaType?,
|
|
873
|
+
private val contentResolver: ContentResolver,
|
|
874
|
+
private val uri: Uri,
|
|
875
|
+
private val declaredLength: Long,
|
|
876
|
+
private val onProgress: (bytesWritten: Long, totalBytes: Long) -> Unit
|
|
877
|
+
) : RequestBody() {
|
|
878
|
+
|
|
879
|
+
override fun contentType(): MediaType? = contentType
|
|
880
|
+
|
|
881
|
+
// -1 (unknown) makes OkHttp fall back to chunked transfer encoding. Declaring 0 for
|
|
882
|
+
// a file whose size couldn't be resolved would promise "Content-Length: 0" and make
|
|
883
|
+
// OkHttp abort the call the moment the first real byte is written.
|
|
884
|
+
override fun contentLength(): Long = if (declaredLength > 0) declaredLength else -1L
|
|
885
|
+
|
|
886
|
+
override fun writeTo(sink: BufferedSink) {
|
|
887
|
+
val input = contentResolver.openInputStream(uri)
|
|
888
|
+
?: throw IOException("Could not open input stream for URI: $uri")
|
|
889
|
+
// 16 KB transfer buffer: fewer read/write iterations and fewer onProgress
|
|
890
|
+
// callbacks per file at no memory cost, improving upload throughput and CPU use
|
|
891
|
+
// on low-end devices. The consumer already throttles notification/state updates,
|
|
892
|
+
// so cadence to JS/notifications is unchanged.
|
|
893
|
+
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
|
|
894
|
+
var bytesWritten = 0L
|
|
895
|
+
input.use { stream ->
|
|
896
|
+
var read: Int
|
|
897
|
+
while (stream.read(buffer).also { read = it } != -1) {
|
|
898
|
+
sink.write(buffer, 0, read)
|
|
899
|
+
bytesWritten += read
|
|
900
|
+
onProgress(bytesWritten, declaredLength)
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
companion object {
|
|
906
|
+
// 16 KB — the standard sweet spot for streamed I/O throughput.
|
|
907
|
+
private const val DEFAULT_BUFFER_SIZE = 16 * 1024
|
|
908
|
+
}
|
|
909
|
+
}
|