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.
Files changed (506) hide show
  1. package/CustomCamera.podspec +20 -0
  2. package/LICENSE +20 -0
  3. package/README.md +833 -0
  4. package/android/build.gradle +135 -0
  5. package/android/consumer-rules.pro +47 -0
  6. package/android/src/main/AndroidManifest.xml +72 -0
  7. package/android/src/main/java/android/print/PdfPrint.java +51 -0
  8. package/android/src/main/java/com/customcamera/AnimatedImageViewManager.kt +199 -0
  9. package/android/src/main/java/com/customcamera/AudioPlayerView.kt +322 -0
  10. package/android/src/main/java/com/customcamera/AudioRecorder.kt +506 -0
  11. package/android/src/main/java/com/customcamera/BarcodeScannerActivity.kt +361 -0
  12. package/android/src/main/java/com/customcamera/BlurBackgroundView.kt +199 -0
  13. package/android/src/main/java/com/customcamera/CacheCleanupManager.kt +386 -0
  14. package/android/src/main/java/com/customcamera/CameraAspectRatioMode.kt +77 -0
  15. package/android/src/main/java/com/customcamera/CameraAsset.kt +211 -0
  16. package/android/src/main/java/com/customcamera/CameraTheme.kt +479 -0
  17. package/android/src/main/java/com/customcamera/CompressionCacheStore.kt +504 -0
  18. package/android/src/main/java/com/customcamera/CompressionFileNameUtils.kt +7 -0
  19. package/android/src/main/java/com/customcamera/CompressionProfile.kt +195 -0
  20. package/android/src/main/java/com/customcamera/CustomCameraActivity.kt +7291 -0
  21. package/android/src/main/java/com/customcamera/CustomCameraFileProvider.kt +9 -0
  22. package/android/src/main/java/com/customcamera/CustomCameraModule.kt +2479 -0
  23. package/android/src/main/java/com/customcamera/CustomCameraPackage.kt +53 -0
  24. package/android/src/main/java/com/customcamera/DisplayUtils.kt +9 -0
  25. package/android/src/main/java/com/customcamera/DocumentPreviewView.kt +846 -0
  26. package/android/src/main/java/com/customcamera/DrawingView.kt +214 -0
  27. package/android/src/main/java/com/customcamera/EmojiPickerData.kt +228 -0
  28. package/android/src/main/java/com/customcamera/FilterStripAdapter.kt +158 -0
  29. package/android/src/main/java/com/customcamera/GifSupport.kt +215 -0
  30. package/android/src/main/java/com/customcamera/GlobalAudioPlayer.kt +320 -0
  31. package/android/src/main/java/com/customcamera/ImageExporter.kt +1394 -0
  32. package/android/src/main/java/com/customcamera/ImageFormatSupport.kt +163 -0
  33. package/android/src/main/java/com/customcamera/Localization.kt +206 -0
  34. package/android/src/main/java/com/customcamera/MediaAdapters.kt +758 -0
  35. package/android/src/main/java/com/customcamera/MediaCacheManager.kt +782 -0
  36. package/android/src/main/java/com/customcamera/MediaExportFormats.kt +201 -0
  37. package/android/src/main/java/com/customcamera/MediaFileIo.kt +310 -0
  38. package/android/src/main/java/com/customcamera/MediaMetadataStore.kt +494 -0
  39. package/android/src/main/java/com/customcamera/MediaPreviewEditor.kt +14865 -0
  40. package/android/src/main/java/com/customcamera/MediaPublisher.kt +423 -0
  41. package/android/src/main/java/com/customcamera/MediaStorageWorkflow.kt +722 -0
  42. package/android/src/main/java/com/customcamera/MediaStoreRepository.kt +378 -0
  43. package/android/src/main/java/com/customcamera/MediaThumbnailView.kt +286 -0
  44. package/android/src/main/java/com/customcamera/MediaUtils.kt +37 -0
  45. package/android/src/main/java/com/customcamera/NativeVideoViewManager.kt +845 -0
  46. package/android/src/main/java/com/customcamera/OfficeDocumentConverter.kt +2791 -0
  47. package/android/src/main/java/com/customcamera/PendingCaptureCrops.kt +88 -0
  48. package/android/src/main/java/com/customcamera/PowerPointPreview.kt +4077 -0
  49. package/android/src/main/java/com/customcamera/RecentPickerStore.kt +74 -0
  50. package/android/src/main/java/com/customcamera/SecureFileEraser.kt +411 -0
  51. package/android/src/main/java/com/customcamera/SecureKeyModule.kt +88 -0
  52. package/android/src/main/java/com/customcamera/SecureMediaDeleter.kt +493 -0
  53. package/android/src/main/java/com/customcamera/StickerPickerData.kt +107 -0
  54. package/android/src/main/java/com/customcamera/SvgDrawable.kt +175 -0
  55. package/android/src/main/java/com/customcamera/SvgIconViewManager.kt +77 -0
  56. package/android/src/main/java/com/customcamera/TextEntryController.kt +553 -0
  57. package/android/src/main/java/com/customcamera/ThumbnailLoader.kt +455 -0
  58. package/android/src/main/java/com/customcamera/UploadService.kt +909 -0
  59. package/android/src/main/java/com/customcamera/VideoEditUtils.kt +210 -0
  60. package/android/src/main/java/com/customcamera/VideoRotationTranscoder.kt +1992 -0
  61. package/android/src/main/java/com/customcamera/gif/AnimatedGifEncoder.kt +473 -0
  62. package/android/src/main/java/com/customcamera/gif/GifDecoder.kt +690 -0
  63. package/android/src/main/java/com/customcamera/gif/GifFramesDrawable.kt +160 -0
  64. package/android/src/main/java/com/customcamera/gif/GifHttp.kt +112 -0
  65. package/android/src/main/java/com/customcamera/gif/GifImageLoader.kt +186 -0
  66. package/android/src/main/java/com/customcamera/gif/GifModels.kt +74 -0
  67. package/android/src/main/java/com/customcamera/gif/GifProvider.kt +49 -0
  68. package/android/src/main/java/com/customcamera/gif/GifProviders.kt +27 -0
  69. package/android/src/main/java/com/customcamera/gif/GifRepository.kt +124 -0
  70. package/android/src/main/java/com/customcamera/gif/GifSafety.kt +33 -0
  71. package/android/src/main/java/com/customcamera/gif/GiphyGifProvider.kt +146 -0
  72. package/android/src/main/java/com/customcamera/gif/LZWEncoder.kt +258 -0
  73. package/android/src/main/java/com/customcamera/gif/NeuQuant.kt +435 -0
  74. package/android/src/main/java/com/customcamera/gif/TenorGifProvider.kt +147 -0
  75. package/android/src/main/res/drawable/bg_brush_selected.xml +5 -0
  76. package/android/src/main/res/drawable/bg_color_swatch.xml +5 -0
  77. package/android/src/main/res/drawable/bg_editor_toolbar.xml +6 -0
  78. package/android/src/main/res/drawable/bg_icon_circle.xml +5 -0
  79. package/android/src/main/res/drawable/bg_icon_circle_light.xml +5 -0
  80. package/android/src/main/res/drawable/bg_mode_tab_active.xml +6 -0
  81. package/android/src/main/res/drawable/bg_pill.xml +6 -0
  82. package/android/src/main/res/drawable/bg_recording_dot.xml +6 -0
  83. package/android/src/main/res/drawable/bg_selection_badge.xml +8 -0
  84. package/android/src/main/res/drawable/bg_send_button.xml +5 -0
  85. package/android/src/main/res/drawable/bg_shutter_inner.xml +5 -0
  86. package/android/src/main/res/drawable/bg_shutter_recording.xml +6 -0
  87. package/android/src/main/res/drawable/bg_shutter_ring.xml +8 -0
  88. package/android/src/main/res/drawable/bg_shutter_video_idle.xml +5 -0
  89. package/android/src/main/res/drawable/bg_thumbnail.xml +9 -0
  90. package/android/src/main/res/drawable/ic_aspect_ratio.xml +17 -0
  91. package/android/src/main/res/drawable/ic_audio.xml +10 -0
  92. package/android/src/main/res/drawable/ic_back.xml +10 -0
  93. package/android/src/main/res/drawable/ic_brush_medium.xml +12 -0
  94. package/android/src/main/res/drawable/ic_brush_thick.xml +12 -0
  95. package/android/src/main/res/drawable/ic_brush_thin.xml +12 -0
  96. package/android/src/main/res/drawable/ic_check.xml +17 -0
  97. package/android/src/main/res/drawable/ic_chevron_down.xml +10 -0
  98. package/android/src/main/res/drawable/ic_chevron_up.xml +13 -0
  99. package/android/src/main/res/drawable/ic_clear_drawing.xml +10 -0
  100. package/android/src/main/res/drawable/ic_close.xml +10 -0
  101. package/android/src/main/res/drawable/ic_cover.xml +9 -0
  102. package/android/src/main/res/drawable/ic_crop.xml +10 -0
  103. package/android/src/main/res/drawable/ic_delete.xml +9 -0
  104. package/android/src/main/res/drawable/ic_document.xml +10 -0
  105. package/android/src/main/res/drawable/ic_done.xml +10 -0
  106. package/android/src/main/res/drawable/ic_download.xml +10 -0
  107. package/android/src/main/res/drawable/ic_emoji.xml +10 -0
  108. package/android/src/main/res/drawable/ic_filter.xml +10 -0
  109. package/android/src/main/res/drawable/ic_flash_auto.xml +7 -0
  110. package/android/src/main/res/drawable/ic_flash_off.xml +23 -0
  111. package/android/src/main/res/drawable/ic_flash_on.xml +16 -0
  112. package/android/src/main/res/drawable/ic_flip.xml +15 -0
  113. package/android/src/main/res/drawable/ic_gallery.xml +10 -0
  114. package/android/src/main/res/drawable/ic_gif.xml +24 -0
  115. package/android/src/main/res/drawable/ic_lock.xml +19 -0
  116. package/android/src/main/res/drawable/ic_lock_open.xml +22 -0
  117. package/android/src/main/res/drawable/ic_marker.xml +10 -0
  118. package/android/src/main/res/drawable/ic_more_vert.xml +18 -0
  119. package/android/src/main/res/drawable/ic_overlay_close.xml +19 -0
  120. package/android/src/main/res/drawable/ic_overlay_resize.xml +18 -0
  121. package/android/src/main/res/drawable/ic_pause.xml +10 -0
  122. package/android/src/main/res/drawable/ic_photo_mode.xml +10 -0
  123. package/android/src/main/res/drawable/ic_play.xml +10 -0
  124. package/android/src/main/res/drawable/ic_redo.xml +10 -0
  125. package/android/src/main/res/drawable/ic_rotate.xml +9 -0
  126. package/android/src/main/res/drawable/ic_send.xml +10 -0
  127. package/android/src/main/res/drawable/ic_share.xml +10 -0
  128. package/android/src/main/res/drawable/ic_stat_upload.xml +10 -0
  129. package/android/src/main/res/drawable/ic_sticker.xml +24 -0
  130. package/android/src/main/res/drawable/ic_text.xml +10 -0
  131. package/android/src/main/res/drawable/ic_text_align_center.xml +11 -0
  132. package/android/src/main/res/drawable/ic_text_align_left.xml +11 -0
  133. package/android/src/main/res/drawable/ic_text_align_right.xml +11 -0
  134. package/android/src/main/res/drawable/ic_text_bg.xml +11 -0
  135. package/android/src/main/res/drawable/ic_trim.xml +23 -0
  136. package/android/src/main/res/drawable/ic_undo.xml +10 -0
  137. package/android/src/main/res/drawable/ic_video_mode.xml +10 -0
  138. package/android/src/main/res/drawable/ic_video_note.xml +13 -0
  139. package/android/src/main/res/drawable/ic_volume_off.xml +9 -0
  140. package/android/src/main/res/drawable/ic_volume_on.xml +9 -0
  141. package/android/src/main/res/drawable/sticker_heart.xml +9 -0
  142. package/android/src/main/res/drawable/sticker_smile.xml +9 -0
  143. package/android/src/main/res/drawable/sticker_star.xml +9 -0
  144. package/android/src/main/res/drawable/sticker_thumbsup.xml +9 -0
  145. package/android/src/main/res/values/colors.xml +34 -0
  146. package/android/src/main/res/values/ids.xml +15 -0
  147. package/android/src/main/res/values/strings.xml +15 -0
  148. package/android/src/main/res/values/styles.xml +20 -0
  149. package/android/src/main/res/xml/file_paths.xml +34 -0
  150. package/ios/CustomCamera.h +5 -0
  151. package/ios/CustomCamera.mm +214 -0
  152. package/lib/module/AudioPlayerContext.js +233 -0
  153. package/lib/module/AudioPlayerContext.js.map +1 -0
  154. package/lib/module/CameraPreview.js +304 -0
  155. package/lib/module/CameraPreview.js.map +1 -0
  156. package/lib/module/NativeAnimatedImageView.js +41 -0
  157. package/lib/module/NativeAnimatedImageView.js.map +1 -0
  158. package/lib/module/NativeAudioPlayerView.js +33 -0
  159. package/lib/module/NativeAudioPlayerView.js.map +1 -0
  160. package/lib/module/NativeBlurView.js +35 -0
  161. package/lib/module/NativeBlurView.js.map +1 -0
  162. package/lib/module/NativeCustomCamera.js +128 -0
  163. package/lib/module/NativeCustomCamera.js.map +1 -0
  164. package/lib/module/NativeCustomCamera.web.js +143 -0
  165. package/lib/module/NativeCustomCamera.web.js.map +1 -0
  166. package/lib/module/NativeCustomVideoView.js +5 -0
  167. package/lib/module/NativeCustomVideoView.js.map +1 -0
  168. package/lib/module/NativeDocumentPreviewView.js +156 -0
  169. package/lib/module/NativeDocumentPreviewView.js.map +1 -0
  170. package/lib/module/NativeMediaThumbnailView.js +110 -0
  171. package/lib/module/NativeMediaThumbnailView.js.map +1 -0
  172. package/lib/module/NativeSvgIcon.js +32 -0
  173. package/lib/module/NativeSvgIcon.js.map +1 -0
  174. package/lib/module/ResponsivePreviewContainer.js +171 -0
  175. package/lib/module/ResponsivePreviewContainer.js.map +1 -0
  176. package/lib/module/accessibility.js +61 -0
  177. package/lib/module/accessibility.js.map +1 -0
  178. package/lib/module/config.js +352 -0
  179. package/lib/module/config.js.map +1 -0
  180. package/lib/module/cpuOptimization.js +296 -0
  181. package/lib/module/cpuOptimization.js.map +1 -0
  182. package/lib/module/db/DatabaseManager.js +191 -0
  183. package/lib/module/db/DatabaseManager.js.map +1 -0
  184. package/lib/module/db/MediaMapper.js +284 -0
  185. package/lib/module/db/MediaMapper.js.map +1 -0
  186. package/lib/module/db/MediaRepository.js +242 -0
  187. package/lib/module/db/MediaRepository.js.map +1 -0
  188. package/lib/module/db/index.js +41 -0
  189. package/lib/module/db/index.js.map +1 -0
  190. package/lib/module/db/migrations.js +180 -0
  191. package/lib/module/db/migrations.js.map +1 -0
  192. package/lib/module/db/react-native-sqlcipher.d.js +2 -0
  193. package/lib/module/db/react-native-sqlcipher.d.js.map +1 -0
  194. package/lib/module/db/secureKey.js +87 -0
  195. package/lib/module/db/secureKey.js.map +1 -0
  196. package/lib/module/db/types.js +57 -0
  197. package/lib/module/db/types.js.map +1 -0
  198. package/lib/module/db/useMediaDatabase.js +123 -0
  199. package/lib/module/db/useMediaDatabase.js.map +1 -0
  200. package/lib/module/defaultIcons.js +2259 -0
  201. package/lib/module/defaultIcons.js.map +1 -0
  202. package/lib/module/deviceCapabilities.js +408 -0
  203. package/lib/module/deviceCapabilities.js.map +1 -0
  204. package/lib/module/errors.js +267 -0
  205. package/lib/module/errors.js.map +1 -0
  206. package/lib/module/iconConfig.js +796 -0
  207. package/lib/module/iconConfig.js.map +1 -0
  208. package/lib/module/icons.js +514 -0
  209. package/lib/module/icons.js.map +1 -0
  210. package/lib/module/index.js +251 -0
  211. package/lib/module/index.js.map +1 -0
  212. package/lib/module/jest-globals.d.js +2 -0
  213. package/lib/module/jest-globals.d.js.map +1 -0
  214. package/lib/module/library/MediaLibrary.js +107 -0
  215. package/lib/module/library/MediaLibrary.js.map +1 -0
  216. package/lib/module/library/MediaLibraryScreen.js +2597 -0
  217. package/lib/module/library/MediaLibraryScreen.js.map +1 -0
  218. package/lib/module/library/components/AudioPreview.js +74 -0
  219. package/lib/module/library/components/AudioPreview.js.map +1 -0
  220. package/lib/module/library/components/DocumentPreview.js +331 -0
  221. package/lib/module/library/components/DocumentPreview.js.map +1 -0
  222. package/lib/module/library/components/ExpandableFab.js +343 -0
  223. package/lib/module/library/components/ExpandableFab.js.map +1 -0
  224. package/lib/module/library/components/FileTiles.js +251 -0
  225. package/lib/module/library/components/FileTiles.js.map +1 -0
  226. package/lib/module/library/components/ImagePreview.js +70 -0
  227. package/lib/module/library/components/ImagePreview.js.map +1 -0
  228. package/lib/module/library/components/MediaMetadataHeader.js +175 -0
  229. package/lib/module/library/components/MediaMetadataHeader.js.map +1 -0
  230. package/lib/module/library/components/RecordAudioScreen.js +816 -0
  231. package/lib/module/library/components/RecordAudioScreen.js.map +1 -0
  232. package/lib/module/library/components/VideoPreview.js +453 -0
  233. package/lib/module/library/components/VideoPreview.js.map +1 -0
  234. package/lib/module/library/components/ViewerCounter.js +55 -0
  235. package/lib/module/library/components/ViewerCounter.js.map +1 -0
  236. package/lib/module/library/compressionEstimate.js +507 -0
  237. package/lib/module/library/compressionEstimate.js.map +1 -0
  238. package/lib/module/library/fileMetadata.js +196 -0
  239. package/lib/module/library/fileMetadata.js.map +1 -0
  240. package/lib/module/library/index.js +42 -0
  241. package/lib/module/library/index.js.map +1 -0
  242. package/lib/module/library/libraryIconRegistry.js +69 -0
  243. package/lib/module/library/libraryIconRegistry.js.map +1 -0
  244. package/lib/module/library/libraryIcons.js +858 -0
  245. package/lib/module/library/libraryIcons.js.map +1 -0
  246. package/lib/module/library/localizedFormat.js +181 -0
  247. package/lib/module/library/localizedFormat.js.map +1 -0
  248. package/lib/module/library/palette.js +225 -0
  249. package/lib/module/library/palette.js.map +1 -0
  250. package/lib/module/library/responsive.js +53 -0
  251. package/lib/module/library/responsive.js.map +1 -0
  252. package/lib/module/locales.js +416 -0
  253. package/lib/module/locales.js.map +1 -0
  254. package/lib/module/localization.js +1064 -0
  255. package/lib/module/localization.js.map +1 -0
  256. package/lib/module/logger.js +20 -0
  257. package/lib/module/logger.js.map +1 -0
  258. package/lib/module/lowMemoryCache.js +206 -0
  259. package/lib/module/lowMemoryCache.js.map +1 -0
  260. package/lib/module/mediaHistory.js +33 -0
  261. package/lib/module/mediaHistory.js.map +1 -0
  262. package/lib/module/mediaStorageService.js +402 -0
  263. package/lib/module/mediaStorageService.js.map +1 -0
  264. package/lib/module/memory.js +79 -0
  265. package/lib/module/memory.js.map +1 -0
  266. package/lib/module/nativeActions.js +307 -0
  267. package/lib/module/nativeActions.js.map +1 -0
  268. package/lib/module/nativeComponentSupport.js +61 -0
  269. package/lib/module/nativeComponentSupport.js.map +1 -0
  270. package/lib/module/networkPerformance.js +24 -0
  271. package/lib/module/networkPerformance.js.map +1 -0
  272. package/lib/module/package.json +1 -0
  273. package/lib/module/performance.js +104 -0
  274. package/lib/module/performance.js.map +1 -0
  275. package/lib/module/performanceMonitor.js +155 -0
  276. package/lib/module/performanceMonitor.js.map +1 -0
  277. package/lib/module/permissions.js +370 -0
  278. package/lib/module/permissions.js.map +1 -0
  279. package/lib/module/rendering.js +40 -0
  280. package/lib/module/rendering.js.map +1 -0
  281. package/lib/module/security.js +18 -0
  282. package/lib/module/security.js.map +1 -0
  283. package/lib/module/state.js +266 -0
  284. package/lib/module/state.js.map +1 -0
  285. package/lib/module/storage.js +400 -0
  286. package/lib/module/storage.js.map +1 -0
  287. package/lib/module/theme.js +390 -0
  288. package/lib/module/theme.js.map +1 -0
  289. package/lib/module/types.js +4 -0
  290. package/lib/module/types.js.map +1 -0
  291. package/lib/module/utils.js +115 -0
  292. package/lib/module/utils.js.map +1 -0
  293. package/lib/module/validation.js +288 -0
  294. package/lib/module/validation.js.map +1 -0
  295. package/lib/typescript/package.json +1 -0
  296. package/lib/typescript/src/AudioPlayerContext.d.ts +27 -0
  297. package/lib/typescript/src/AudioPlayerContext.d.ts.map +1 -0
  298. package/lib/typescript/src/CameraPreview.d.ts +13 -0
  299. package/lib/typescript/src/CameraPreview.d.ts.map +1 -0
  300. package/lib/typescript/src/NativeAnimatedImageView.d.ts +33 -0
  301. package/lib/typescript/src/NativeAnimatedImageView.d.ts.map +1 -0
  302. package/lib/typescript/src/NativeAudioPlayerView.d.ts +12 -0
  303. package/lib/typescript/src/NativeAudioPlayerView.d.ts.map +1 -0
  304. package/lib/typescript/src/NativeBlurView.d.ts +12 -0
  305. package/lib/typescript/src/NativeBlurView.d.ts.map +1 -0
  306. package/lib/typescript/src/NativeCustomCamera.d.ts +167 -0
  307. package/lib/typescript/src/NativeCustomCamera.d.ts.map +1 -0
  308. package/lib/typescript/src/NativeCustomCamera.web.d.ts +47 -0
  309. package/lib/typescript/src/NativeCustomCamera.web.d.ts.map +1 -0
  310. package/lib/typescript/src/NativeCustomVideoView.d.ts +30 -0
  311. package/lib/typescript/src/NativeCustomVideoView.d.ts.map +1 -0
  312. package/lib/typescript/src/NativeDocumentPreviewView.d.ts +48 -0
  313. package/lib/typescript/src/NativeDocumentPreviewView.d.ts.map +1 -0
  314. package/lib/typescript/src/NativeMediaThumbnailView.d.ts +54 -0
  315. package/lib/typescript/src/NativeMediaThumbnailView.d.ts.map +1 -0
  316. package/lib/typescript/src/NativeSvgIcon.d.ts +25 -0
  317. package/lib/typescript/src/NativeSvgIcon.d.ts.map +1 -0
  318. package/lib/typescript/src/ResponsivePreviewContainer.d.ts +92 -0
  319. package/lib/typescript/src/ResponsivePreviewContainer.d.ts.map +1 -0
  320. package/lib/typescript/src/accessibility.d.ts +20 -0
  321. package/lib/typescript/src/accessibility.d.ts.map +1 -0
  322. package/lib/typescript/src/config.d.ts +145 -0
  323. package/lib/typescript/src/config.d.ts.map +1 -0
  324. package/lib/typescript/src/cpuOptimization.d.ts +63 -0
  325. package/lib/typescript/src/cpuOptimization.d.ts.map +1 -0
  326. package/lib/typescript/src/db/DatabaseManager.d.ts +48 -0
  327. package/lib/typescript/src/db/DatabaseManager.d.ts.map +1 -0
  328. package/lib/typescript/src/db/MediaMapper.d.ts +77 -0
  329. package/lib/typescript/src/db/MediaMapper.d.ts.map +1 -0
  330. package/lib/typescript/src/db/MediaRepository.d.ts +66 -0
  331. package/lib/typescript/src/db/MediaRepository.d.ts.map +1 -0
  332. package/lib/typescript/src/db/index.d.ts +23 -0
  333. package/lib/typescript/src/db/index.d.ts.map +1 -0
  334. package/lib/typescript/src/db/migrations.d.ts +31 -0
  335. package/lib/typescript/src/db/migrations.d.ts.map +1 -0
  336. package/lib/typescript/src/db/secureKey.d.ts +10 -0
  337. package/lib/typescript/src/db/secureKey.d.ts.map +1 -0
  338. package/lib/typescript/src/db/types.d.ts +115 -0
  339. package/lib/typescript/src/db/types.d.ts.map +1 -0
  340. package/lib/typescript/src/db/useMediaDatabase.d.ts +41 -0
  341. package/lib/typescript/src/db/useMediaDatabase.d.ts.map +1 -0
  342. package/lib/typescript/src/defaultIcons.d.ts +139 -0
  343. package/lib/typescript/src/defaultIcons.d.ts.map +1 -0
  344. package/lib/typescript/src/deviceCapabilities.d.ts +92 -0
  345. package/lib/typescript/src/deviceCapabilities.d.ts.map +1 -0
  346. package/lib/typescript/src/errors.d.ts +95 -0
  347. package/lib/typescript/src/errors.d.ts.map +1 -0
  348. package/lib/typescript/src/iconConfig.d.ts +176 -0
  349. package/lib/typescript/src/iconConfig.d.ts.map +1 -0
  350. package/lib/typescript/src/icons.d.ts +217 -0
  351. package/lib/typescript/src/icons.d.ts.map +1 -0
  352. package/lib/typescript/src/index.d.ts +104 -0
  353. package/lib/typescript/src/index.d.ts.map +1 -0
  354. package/lib/typescript/src/library/MediaLibrary.d.ts +57 -0
  355. package/lib/typescript/src/library/MediaLibrary.d.ts.map +1 -0
  356. package/lib/typescript/src/library/MediaLibraryScreen.d.ts +51 -0
  357. package/lib/typescript/src/library/MediaLibraryScreen.d.ts.map +1 -0
  358. package/lib/typescript/src/library/components/AudioPreview.d.ts +6 -0
  359. package/lib/typescript/src/library/components/AudioPreview.d.ts.map +1 -0
  360. package/lib/typescript/src/library/components/DocumentPreview.d.ts +10 -0
  361. package/lib/typescript/src/library/components/DocumentPreview.d.ts.map +1 -0
  362. package/lib/typescript/src/library/components/ExpandableFab.d.ts +39 -0
  363. package/lib/typescript/src/library/components/ExpandableFab.d.ts.map +1 -0
  364. package/lib/typescript/src/library/components/FileTiles.d.ts +8 -0
  365. package/lib/typescript/src/library/components/FileTiles.d.ts.map +1 -0
  366. package/lib/typescript/src/library/components/ImagePreview.d.ts +5 -0
  367. package/lib/typescript/src/library/components/ImagePreview.d.ts.map +1 -0
  368. package/lib/typescript/src/library/components/MediaMetadataHeader.d.ts +16 -0
  369. package/lib/typescript/src/library/components/MediaMetadataHeader.d.ts.map +1 -0
  370. package/lib/typescript/src/library/components/RecordAudioScreen.d.ts +8 -0
  371. package/lib/typescript/src/library/components/RecordAudioScreen.d.ts.map +1 -0
  372. package/lib/typescript/src/library/components/VideoPreview.d.ts +20 -0
  373. package/lib/typescript/src/library/components/VideoPreview.d.ts.map +1 -0
  374. package/lib/typescript/src/library/components/ViewerCounter.d.ts +13 -0
  375. package/lib/typescript/src/library/components/ViewerCounter.d.ts.map +1 -0
  376. package/lib/typescript/src/library/compressionEstimate.d.ts +313 -0
  377. package/lib/typescript/src/library/compressionEstimate.d.ts.map +1 -0
  378. package/lib/typescript/src/library/fileMetadata.d.ts +33 -0
  379. package/lib/typescript/src/library/fileMetadata.d.ts.map +1 -0
  380. package/lib/typescript/src/library/index.d.ts +40 -0
  381. package/lib/typescript/src/library/index.d.ts.map +1 -0
  382. package/lib/typescript/src/library/libraryIconRegistry.d.ts +35 -0
  383. package/lib/typescript/src/library/libraryIconRegistry.d.ts.map +1 -0
  384. package/lib/typescript/src/library/libraryIcons.d.ts +85 -0
  385. package/lib/typescript/src/library/libraryIcons.d.ts.map +1 -0
  386. package/lib/typescript/src/library/localizedFormat.d.ts +49 -0
  387. package/lib/typescript/src/library/localizedFormat.d.ts.map +1 -0
  388. package/lib/typescript/src/library/palette.d.ts +104 -0
  389. package/lib/typescript/src/library/palette.d.ts.map +1 -0
  390. package/lib/typescript/src/library/responsive.d.ts +15 -0
  391. package/lib/typescript/src/library/responsive.d.ts.map +1 -0
  392. package/lib/typescript/src/locales.d.ts +103 -0
  393. package/lib/typescript/src/locales.d.ts.map +1 -0
  394. package/lib/typescript/src/localization.d.ts +1051 -0
  395. package/lib/typescript/src/localization.d.ts.map +1 -0
  396. package/lib/typescript/src/logger.d.ts +4 -0
  397. package/lib/typescript/src/logger.d.ts.map +1 -0
  398. package/lib/typescript/src/lowMemoryCache.d.ts +64 -0
  399. package/lib/typescript/src/lowMemoryCache.d.ts.map +1 -0
  400. package/lib/typescript/src/mediaHistory.d.ts +3 -0
  401. package/lib/typescript/src/mediaHistory.d.ts.map +1 -0
  402. package/lib/typescript/src/mediaStorageService.d.ts +155 -0
  403. package/lib/typescript/src/mediaStorageService.d.ts.map +1 -0
  404. package/lib/typescript/src/memory.d.ts +21 -0
  405. package/lib/typescript/src/memory.d.ts.map +1 -0
  406. package/lib/typescript/src/nativeActions.d.ts +174 -0
  407. package/lib/typescript/src/nativeActions.d.ts.map +1 -0
  408. package/lib/typescript/src/nativeComponentSupport.d.ts +30 -0
  409. package/lib/typescript/src/nativeComponentSupport.d.ts.map +1 -0
  410. package/lib/typescript/src/networkPerformance.d.ts +12 -0
  411. package/lib/typescript/src/networkPerformance.d.ts.map +1 -0
  412. package/lib/typescript/src/performance.d.ts +32 -0
  413. package/lib/typescript/src/performance.d.ts.map +1 -0
  414. package/lib/typescript/src/performanceMonitor.d.ts +45 -0
  415. package/lib/typescript/src/performanceMonitor.d.ts.map +1 -0
  416. package/lib/typescript/src/permissions.d.ts +132 -0
  417. package/lib/typescript/src/permissions.d.ts.map +1 -0
  418. package/lib/typescript/src/rendering.d.ts +9 -0
  419. package/lib/typescript/src/rendering.d.ts.map +1 -0
  420. package/lib/typescript/src/security.d.ts +6 -0
  421. package/lib/typescript/src/security.d.ts.map +1 -0
  422. package/lib/typescript/src/state.d.ts +102 -0
  423. package/lib/typescript/src/state.d.ts.map +1 -0
  424. package/lib/typescript/src/storage.d.ts +117 -0
  425. package/lib/typescript/src/storage.d.ts.map +1 -0
  426. package/lib/typescript/src/theme.d.ts +15 -0
  427. package/lib/typescript/src/theme.d.ts.map +1 -0
  428. package/lib/typescript/src/types.d.ts +802 -0
  429. package/lib/typescript/src/types.d.ts.map +1 -0
  430. package/lib/typescript/src/utils.d.ts +50 -0
  431. package/lib/typescript/src/utils.d.ts.map +1 -0
  432. package/lib/typescript/src/validation.d.ts +52 -0
  433. package/lib/typescript/src/validation.d.ts.map +1 -0
  434. package/package.json +204 -0
  435. package/react-native.config.js +17 -0
  436. package/src/AudioPlayerContext.tsx +220 -0
  437. package/src/CameraPreview.tsx +352 -0
  438. package/src/NativeAnimatedImageView.tsx +71 -0
  439. package/src/NativeAudioPlayerView.tsx +39 -0
  440. package/src/NativeBlurView.tsx +40 -0
  441. package/src/NativeCustomCamera.ts +305 -0
  442. package/src/NativeCustomCamera.web.ts +192 -0
  443. package/src/NativeCustomVideoView.ts +30 -0
  444. package/src/NativeDocumentPreviewView.tsx +196 -0
  445. package/src/NativeMediaThumbnailView.tsx +167 -0
  446. package/src/NativeSvgIcon.tsx +46 -0
  447. package/src/ResponsivePreviewContainer.tsx +227 -0
  448. package/src/accessibility.ts +80 -0
  449. package/src/config.ts +406 -0
  450. package/src/cpuOptimization.ts +365 -0
  451. package/src/db/DatabaseManager.ts +212 -0
  452. package/src/db/MediaMapper.ts +377 -0
  453. package/src/db/MediaRepository.ts +313 -0
  454. package/src/db/index.ts +73 -0
  455. package/src/db/migrations.ts +222 -0
  456. package/src/db/react-native-sqlcipher.d.ts +76 -0
  457. package/src/db/secureKey.ts +100 -0
  458. package/src/db/types.ts +123 -0
  459. package/src/db/useMediaDatabase.ts +191 -0
  460. package/src/defaultIcons.tsx +2390 -0
  461. package/src/deviceCapabilities.ts +438 -0
  462. package/src/errors.ts +350 -0
  463. package/src/iconConfig.tsx +962 -0
  464. package/src/icons.tsx +788 -0
  465. package/src/index.tsx +763 -0
  466. package/src/jest-globals.d.ts +9 -0
  467. package/src/library/MediaLibrary.tsx +141 -0
  468. package/src/library/MediaLibraryScreen.tsx +3003 -0
  469. package/src/library/components/AudioPreview.tsx +73 -0
  470. package/src/library/components/DocumentPreview.tsx +361 -0
  471. package/src/library/components/ExpandableFab.tsx +342 -0
  472. package/src/library/components/FileTiles.tsx +252 -0
  473. package/src/library/components/ImagePreview.tsx +72 -0
  474. package/src/library/components/MediaMetadataHeader.tsx +208 -0
  475. package/src/library/components/RecordAudioScreen.tsx +845 -0
  476. package/src/library/components/VideoPreview.tsx +478 -0
  477. package/src/library/components/ViewerCounter.tsx +71 -0
  478. package/src/library/compressionEstimate.ts +662 -0
  479. package/src/library/fileMetadata.ts +212 -0
  480. package/src/library/index.ts +66 -0
  481. package/src/library/libraryIconRegistry.tsx +86 -0
  482. package/src/library/libraryIcons.tsx +928 -0
  483. package/src/library/localizedFormat.ts +207 -0
  484. package/src/library/palette.ts +281 -0
  485. package/src/library/responsive.ts +49 -0
  486. package/src/locales.ts +381 -0
  487. package/src/localization.ts +1947 -0
  488. package/src/logger.ts +20 -0
  489. package/src/lowMemoryCache.ts +218 -0
  490. package/src/mediaHistory.ts +42 -0
  491. package/src/mediaStorageService.ts +546 -0
  492. package/src/memory.ts +102 -0
  493. package/src/nativeActions.ts +422 -0
  494. package/src/nativeComponentSupport.ts +60 -0
  495. package/src/networkPerformance.ts +30 -0
  496. package/src/performance.ts +137 -0
  497. package/src/performanceMonitor.ts +219 -0
  498. package/src/permissions.ts +403 -0
  499. package/src/rendering.ts +47 -0
  500. package/src/security.ts +19 -0
  501. package/src/state.ts +333 -0
  502. package/src/storage.ts +453 -0
  503. package/src/theme.tsx +467 -0
  504. package/src/types.ts +946 -0
  505. package/src/utils.ts +177 -0
  506. 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
+ }