motorinc-gallery-picker-pro 1.0.0

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.
@@ -0,0 +1,1968 @@
1
+ package com.gallerypicker.imagepicker;
2
+
3
+ import android.Manifest;
4
+ import android.app.Activity;
5
+ import android.content.ContentUris;
6
+ import android.content.Intent;
7
+ import android.content.pm.PackageManager;
8
+ import android.database.Cursor;
9
+ import android.graphics.Bitmap;
10
+ import android.graphics.BitmapFactory;
11
+ import android.net.Uri;
12
+ import android.os.Build;
13
+ import android.os.Environment;
14
+ import android.provider.MediaStore;
15
+ import android.util.Base64;
16
+ import android.util.Log;
17
+ import android.content.ContentResolver;
18
+ import java.io.InputStream;
19
+ import java.io.ByteArrayOutputStream;
20
+ import java.io.IOException;
21
+ import java.util.UUID;
22
+
23
+ import androidx.core.app.ActivityCompat;
24
+ import androidx.core.content.ContextCompat;
25
+ import androidx.core.content.FileProvider;
26
+
27
+ import com.facebook.react.bridge.ActivityEventListener;
28
+ import com.facebook.react.bridge.BaseActivityEventListener;
29
+ import com.facebook.react.bridge.Promise;
30
+ import com.facebook.react.bridge.ReactApplicationContext;
31
+ import com.facebook.react.bridge.ReactContextBaseJavaModule;
32
+ import com.facebook.react.bridge.ReactMethod;
33
+ import com.facebook.react.modules.core.RCTNativeAppEventEmitter;
34
+ import com.facebook.react.bridge.ReadableMap;
35
+ import com.facebook.react.bridge.WritableArray;
36
+ import com.facebook.react.bridge.WritableMap;
37
+ import com.facebook.react.bridge.WritableNativeArray;
38
+ import com.facebook.react.bridge.WritableNativeMap;
39
+ import com.facebook.react.bridge.LifecycleEventListener;
40
+ import com.facebook.react.modules.core.DeviceEventManagerModule;
41
+
42
+ import java.io.File;
43
+ import java.io.FileOutputStream;
44
+ import java.io.IOException;
45
+ import java.io.InputStream;
46
+ import java.text.SimpleDateFormat;
47
+ import java.util.Arrays;
48
+ import java.util.Date;
49
+ import java.util.Locale;
50
+ import java.util.UUID;
51
+
52
+ public class ImagePickerModule extends ReactContextBaseJavaModule {
53
+
54
+ private static final String[] SUPPORTED_EVENTS = {
55
+ "PhotoLibraryChanged",
56
+ "onPermissionRationale"
57
+ };
58
+ private static final String TAG = "ImagePickerModule";
59
+ private static final int CAMERA_REQUEST_CODE = 1001;
60
+ private static final int GALLERY_REQUEST_CODE = 1002;
61
+ private static final int CAMERA_PERMISSION_REQUEST_CODE = 2001;
62
+ private static final int STORAGE_PERMISSION_REQUEST_CODE = 2002;
63
+
64
+ // Android 13+ permission constants
65
+ private static final String READ_MEDIA_VIDEO = "android.permission.READ_MEDIA_VIDEO";
66
+ private static final String READ_MEDIA_VISUAL_USER_SELECTED = "android.permission.READ_MEDIA_VISUAL_USER_SELECTED";
67
+
68
+ private Promise currentPromise;
69
+ private ReadableMap currentOptions;
70
+ private File currentPhotoFile;
71
+ private boolean waitingForCameraPermission = false;
72
+ private boolean waitingForStoragePermission = false;
73
+
74
+ private final ActivityEventListener activityEventListener = new BaseActivityEventListener() {
75
+ @Override
76
+ public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) {
77
+ handleActivityResult(requestCode, resultCode, data);
78
+ }
79
+ };
80
+
81
+ public ImagePickerModule(ReactApplicationContext reactContext) {
82
+ super(reactContext);
83
+ reactContext.addActivityEventListener(activityEventListener);
84
+ setupPermissionListeners();
85
+ }
86
+
87
+ @Override
88
+ public String getName() {
89
+ return "ImagePickerModule";
90
+ }
91
+
92
+ @ReactMethod
93
+ public void openCamera(ReadableMap options, Promise promise) {
94
+ currentPromise = promise;
95
+ currentOptions = options;
96
+
97
+ if (!hasCameraPermission()) {
98
+ waitingForCameraPermission = true;
99
+ requestCameraPermissionInternal();
100
+ return;
101
+ }
102
+
103
+ if (!hasStoragePermission()) {
104
+ waitingForStoragePermission = true;
105
+ requestStoragePermissionInternal();
106
+ return;
107
+ }
108
+
109
+ launchCamera();
110
+ }
111
+
112
+ @ReactMethod
113
+ public void openGallery(ReadableMap options, Promise promise) {
114
+ currentPromise = promise;
115
+ currentOptions = options;
116
+
117
+ if (!hasStoragePermission()) {
118
+ waitingForStoragePermission = true;
119
+ requestStoragePermissionInternal();
120
+ return;
121
+ }
122
+
123
+ launchGallery();
124
+ }
125
+
126
+ @ReactMethod
127
+ public void requestCameraPermission(Promise promise) {
128
+ if (hasCameraPermission()) {
129
+ promise.resolve(true);
130
+ } else {
131
+ // Actually request the permission - use a separate promise to avoid conflicts
132
+ requestCameraPermissionInternal();
133
+ // For this method, resolve immediately after requesting, not waiting for result
134
+ promise.resolve(false); // Will be true once permission is granted
135
+ }
136
+ }
137
+
138
+ @ReactMethod
139
+ public void requestGalleryPermission(Promise promise) {
140
+ if (hasStoragePermission()) {
141
+ promise.resolve(true);
142
+ } else {
143
+ // Actually request the permission - use a separate promise to avoid conflicts
144
+ requestStoragePermissionInternal();
145
+ // For this method, resolve immediately after requesting, not waiting for result
146
+ promise.resolve(false); // Will be true once permission is granted
147
+ }
148
+ }
149
+
150
+ @ReactMethod
151
+ public void openMultiSelectGallery(ReadableMap options, Promise promise) {
152
+ currentPromise = promise;
153
+ currentOptions = options;
154
+
155
+ if (!hasStoragePermission()) {
156
+ promise.reject("PERMISSION_DENIED", "Storage permission not granted");
157
+ return;
158
+ }
159
+
160
+ // Launch gallery with multi-select capability
161
+ launchMultiSelectGallery(options);
162
+ }
163
+
164
+ @ReactMethod
165
+ public void managePhotoSelection(Promise promise) {
166
+ // This method allows users to update their selected photos in limited access mode
167
+ if (Build.VERSION.SDK_INT >= 34 && isLimitedAccessMode()) {
168
+ Log.d(TAG, "Opening photo selection management for limited access mode");
169
+ // Re-request permissions to trigger the photo selection picker
170
+ requestStoragePermissionInternal();
171
+ promise.resolve(true);
172
+ } else {
173
+ Log.d(TAG, "Not in limited access mode or not Android 14+, opening app settings");
174
+ // For non-limited mode or older Android, open app permission settings
175
+ try {
176
+ Intent intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
177
+ intent.setData(android.net.Uri.parse("package:" + getReactApplicationContext().getPackageName()));
178
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
179
+ getReactApplicationContext().startActivity(intent);
180
+ promise.resolve(true);
181
+ } catch (Exception e) {
182
+ Log.e(TAG, "Error opening app settings: " + e.getMessage());
183
+ promise.reject("SETTINGS_ERROR", "Failed to open app settings");
184
+ }
185
+ }
186
+ }
187
+
188
+ @ReactMethod
189
+ public void getPhotoLibraryPermissionStatus(Promise promise) {
190
+ if (hasStoragePermission()) {
191
+ // Check if we're in limited access mode (Android 14+ partial permissions)
192
+ if (isLimitedAccessMode()) {
193
+ promise.resolve("limited");
194
+ } else {
195
+ promise.resolve("authorized");
196
+ }
197
+ } else {
198
+ // Check if permission was permanently denied
199
+ Activity activity = getCurrentActivity();
200
+ if (activity != null) {
201
+ String permission;
202
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
203
+ permission = Manifest.permission.READ_MEDIA_IMAGES;
204
+ } else {
205
+ permission = Manifest.permission.READ_EXTERNAL_STORAGE;
206
+ }
207
+
208
+ boolean shouldShow = ActivityCompat.shouldShowRequestPermissionRationale(activity, permission);
209
+ if (!shouldShow) {
210
+ // Permission was permanently denied
211
+ promise.resolve("denied");
212
+ } else {
213
+ // Permission not requested yet
214
+ promise.resolve("not_determined");
215
+ }
216
+ } else {
217
+ promise.resolve("not_determined");
218
+ }
219
+ }
220
+ }
221
+
222
+ @ReactMethod
223
+ public void fetchPhotoLibraryAssets(ReadableMap options, Promise promise) {
224
+ Log.d(TAG, "fetchPhotoLibraryAssets called, checking permission...");
225
+
226
+ int limit = options.hasKey("limit") ? options.getInt("limit") : 20;
227
+ int offset = options.hasKey("offset") ? options.getInt("offset") : 0;
228
+ String mediaType = options.hasKey("mediaType") ? options.getString("mediaType") : "photo";
229
+
230
+ // Check specific permissions based on media type
231
+ if ("video".equals(mediaType)) {
232
+ if (!hasVideoPermission()) {
233
+ Log.e(TAG, "Video permission not granted");
234
+ promise.reject("PERMISSION_DENIED", "Video permission not granted");
235
+ return;
236
+ }
237
+ } else if ("mixed".equals(mediaType)) {
238
+ if (!hasImagePermission() && !hasVideoPermission()) {
239
+ Log.e(TAG, "Neither image nor video permission granted");
240
+ promise.reject("PERMISSION_DENIED", "Media permissions not granted");
241
+ return;
242
+ }
243
+ } else {
244
+ // Default to images
245
+ if (!hasImagePermission()) {
246
+ Log.e(TAG, "Image permission not granted");
247
+ promise.reject("PERMISSION_DENIED", "Image permission not granted");
248
+ return;
249
+ }
250
+ }
251
+
252
+ Log.d(TAG, "Permission granted for " + mediaType + ", proceeding with fetch");
253
+
254
+ // Check if we're in limited access mode (Android 14+ partial permissions)
255
+ boolean isLimitedMode = isLimitedAccessMode();
256
+
257
+ // In limited mode, the system already restricts access to user-selected images only
258
+ // We don't need to artificially limit the count as the MediaStore query will only return
259
+ // the images the user has granted access to via READ_MEDIA_VISUAL_USER_SELECTED
260
+ final int finalLimit = limit;
261
+
262
+ Log.d(TAG, "Fetching assets with limit: " + finalLimit + ", offset: " + offset + ", mediaType: " + mediaType + ", limited mode: " + isLimitedMode);
263
+
264
+ new Thread(() -> {
265
+ try {
266
+ WritableArray assets = new WritableNativeArray();
267
+
268
+ if ("mixed".equals(mediaType)) {
269
+ // For mixed media, query both images and videos separately and combine
270
+ assets = fetchMixedMediaAssets(offset, finalLimit);
271
+ } else if ("video".equals(mediaType)) {
272
+ // Query videos only
273
+ assets = fetchVideoAssets(offset, finalLimit);
274
+ } else {
275
+ // Query images only (default)
276
+ assets = fetchImageAssets(offset, finalLimit);
277
+ }
278
+
279
+ Log.d(TAG, "Fetched " + assets.size() + " assets");
280
+
281
+ // Get total count with separate query
282
+ // In limited mode, getTotalMediaCount() will already return the correct count
283
+ // of user-selected media due to the permission restriction
284
+ int totalCount = getTotalMediaCount(mediaType);
285
+ boolean hasMore = (offset + assets.size()) < totalCount;
286
+
287
+ Log.d(TAG, "Processed " + assets.size() + " assets, total count: " + totalCount + ", has more: " + hasMore);
288
+
289
+ WritableMap result = new WritableNativeMap();
290
+ result.putArray("assets", assets);
291
+ result.putBoolean("hasMore", hasMore);
292
+ result.putInt("totalCount", totalCount);
293
+
294
+ promise.resolve(result);
295
+
296
+ } catch (Exception e) {
297
+ Log.e(TAG, "Error fetching gallery assets: " + e.getMessage(), e);
298
+ promise.reject("FETCH_ERROR", "Failed to fetch gallery assets: " + e.getMessage());
299
+ }
300
+ }).start();
301
+ }
302
+
303
+ @ReactMethod
304
+ public void getImageForAsset(String assetId, Integer targetWidth, Integer targetHeight, Promise promise) {
305
+ new Thread(() -> {
306
+ try {
307
+ Log.d(TAG, "getImageForAsset called with assetId: " + assetId);
308
+
309
+ // Check storage permission first
310
+ if (!hasStoragePermission()) {
311
+ promise.reject("PERMISSION_DENIED", "Storage permission not granted");
312
+ return;
313
+ }
314
+
315
+ Uri contentUri;
316
+
317
+ if (assetId.startsWith("content://")) {
318
+ // Already a content URI, use it directly
319
+ contentUri = Uri.parse(assetId);
320
+ Log.d(TAG, "Using provided content URI: " + contentUri);
321
+ } else {
322
+ // Convert numeric ID to content URI
323
+ try {
324
+ long id = Long.parseLong(assetId);
325
+
326
+ // Try to find the asset in both Images and Videos
327
+ contentUri = null;
328
+
329
+ // First try images
330
+ Uri imageUri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id);
331
+ if (assetExists(imageUri)) {
332
+ contentUri = imageUri;
333
+ Log.d(TAG, "Found as image: " + contentUri);
334
+ } else {
335
+ // Try videos
336
+ Uri videoUri = ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, id);
337
+ if (assetExists(videoUri)) {
338
+ contentUri = videoUri;
339
+ Log.d(TAG, "Found as video: " + contentUri);
340
+ }
341
+ }
342
+
343
+ if (contentUri == null) {
344
+ // If not found by ID, maybe the assetId is actually a content URI in disguise
345
+ if (assetId.contains("content") || assetId.contains("media")) {
346
+ Log.w(TAG, "Trying to parse assetId as potential content URI: " + assetId);
347
+ try {
348
+ contentUri = Uri.parse("content://media/external/images/media/" + id);
349
+ if (!assetExists(contentUri)) {
350
+ contentUri = Uri.parse("content://media/external/video/media/" + id);
351
+ }
352
+ } catch (Exception ex) {
353
+ Log.e(TAG, "Failed to create fallback content URI: " + ex.getMessage());
354
+ }
355
+ }
356
+
357
+ if (contentUri == null) {
358
+ promise.reject("ASSET_NOT_FOUND", "Asset not found with ID: " + assetId);
359
+ return;
360
+ }
361
+ }
362
+
363
+ } catch (NumberFormatException e) {
364
+ promise.reject("INVALID_ASSET_ID", "Invalid asset ID format: " + assetId);
365
+ return;
366
+ }
367
+ }
368
+
369
+ // Default dimensions if not specified
370
+ int width = targetWidth != null ? targetWidth : 300;
371
+ int height = targetHeight != null ? targetHeight : 300;
372
+
373
+ Log.d(TAG, "Loading image with dimensions: " + width + "x" + height);
374
+
375
+ // Try to load the image
376
+ Bitmap bitmap = loadAndResizeBitmapSafely(contentUri, width, height);
377
+
378
+ if (bitmap != null) {
379
+ // Save to temporary file
380
+ File tempFile = createImageFile();
381
+ saveBitmapToFile(bitmap, tempFile);
382
+
383
+ String fileUri = "file://" + tempFile.getAbsolutePath();
384
+ Log.d(TAG, "Image saved to: " + fileUri);
385
+ promise.resolve(fileUri);
386
+ } else {
387
+ Log.e(TAG, "Failed to load bitmap for URI: " + contentUri);
388
+ promise.reject("IMAGE_LOAD_ERROR", "Failed to load image from content URI: " + contentUri);
389
+ }
390
+
391
+ } catch (Exception e) {
392
+ Log.e(TAG, "Error processing asset: " + e.getMessage(), e);
393
+ promise.reject("ASSET_PROCESSING_ERROR", "Failed to process asset: " + e.getMessage());
394
+ }
395
+ }).start();
396
+ }
397
+
398
+ @ReactMethod
399
+ public void getVideoBase64(String videoUri, Promise promise) {
400
+ new Thread(() -> {
401
+ try {
402
+ Log.d(TAG, "Getting base64 for video URI: " + videoUri);
403
+
404
+ Uri contentUri = Uri.parse(videoUri);
405
+ ContentResolver resolver = getReactApplicationContext().getContentResolver();
406
+
407
+ // Check file size first to avoid memory issues
408
+ long fileSize = 0;
409
+ String[] sizeProjection = {MediaStore.Video.Media.SIZE};
410
+ try (Cursor cursor = resolver.query(contentUri, sizeProjection, null, null, null)) {
411
+ if (cursor != null && cursor.moveToFirst()) {
412
+ int sizeColumn = cursor.getColumnIndex(MediaStore.Video.Media.SIZE);
413
+ if (sizeColumn >= 0) {
414
+ fileSize = cursor.getLong(sizeColumn);
415
+ }
416
+ }
417
+ }
418
+
419
+ // Skip base64 for large files to avoid OutOfMemoryError (limit to 10MB)
420
+ if (fileSize > 10 * 1024 * 1024) { // 10MB
421
+ Log.w(TAG, "Video file too large for base64 conversion: " + fileSize + " bytes. Returning empty base64.");
422
+ promise.resolve(""); // Return empty string for large files
423
+ return;
424
+ }
425
+
426
+ // Read the video file into memory (only for smaller files)
427
+ try (InputStream inputStream = resolver.openInputStream(contentUri);
428
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
429
+
430
+ if (inputStream == null) {
431
+ promise.reject("VIDEO_NOT_FOUND", "Could not open video stream for URI: " + videoUri);
432
+ return;
433
+ }
434
+
435
+ byte[] buffer = new byte[8192];
436
+ int bytesRead;
437
+ long totalBytesRead = 0;
438
+
439
+ // Add safety check during reading
440
+ while ((bytesRead = inputStream.read(buffer)) != -1) {
441
+ totalBytesRead += bytesRead;
442
+
443
+ // Additional safety check during read
444
+ if (totalBytesRead > 10 * 1024 * 1024) {
445
+ Log.w(TAG, "Video file exceeded size limit during reading. Returning empty base64.");
446
+ promise.resolve("");
447
+ return;
448
+ }
449
+
450
+ outputStream.write(buffer, 0, bytesRead);
451
+ }
452
+
453
+ byte[] videoBytes = outputStream.toByteArray();
454
+ String base64String = "data:video/mp4;base64," + Base64.encodeToString(videoBytes, Base64.NO_WRAP);
455
+
456
+ Log.d(TAG, "Video base64 generated successfully. Length: " + base64String.length());
457
+ promise.resolve(base64String);
458
+
459
+ } catch (IOException e) {
460
+ Log.e(TAG, "Error reading video file: " + e.getMessage(), e);
461
+ promise.reject("VIDEO_READ_ERROR", "Failed to read video file: " + e.getMessage());
462
+ }
463
+
464
+ } catch (Exception e) {
465
+ Log.e(TAG, "Error getting video base64: " + e.getMessage(), e);
466
+ promise.reject("VIDEO_BASE64_ERROR", "Failed to get video base64: " + e.getMessage());
467
+ }
468
+ }).start();
469
+ }
470
+
471
+ @ReactMethod
472
+ public void openPhotoLibraryLimitedPicker(Promise promise) {
473
+ // Android doesn't have limited photo access like iOS, but we can simulate it
474
+ try {
475
+ Activity activity = getCurrentActivity();
476
+ if (activity != null) {
477
+ // Set limited access mode
478
+ android.content.SharedPreferences prefs = getReactApplicationContext()
479
+ .getSharedPreferences("ImagePickerPrefs", android.content.Context.MODE_PRIVATE);
480
+ prefs.edit().putBoolean("limited_access_mode", true).apply();
481
+
482
+ Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
483
+ intent.setType("image/*");
484
+ intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
485
+ activity.startActivityForResult(intent, GALLERY_REQUEST_CODE);
486
+
487
+ // Simulate iOS behavior by sending gallery change event after a delay
488
+ new Thread(() -> {
489
+ try {
490
+ Thread.sleep(1000); // Wait for potential changes
491
+ sendEvent("PhotoLibraryChanged");
492
+ } catch (InterruptedException e) {
493
+ Thread.currentThread().interrupt();
494
+ }
495
+ }).start();
496
+
497
+ promise.resolve(true);
498
+ } else {
499
+ promise.reject("NO_ACTIVITY", "No current activity available");
500
+ }
501
+ } catch (Exception e) {
502
+ promise.reject("PICKER_ERROR", "Failed to open photo picker: " + e.getMessage());
503
+ }
504
+ }
505
+
506
+ @ReactMethod
507
+ public void openPhotoLibraryLimitedSettings(Promise promise) {
508
+ try {
509
+ Intent intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
510
+ intent.setData(Uri.parse("package:" + getReactApplicationContext().getPackageName()));
511
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
512
+
513
+ Activity activity = getCurrentActivity();
514
+ if (activity != null) {
515
+ activity.startActivity(intent);
516
+ promise.resolve(true);
517
+ } else {
518
+ promise.resolve(false);
519
+ }
520
+ } catch (Exception e) {
521
+ promise.resolve(false);
522
+ }
523
+ }
524
+
525
+ @ReactMethod
526
+ public void setLimitedAccessMode(boolean limited, Promise promise) {
527
+ try {
528
+ android.content.SharedPreferences prefs = getReactApplicationContext()
529
+ .getSharedPreferences("ImagePickerPrefs", android.content.Context.MODE_PRIVATE);
530
+ prefs.edit().putBoolean("limited_access_mode", limited).apply();
531
+
532
+ Log.d(TAG, "Limited access mode set to: " + limited);
533
+ promise.resolve(true);
534
+ } catch (Exception e) {
535
+ Log.e(TAG, "Error setting limited access mode: " + e.getMessage());
536
+ promise.reject("PREFERENCE_ERROR", "Failed to set limited access mode");
537
+ }
538
+ }
539
+
540
+ @ReactMethod
541
+ public void testSystemVideoPicker(Promise promise) {
542
+ try {
543
+ Log.d(TAG, "🎯 Testing system video picker...");
544
+
545
+ Intent intent = new Intent(Intent.ACTION_PICK);
546
+ intent.setType("video/*");
547
+
548
+ Activity activity = getCurrentActivity();
549
+ if (activity != null) {
550
+ activity.startActivityForResult(intent, 9999); // Use unique code
551
+ promise.resolve("System picker launched - check if videos appear");
552
+ } else {
553
+ promise.reject("NO_ACTIVITY", "No activity available");
554
+ }
555
+ } catch (Exception e) {
556
+ promise.reject("PICKER_ERROR", e.getMessage());
557
+ }
558
+ }
559
+
560
+ @ReactMethod
561
+ public void simpleVideoTest(Promise promise) {
562
+ Log.d(TAG, "🎬 SIMPLE VIDEO TEST START");
563
+
564
+ try {
565
+ WritableMap result = new WritableNativeMap();
566
+
567
+ // Test 1: Check permissions
568
+ boolean hasVideo = hasVideoPermission();
569
+ boolean hasStorage = hasStoragePermission();
570
+ Log.d(TAG, "🎬 hasVideoPermission: " + hasVideo);
571
+ Log.d(TAG, "🎬 hasStoragePermission: " + hasStorage);
572
+ result.putBoolean("hasVideoPermission", hasVideo);
573
+ result.putBoolean("hasStoragePermission", hasStorage);
574
+
575
+ // Test ALL possible video URIs
576
+ Uri[] testUris = {
577
+ MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
578
+ MediaStore.Video.Media.INTERNAL_CONTENT_URI
579
+ };
580
+
581
+ // Add volume URIs for Android 10+
582
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
583
+ try {
584
+ Uri volumeExternal = MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL);
585
+ Uri volumeInternal = MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_INTERNAL);
586
+ testUris = new Uri[]{
587
+ MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
588
+ MediaStore.Video.Media.INTERNAL_CONTENT_URI,
589
+ volumeExternal,
590
+ volumeInternal
591
+ };
592
+ } catch (Exception e) {
593
+ Log.w(TAG, "Volume URIs failed: " + e.getMessage());
594
+ }
595
+ }
596
+
597
+ WritableArray uriResults = new WritableNativeArray();
598
+ int totalVideos = 0;
599
+
600
+ for (Uri videoUri : testUris) {
601
+ Log.d(TAG, "🎬 Testing URI: " + videoUri);
602
+
603
+ Cursor cursor = null;
604
+ try {
605
+ cursor = getReactApplicationContext().getContentResolver().query(
606
+ videoUri,
607
+ new String[]{MediaStore.Video.Media._ID, MediaStore.Video.Media.DISPLAY_NAME},
608
+ null,
609
+ null,
610
+ null
611
+ );
612
+
613
+ WritableMap uriResult = new WritableNativeMap();
614
+ uriResult.putString("uri", videoUri.toString());
615
+
616
+ if (cursor == null) {
617
+ Log.w(TAG, "🎬 Cursor null for: " + videoUri);
618
+ uriResult.putString("status", "null_cursor");
619
+ uriResult.putInt("count", 0);
620
+ } else {
621
+ int count = cursor.getCount();
622
+ Log.d(TAG, "🎬 Found " + count + " videos in: " + videoUri);
623
+ uriResult.putString("status", "success");
624
+ uriResult.putInt("count", count);
625
+ totalVideos += count;
626
+
627
+ if (count > 0 && cursor.moveToFirst()) {
628
+ long firstVideoId = cursor.getLong(0);
629
+ String firstName = cursor.getString(1);
630
+ uriResult.putString("firstVideoId", String.valueOf(firstVideoId));
631
+ uriResult.putString("firstName", firstName != null ? firstName : "null");
632
+ Log.d(TAG, "🎬 First video: " + firstName + " (ID: " + firstVideoId + ")");
633
+ }
634
+ }
635
+
636
+ uriResults.pushMap(uriResult);
637
+
638
+ } catch (Exception e) {
639
+ Log.e(TAG, "🎬 Error querying " + videoUri + ": " + e.getMessage());
640
+ WritableMap uriResult = new WritableNativeMap();
641
+ uriResult.putString("uri", videoUri.toString());
642
+ uriResult.putString("status", "error");
643
+ uriResult.putString("error", e.getMessage());
644
+ uriResult.putInt("count", 0);
645
+ uriResults.pushMap(uriResult);
646
+ } finally {
647
+ if (cursor != null) {
648
+ cursor.close();
649
+ }
650
+ }
651
+ }
652
+
653
+ result.putArray("uriTests", uriResults);
654
+ result.putInt("totalVideosFound", totalVideos);
655
+
656
+ // Test 3: Call our actual fetchVideoAssets method
657
+ Log.d(TAG, "🎬 Testing fetchVideoAssets(0, 5)...");
658
+ WritableArray assets = fetchVideoAssets(0, 5);
659
+ result.putInt("fetchVideoAssetsCount", assets.size());
660
+ result.putArray("sampleAssets", assets);
661
+
662
+ Log.d(TAG, "🎬 SIMPLE VIDEO TEST COMPLETE - Total videos found: " + totalVideos);
663
+ promise.resolve(result);
664
+
665
+ } catch (Exception e) {
666
+ Log.e(TAG, "🎬 Simple video test failed: " + e.getMessage(), e);
667
+ promise.reject("TEST_ERROR", e.getMessage());
668
+ }
669
+ }
670
+
671
+ @ReactMethod
672
+ public void testVideoQuery(Promise promise) {
673
+ try {
674
+ Log.d(TAG, "=== TESTING VIDEO QUERY WITH CORRECTED IMPLEMENTATION ===");
675
+
676
+ WritableMap result = new WritableNativeMap();
677
+ WritableArray videos = new WritableNativeArray();
678
+
679
+ // Use the same URI logic as the main implementation
680
+ Uri videoUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
681
+
682
+ Log.d(TAG, "Testing URI: " + videoUri);
683
+ Log.d(TAG, "Android API level: " + Build.VERSION.SDK_INT);
684
+ Log.d(TAG, "Video permission granted: " + hasVideoPermission());
685
+
686
+ String[] projection = {
687
+ MediaStore.Video.Media._ID,
688
+ MediaStore.Video.Media.DISPLAY_NAME,
689
+ MediaStore.Video.Media.DATA,
690
+ MediaStore.Video.Media.SIZE,
691
+ MediaStore.Video.Media.DURATION,
692
+ MediaStore.Video.Media.MIME_TYPE,
693
+ MediaStore.Video.Media.DATE_ADDED
694
+ };
695
+
696
+ String sortOrder = MediaStore.Video.Media.DATE_ADDED + " DESC";
697
+
698
+ try (Cursor cursor = getReactApplicationContext().getContentResolver().query(
699
+ videoUri,
700
+ projection,
701
+ null,
702
+ null,
703
+ sortOrder)) {
704
+
705
+ if (cursor != null) {
706
+ int count = cursor.getCount();
707
+ Log.d(TAG, "Query returned " + count + " videos");
708
+ result.putInt("totalVideoCount", count);
709
+
710
+ if (count > 0 && cursor.moveToFirst()) {
711
+ int sampleCount = 0;
712
+ do {
713
+ if (sampleCount >= 5) break; // Limit sample to 5 videos
714
+
715
+ long id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Video.Media._ID));
716
+ String name = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DISPLAY_NAME));
717
+ String data = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DATA));
718
+ long size = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.SIZE));
719
+ long duration = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATION));
720
+ String mimeType = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.MIME_TYPE));
721
+ long dateAdded = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DATE_ADDED));
722
+
723
+ Uri contentUri = ContentUris.withAppendedId(videoUri, id);
724
+
725
+ WritableMap video = new WritableNativeMap();
726
+ video.putString("id", String.valueOf(id));
727
+ video.putString("name", name != null ? name : "null");
728
+ video.putString("path", data != null ? data : "null");
729
+ video.putString("contentUri", contentUri.toString());
730
+ video.putDouble("size", size);
731
+ video.putDouble("duration", duration / 1000.0);
732
+ video.putString("mimeType", mimeType != null ? mimeType : "null");
733
+ video.putDouble("dateAdded", dateAdded * 1000L);
734
+
735
+ videos.pushMap(video);
736
+ sampleCount++;
737
+
738
+ Log.d(TAG, "Sample video " + sampleCount + ": " + name + " (ID: " + id + ", Duration: " + (duration/1000.0) + "s, URI: " + contentUri + ")");
739
+ } while (cursor.moveToNext());
740
+ }
741
+ } else {
742
+ Log.w(TAG, "Cursor is null - query failed");
743
+ result.putInt("totalVideoCount", 0);
744
+ result.putString("error", "Query returned null cursor");
745
+ }
746
+ }
747
+
748
+ result.putArray("sampleVideos", videos);
749
+ result.putString("queryUri", videoUri.toString());
750
+ result.putInt("androidVersion", Build.VERSION.SDK_INT);
751
+ result.putBoolean("hasVideoPermission", hasVideoPermission());
752
+
753
+ Log.d(TAG, "=== VIDEO QUERY TEST COMPLETED ===");
754
+ promise.resolve(result);
755
+
756
+ } catch (Exception e) {
757
+ Log.e(TAG, "Video test failed: " + e.getMessage(), e);
758
+ promise.reject("TEST_ERROR", e.getMessage());
759
+ }
760
+ }
761
+
762
+ @ReactMethod
763
+ public void debugPermissionsAndMediaStore(Promise promise) {
764
+ try {
765
+ WritableMap debugInfo = new WritableNativeMap();
766
+
767
+ // Check permissions
768
+ debugInfo.putBoolean("hasStoragePermission", hasStoragePermission());
769
+ debugInfo.putBoolean("hasImagePermission", hasImagePermission());
770
+ debugInfo.putBoolean("hasVideoPermission", hasVideoPermission());
771
+ debugInfo.putInt("androidVersion", Build.VERSION.SDK_INT);
772
+
773
+ // Try to query MediaStore for IMAGES
774
+ try {
775
+ Cursor cursor = getReactApplicationContext().getContentResolver().query(
776
+ MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
777
+ new String[]{MediaStore.Images.Media._ID, MediaStore.Images.Media.DISPLAY_NAME},
778
+ null,
779
+ null,
780
+ MediaStore.Images.Media._ID + " DESC"
781
+ );
782
+
783
+ if (cursor != null) {
784
+ debugInfo.putInt("mediaStoreImageCount", cursor.getCount());
785
+ WritableArray sampleImages = new WritableNativeArray();
786
+
787
+ while (cursor.moveToNext() && sampleImages.size() < 3) {
788
+ long id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID));
789
+ String name = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME));
790
+
791
+ WritableMap imageInfo = new WritableNativeMap();
792
+ imageInfo.putString("id", String.valueOf(id));
793
+ imageInfo.putString("name", name != null ? name : "null");
794
+ sampleImages.pushMap(imageInfo);
795
+ }
796
+
797
+ debugInfo.putArray("sampleImages", sampleImages);
798
+ cursor.close();
799
+ } else {
800
+ debugInfo.putInt("mediaStoreImageCount", -1);
801
+ debugInfo.putString("mediaStoreImageError", "Cursor is null");
802
+ }
803
+ } catch (Exception e) {
804
+ debugInfo.putString("mediaStoreImageError", e.getMessage());
805
+ debugInfo.putInt("mediaStoreImageCount", -2);
806
+ }
807
+
808
+ // Try to query MediaStore for VIDEOS - test both internal and external
809
+ try {
810
+ // First test: External storage
811
+ Cursor videoCursor = getReactApplicationContext().getContentResolver().query(
812
+ MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
813
+ new String[]{MediaStore.Video.Media._ID, MediaStore.Video.Media.DISPLAY_NAME, MediaStore.Video.Media.DURATION},
814
+ null,
815
+ null,
816
+ MediaStore.Video.Media._ID + " DESC"
817
+ );
818
+
819
+ if (videoCursor != null) {
820
+ debugInfo.putInt("mediaStoreVideoCount", videoCursor.getCount());
821
+ WritableArray sampleVideos = new WritableNativeArray();
822
+
823
+ while (videoCursor.moveToNext() && sampleVideos.size() < 3) {
824
+ long id = videoCursor.getLong(videoCursor.getColumnIndexOrThrow(MediaStore.Video.Media._ID));
825
+ String name = videoCursor.getString(videoCursor.getColumnIndexOrThrow(MediaStore.Video.Media.DISPLAY_NAME));
826
+ long duration = videoCursor.getLong(videoCursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATION));
827
+
828
+ WritableMap videoInfo = new WritableNativeMap();
829
+ videoInfo.putString("id", String.valueOf(id));
830
+ videoInfo.putString("name", name != null ? name : "null");
831
+ videoInfo.putDouble("duration", duration / 1000.0);
832
+ sampleVideos.pushMap(videoInfo);
833
+ }
834
+
835
+ debugInfo.putArray("sampleVideos", sampleVideos);
836
+ videoCursor.close();
837
+ } else {
838
+ debugInfo.putInt("mediaStoreVideoCount", -1);
839
+ debugInfo.putString("mediaStoreVideoError", "Video cursor is null");
840
+ }
841
+ } catch (Exception e) {
842
+ debugInfo.putString("mediaStoreVideoError", e.getMessage());
843
+ debugInfo.putInt("mediaStoreVideoCount", -2);
844
+ }
845
+
846
+ Log.d(TAG, "Debug info: " + debugInfo.toString());
847
+ promise.resolve(debugInfo);
848
+
849
+ } catch (Exception e) {
850
+ Log.e(TAG, "Error getting debug info: " + e.getMessage());
851
+ promise.reject("DEBUG_ERROR", "Failed to get debug info: " + e.getMessage());
852
+ }
853
+ }
854
+
855
+ private boolean hasCameraPermission() {
856
+ return ContextCompat.checkSelfPermission(getReactApplicationContext(),
857
+ Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED;
858
+ }
859
+
860
+ private boolean hasStoragePermission() {
861
+ boolean hasImagePermission = false;
862
+ boolean hasVideoPermission = false;
863
+ String permissions = "";
864
+
865
+ if (Build.VERSION.SDK_INT >= 34) {
866
+ // Android 14+ - check for limited access first, then full access
867
+ boolean hasLimitedAccess = ContextCompat.checkSelfPermission(getReactApplicationContext(),
868
+ READ_MEDIA_VISUAL_USER_SELECTED) == PackageManager.PERMISSION_GRANTED;
869
+ boolean hasFullImageAccess = ContextCompat.checkSelfPermission(getReactApplicationContext(),
870
+ Manifest.permission.READ_MEDIA_IMAGES) == PackageManager.PERMISSION_GRANTED;
871
+ boolean hasFullVideoAccess = ContextCompat.checkSelfPermission(getReactApplicationContext(),
872
+ READ_MEDIA_VIDEO) == PackageManager.PERMISSION_GRANTED;
873
+
874
+ hasImagePermission = hasLimitedAccess || hasFullImageAccess;
875
+ hasVideoPermission = hasLimitedAccess || hasFullVideoAccess;
876
+ permissions = "READ_MEDIA_VISUAL_USER_SELECTED=" + hasLimitedAccess +
877
+ ", READ_MEDIA_IMAGES=" + hasFullImageAccess +
878
+ ", READ_MEDIA_VIDEO=" + hasFullVideoAccess;
879
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
880
+ // Android 13 uses granular media permissions
881
+ hasImagePermission = ContextCompat.checkSelfPermission(getReactApplicationContext(),
882
+ Manifest.permission.READ_MEDIA_IMAGES) == PackageManager.PERMISSION_GRANTED;
883
+ hasVideoPermission = ContextCompat.checkSelfPermission(getReactApplicationContext(),
884
+ READ_MEDIA_VIDEO) == PackageManager.PERMISSION_GRANTED;
885
+ permissions = "READ_MEDIA_IMAGES=" + hasImagePermission + ", READ_MEDIA_VIDEO=" + hasVideoPermission;
886
+ } else {
887
+ // Android 12 and below use legacy storage permission
888
+ hasImagePermission = ContextCompat.checkSelfPermission(getReactApplicationContext(),
889
+ Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
890
+ hasVideoPermission = hasImagePermission; // Same permission for both
891
+ permissions = "READ_EXTERNAL_STORAGE=" + hasImagePermission;
892
+ }
893
+
894
+ // For now, return true if we have either permission (to maintain compatibility)
895
+ boolean hasPermission = hasImagePermission || hasVideoPermission;
896
+ Log.d(TAG, "hasStoragePermission: " + hasPermission + " (" + permissions + ", API: " + Build.VERSION.SDK_INT + ")");
897
+ return hasPermission;
898
+ }
899
+
900
+ private boolean hasImagePermission() {
901
+ if (Build.VERSION.SDK_INT >= 34) {
902
+ // Android 14+
903
+ boolean hasLimitedAccess = ContextCompat.checkSelfPermission(getReactApplicationContext(),
904
+ READ_MEDIA_VISUAL_USER_SELECTED) == PackageManager.PERMISSION_GRANTED;
905
+ boolean hasFullAccess = ContextCompat.checkSelfPermission(getReactApplicationContext(),
906
+ Manifest.permission.READ_MEDIA_IMAGES) == PackageManager.PERMISSION_GRANTED;
907
+ return hasLimitedAccess || hasFullAccess;
908
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
909
+ // Android 13
910
+ return ContextCompat.checkSelfPermission(getReactApplicationContext(),
911
+ Manifest.permission.READ_MEDIA_IMAGES) == PackageManager.PERMISSION_GRANTED;
912
+ } else {
913
+ // Android 12 and below
914
+ return ContextCompat.checkSelfPermission(getReactApplicationContext(),
915
+ Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
916
+ }
917
+ }
918
+
919
+ private boolean hasVideoPermission() {
920
+ boolean hasPermission = false;
921
+ String permissionInfo = "";
922
+
923
+ if (Build.VERSION.SDK_INT >= 34) {
924
+ // Android 14+
925
+ boolean hasLimitedAccess = ContextCompat.checkSelfPermission(getReactApplicationContext(),
926
+ READ_MEDIA_VISUAL_USER_SELECTED) == PackageManager.PERMISSION_GRANTED;
927
+ boolean hasFullAccess = ContextCompat.checkSelfPermission(getReactApplicationContext(),
928
+ READ_MEDIA_VIDEO) == PackageManager.PERMISSION_GRANTED;
929
+ hasPermission = hasLimitedAccess || hasFullAccess;
930
+ permissionInfo = "Android 14+: LIMITED=" + hasLimitedAccess + ", FULL_VIDEO=" + hasFullAccess;
931
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
932
+ // Android 13
933
+ hasPermission = ContextCompat.checkSelfPermission(getReactApplicationContext(),
934
+ READ_MEDIA_VIDEO) == PackageManager.PERMISSION_GRANTED;
935
+ permissionInfo = "Android 13: READ_MEDIA_VIDEO=" + hasPermission;
936
+ } else {
937
+ // Android 12 and below
938
+ hasPermission = ContextCompat.checkSelfPermission(getReactApplicationContext(),
939
+ Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
940
+ permissionInfo = "Android <=12: READ_EXTERNAL_STORAGE=" + hasPermission;
941
+ }
942
+
943
+ Log.d(TAG, "hasVideoPermission: " + hasPermission + " (" + permissionInfo + ")");
944
+ return hasPermission;
945
+ }
946
+
947
+ private boolean isLimitedAccessMode() {
948
+ if (Build.VERSION.SDK_INT >= 34) {
949
+ boolean hasLimitedAccess = ContextCompat.checkSelfPermission(getReactApplicationContext(),
950
+ READ_MEDIA_VISUAL_USER_SELECTED) == PackageManager.PERMISSION_GRANTED;
951
+ boolean hasFullAccess = ContextCompat.checkSelfPermission(getReactApplicationContext(),
952
+ Manifest.permission.READ_MEDIA_IMAGES) == PackageManager.PERMISSION_GRANTED;
953
+
954
+ // Limited access mode means we have limited access but not full access
955
+ return hasLimitedAccess && !hasFullAccess;
956
+ }
957
+ return false;
958
+ }
959
+
960
+ private void requestCameraPermissionInternal() {
961
+ Activity activity = getCurrentActivity();
962
+ if (activity != null) {
963
+ // Show rationale if needed
964
+ if (ActivityCompat.shouldShowRequestPermissionRationale(activity, Manifest.permission.CAMERA)) {
965
+ showPermissionRationale("Camera", "This app needs access to your camera to take photos.");
966
+ }
967
+
968
+ ActivityCompat.requestPermissions(activity,
969
+ new String[]{Manifest.permission.CAMERA},
970
+ CAMERA_PERMISSION_REQUEST_CODE);
971
+ }
972
+ }
973
+
974
+ private void requestStoragePermissionInternal() {
975
+ Activity activity = getCurrentActivity();
976
+ if (activity != null) {
977
+ String[] permissions;
978
+ if (Build.VERSION.SDK_INT >= 34) {
979
+ // Android 14+ - request both full and limited access for images and videos
980
+ permissions = new String[]{
981
+ Manifest.permission.READ_MEDIA_IMAGES,
982
+ READ_MEDIA_VIDEO,
983
+ READ_MEDIA_VISUAL_USER_SELECTED
984
+ };
985
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
986
+ // Android 13 - request both media images and videos
987
+ permissions = new String[]{
988
+ Manifest.permission.READ_MEDIA_IMAGES,
989
+ READ_MEDIA_VIDEO
990
+ };
991
+ } else {
992
+ // Android 12 and below - use legacy permission
993
+ permissions = new String[]{Manifest.permission.READ_EXTERNAL_STORAGE};
994
+ }
995
+
996
+ Log.d(TAG, "Requesting storage permissions: " + java.util.Arrays.toString(permissions));
997
+
998
+ // Check if we should show rationale for the primary permission
999
+ String primaryPermission = permissions[0];
1000
+ if (ActivityCompat.shouldShowRequestPermissionRationale(activity, primaryPermission)) {
1001
+ showPermissionRationale("Storage", "This app needs access to your photos and videos to pick media from gallery.");
1002
+ }
1003
+
1004
+ ActivityCompat.requestPermissions(activity, permissions, STORAGE_PERMISSION_REQUEST_CODE);
1005
+ }
1006
+ }
1007
+
1008
+ private void launchCamera() {
1009
+ try {
1010
+ Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
1011
+
1012
+ if (takePictureIntent.resolveActivity(getReactApplicationContext().getPackageManager()) != null) {
1013
+ currentPhotoFile = createImageFile();
1014
+
1015
+ Uri photoURI = FileProvider.getUriForFile(getReactApplicationContext(),
1016
+ "com.motorinceditor.fileprovider",
1017
+ currentPhotoFile);
1018
+
1019
+ takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI);
1020
+
1021
+ Activity activity = getCurrentActivity();
1022
+ if (activity != null) {
1023
+ activity.startActivityForResult(takePictureIntent, CAMERA_REQUEST_CODE);
1024
+ }
1025
+ } else {
1026
+ rejectPromise("CAMERA_NOT_AVAILABLE", "Camera not available");
1027
+ }
1028
+ } catch (Exception e) {
1029
+ rejectPromise("CAMERA_ERROR", e.getMessage());
1030
+ }
1031
+ }
1032
+
1033
+ private void launchGallery() {
1034
+ Intent pickPhotoIntent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
1035
+ pickPhotoIntent.setType("image/*");
1036
+
1037
+ Activity activity = getCurrentActivity();
1038
+ if (activity != null) {
1039
+ activity.startActivityForResult(pickPhotoIntent, GALLERY_REQUEST_CODE);
1040
+ }
1041
+ }
1042
+
1043
+ private void launchMultiSelectGallery(ReadableMap options) {
1044
+ String mediaType = options.hasKey("mediaType") ? options.getString("mediaType") : "photo";
1045
+
1046
+ Intent pickPhotoIntent = new Intent(Intent.ACTION_PICK);
1047
+
1048
+ if ("video".equals(mediaType)) {
1049
+ pickPhotoIntent.setData(MediaStore.Video.Media.EXTERNAL_CONTENT_URI);
1050
+ pickPhotoIntent.setType("video/*");
1051
+ } else if ("mixed".equals(mediaType)) {
1052
+ pickPhotoIntent.setType("*/*");
1053
+ String[] mimeTypes = {"image/*", "video/*"};
1054
+ pickPhotoIntent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes);
1055
+ } else {
1056
+ pickPhotoIntent.setData(MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
1057
+ pickPhotoIntent.setType("image/*");
1058
+ }
1059
+
1060
+ // Enable multiple selection
1061
+ pickPhotoIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
1062
+
1063
+ Activity activity = getCurrentActivity();
1064
+ if (activity != null) {
1065
+ activity.startActivityForResult(pickPhotoIntent, GALLERY_REQUEST_CODE);
1066
+ }
1067
+ }
1068
+
1069
+ private File createImageFile() throws IOException {
1070
+ String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(new Date());
1071
+ String imageFileName = "IMG_" + timeStamp + "_";
1072
+ File storageDir = new File(getReactApplicationContext().getCacheDir(), "images");
1073
+
1074
+ if (!storageDir.exists()) {
1075
+ storageDir.mkdirs();
1076
+ }
1077
+
1078
+ return File.createTempFile(imageFileName, ".jpg", storageDir);
1079
+ }
1080
+
1081
+ private void handleActivityResult(int requestCode, int resultCode, Intent data) {
1082
+ if (currentPromise == null) return;
1083
+
1084
+ if (resultCode != Activity.RESULT_OK) {
1085
+ WritableMap result = new WritableNativeMap();
1086
+ result.putBoolean("success", false);
1087
+ result.putString("error", "User cancelled");
1088
+ currentPromise.resolve(result);
1089
+ currentPromise = null;
1090
+ return;
1091
+ }
1092
+
1093
+ try {
1094
+ switch (requestCode) {
1095
+ case CAMERA_REQUEST_CODE:
1096
+ handleCameraResult();
1097
+ break;
1098
+ case GALLERY_REQUEST_CODE:
1099
+ handleGalleryResult(data);
1100
+ break;
1101
+ }
1102
+ } catch (Exception e) {
1103
+ rejectPromise("PROCESSING_ERROR", e.getMessage());
1104
+ }
1105
+ }
1106
+
1107
+ private void handleCameraResult() throws IOException {
1108
+ if (currentPhotoFile != null && currentPhotoFile.exists()) {
1109
+ processImage(currentPhotoFile);
1110
+ } else {
1111
+ rejectPromise("FILE_ERROR", "Photo file not found");
1112
+ }
1113
+ }
1114
+
1115
+ private void handleGalleryResult(Intent data) throws IOException {
1116
+ // Check if this is a multi-select request by looking at current options
1117
+ boolean isMultiSelect = currentOptions != null && currentOptions.hasKey("maxSelectionLimit");
1118
+
1119
+ if (isMultiSelect && data.getClipData() != null) {
1120
+ // Multiple images selected
1121
+ processMultipleImages(data.getClipData());
1122
+ } else if (data.getData() != null) {
1123
+ // Single image selection
1124
+ Uri selectedImageUri = data.getData();
1125
+ if (isMultiSelect) {
1126
+ // Convert single selection to multi-select format
1127
+ processSingleImageAsMultiple(selectedImageUri);
1128
+ } else {
1129
+ // Process as single image (existing behavior)
1130
+ File tempFile = createImageFile();
1131
+ copyUriToFile(selectedImageUri, tempFile);
1132
+ processImage(tempFile);
1133
+ }
1134
+ } else {
1135
+ rejectPromise("URI_ERROR", "No image URI found");
1136
+ }
1137
+
1138
+ // Notify that gallery might have changed (similar to iOS)
1139
+ sendEvent("PhotoLibraryChanged");
1140
+ }
1141
+
1142
+ private void copyUriToFile(Uri uri, File file) throws IOException {
1143
+ try (InputStream inputStream = getReactApplicationContext().getContentResolver().openInputStream(uri);
1144
+ FileOutputStream outputStream = new FileOutputStream(file)) {
1145
+
1146
+ byte[] buffer = new byte[4096];
1147
+ int bytesRead;
1148
+ while ((bytesRead = inputStream.read(buffer)) != -1) {
1149
+ outputStream.write(buffer, 0, bytesRead);
1150
+ }
1151
+ }
1152
+ }
1153
+
1154
+ private void processMultipleImages(android.content.ClipData clipData) {
1155
+ try {
1156
+ int maxSelectionLimit = currentOptions.hasKey("maxSelectionLimit") ? currentOptions.getInt("maxSelectionLimit") : 10;
1157
+ boolean includeBase64 = currentOptions.hasKey("includeBase64") ? currentOptions.getBoolean("includeBase64") : true;
1158
+ double quality = currentOptions.hasKey("quality") ? currentOptions.getDouble("quality") : 0.8;
1159
+
1160
+ int mediaCount = Math.min(clipData.getItemCount(), maxSelectionLimit);
1161
+ WritableArray mediaArray = new WritableNativeArray();
1162
+
1163
+ for (int i = 0; i < mediaCount; i++) {
1164
+ Uri mediaUri = clipData.getItemAt(i).getUri();
1165
+ WritableMap mediaData = processMediaUri(mediaUri, i, quality, includeBase64);
1166
+ if (mediaData != null) {
1167
+ mediaArray.pushMap(mediaData);
1168
+ }
1169
+ }
1170
+
1171
+ WritableMap result = new WritableNativeMap();
1172
+ result.putBoolean("success", true);
1173
+ result.putArray("images", mediaArray);
1174
+ result.putInt("count", mediaArray.size());
1175
+
1176
+ currentPromise.resolve(result);
1177
+ currentPromise = null;
1178
+
1179
+ } catch (Exception e) {
1180
+ rejectPromise("PROCESSING_ERROR", "Error processing multiple media items: " + e.getMessage());
1181
+ }
1182
+ }
1183
+
1184
+ private void processSingleImageAsMultiple(Uri mediaUri) {
1185
+ try {
1186
+ boolean includeBase64 = currentOptions.hasKey("includeBase64") ? currentOptions.getBoolean("includeBase64") : true;
1187
+ double quality = currentOptions.hasKey("quality") ? currentOptions.getDouble("quality") : 0.8;
1188
+
1189
+ WritableMap mediaData = processMediaUri(mediaUri, 0, quality, includeBase64);
1190
+ if (mediaData != null) {
1191
+ WritableArray mediaArray = new WritableNativeArray();
1192
+ mediaArray.pushMap(mediaData);
1193
+
1194
+ WritableMap result = new WritableNativeMap();
1195
+ result.putBoolean("success", true);
1196
+ result.putArray("images", mediaArray);
1197
+ result.putInt("count", 1);
1198
+
1199
+ currentPromise.resolve(result);
1200
+ } else {
1201
+ rejectPromise("PROCESSING_ERROR", "Failed to process selected media");
1202
+ }
1203
+ currentPromise = null;
1204
+
1205
+ } catch (Exception e) {
1206
+ rejectPromise("PROCESSING_ERROR", "Error processing single image: " + e.getMessage());
1207
+ }
1208
+ }
1209
+
1210
+ private WritableMap processImageUri(Uri imageUri, int index, double quality, boolean includeBase64) {
1211
+ try {
1212
+ File tempFile = createImageFile();
1213
+ copyUriToFile(imageUri, tempFile);
1214
+
1215
+ Bitmap bitmap = BitmapFactory.decodeFile(tempFile.getAbsolutePath());
1216
+ if (bitmap == null) {
1217
+ Log.e(TAG, "Failed to decode image at index " + index);
1218
+ return null;
1219
+ }
1220
+
1221
+ // Apply options (resize, quality, etc.)
1222
+ if (currentOptions != null) {
1223
+ if (currentOptions.hasKey("maxWidth") || currentOptions.hasKey("maxHeight")) {
1224
+ int maxWidth = currentOptions.hasKey("maxWidth") ?
1225
+ currentOptions.getInt("maxWidth") : bitmap.getWidth();
1226
+ int maxHeight = currentOptions.hasKey("maxHeight") ?
1227
+ currentOptions.getInt("maxHeight") : bitmap.getHeight();
1228
+
1229
+ bitmap = resizeBitmap(bitmap, maxWidth, maxHeight);
1230
+ }
1231
+ }
1232
+
1233
+ // Save processed image
1234
+ File processedFile = createImageFile();
1235
+ saveBitmapToFile(bitmap, processedFile);
1236
+
1237
+ // Create image data object
1238
+ WritableMap imageData = new WritableNativeMap();
1239
+ imageData.putString("uri", "file://" + processedFile.getAbsolutePath());
1240
+ imageData.putString("fileName", processedFile.getName());
1241
+ imageData.putDouble("fileSize", processedFile.length());
1242
+ imageData.putInt("width", bitmap.getWidth());
1243
+ imageData.putInt("height", bitmap.getHeight());
1244
+ imageData.putString("type", "image/jpeg");
1245
+ imageData.putString("id", UUID.randomUUID().toString());
1246
+
1247
+ // Add base64 if requested
1248
+ if (includeBase64) {
1249
+ try {
1250
+ java.io.FileInputStream fileInputStream = new java.io.FileInputStream(processedFile);
1251
+ byte[] imageBytes = new byte[(int) processedFile.length()];
1252
+ fileInputStream.read(imageBytes);
1253
+ fileInputStream.close();
1254
+
1255
+ String base64String = android.util.Base64.encodeToString(imageBytes, android.util.Base64.NO_WRAP);
1256
+ imageData.putString("base64", "data:image/jpeg;base64," + base64String);
1257
+ } catch (Exception e) {
1258
+ Log.w(TAG, "Failed to generate base64 for image " + index + ": " + e.getMessage());
1259
+ }
1260
+ }
1261
+
1262
+ return imageData;
1263
+
1264
+ } catch (Exception e) {
1265
+ Log.e(TAG, "Error processing image at index " + index + ": " + e.getMessage());
1266
+ return null;
1267
+ }
1268
+ }
1269
+
1270
+ private WritableMap processMediaUri(Uri mediaUri, int index, double quality, boolean includeBase64) {
1271
+ try {
1272
+ // Determine if this is a video or image by checking the MIME type
1273
+ ContentResolver resolver = getReactApplicationContext().getContentResolver();
1274
+ String mimeType = resolver.getType(mediaUri);
1275
+
1276
+ if (mimeType != null && mimeType.startsWith("video/")) {
1277
+ return processVideoUri(mediaUri, index, includeBase64);
1278
+ } else {
1279
+ return processImageUri(mediaUri, index, quality, includeBase64);
1280
+ }
1281
+ } catch (Exception e) {
1282
+ Log.e(TAG, "Error processing media at index " + index + ": " + e.getMessage());
1283
+ return null;
1284
+ }
1285
+ }
1286
+
1287
+ private WritableMap processVideoUri(Uri videoUri, int index, boolean includeBase64) {
1288
+ try {
1289
+ Log.d(TAG, "Processing video URI: " + videoUri);
1290
+
1291
+ // Get video metadata
1292
+ ContentResolver resolver = getReactApplicationContext().getContentResolver();
1293
+
1294
+ // Query video metadata
1295
+ String[] projection = {
1296
+ MediaStore.Video.Media._ID,
1297
+ MediaStore.Video.Media.DISPLAY_NAME,
1298
+ MediaStore.Video.Media.SIZE,
1299
+ MediaStore.Video.Media.WIDTH,
1300
+ MediaStore.Video.Media.HEIGHT,
1301
+ MediaStore.Video.Media.DURATION
1302
+ };
1303
+
1304
+ int width = 0, height = 0, duration = 0;
1305
+ long fileSize = 0;
1306
+ String fileName = "video_" + index + ".mp4";
1307
+
1308
+ try (Cursor cursor = resolver.query(videoUri, projection, null, null, null)) {
1309
+ if (cursor != null && cursor.moveToFirst()) {
1310
+ int nameColumn = cursor.getColumnIndex(MediaStore.Video.Media.DISPLAY_NAME);
1311
+ int sizeColumn = cursor.getColumnIndex(MediaStore.Video.Media.SIZE);
1312
+ int widthColumn = cursor.getColumnIndex(MediaStore.Video.Media.WIDTH);
1313
+ int heightColumn = cursor.getColumnIndex(MediaStore.Video.Media.HEIGHT);
1314
+ int durationColumn = cursor.getColumnIndex(MediaStore.Video.Media.DURATION);
1315
+
1316
+ if (nameColumn >= 0) fileName = cursor.getString(nameColumn);
1317
+ if (sizeColumn >= 0) fileSize = cursor.getLong(sizeColumn);
1318
+ if (widthColumn >= 0) width = cursor.getInt(widthColumn);
1319
+ if (heightColumn >= 0) height = cursor.getInt(heightColumn);
1320
+ if (durationColumn >= 0) duration = cursor.getInt(durationColumn);
1321
+ }
1322
+ }
1323
+
1324
+ // Create video data object
1325
+ WritableMap videoData = new WritableNativeMap();
1326
+ videoData.putString("uri", videoUri.toString());
1327
+ videoData.putString("fileName", fileName);
1328
+ videoData.putDouble("fileSize", fileSize);
1329
+ videoData.putInt("width", width);
1330
+ videoData.putInt("height", height);
1331
+ videoData.putString("type", "video/mp4");
1332
+ videoData.putString("id", UUID.randomUUID().toString());
1333
+
1334
+ // Add base64 if requested
1335
+ if (includeBase64) {
1336
+ try {
1337
+ String base64 = getVideoBase64Sync(videoUri.toString());
1338
+ videoData.putString("base64", base64);
1339
+ } catch (Exception e) {
1340
+ Log.w(TAG, "Failed to get base64 for video: " + e.getMessage());
1341
+ videoData.putString("base64", ""); // Fallback to empty string
1342
+ }
1343
+ }
1344
+
1345
+ Log.d(TAG, "Successfully processed video: " + fileName);
1346
+ return videoData;
1347
+
1348
+ } catch (Exception e) {
1349
+ Log.e(TAG, "Error processing video at index " + index + ": " + e.getMessage(), e);
1350
+ return null;
1351
+ }
1352
+ }
1353
+
1354
+ private String getVideoBase64Sync(String videoUri) throws Exception {
1355
+ Uri contentUri = Uri.parse(videoUri);
1356
+ ContentResolver resolver = getReactApplicationContext().getContentResolver();
1357
+
1358
+ // Check file size first to avoid memory issues
1359
+ long fileSize = 0;
1360
+ String[] sizeProjection = {MediaStore.Video.Media.SIZE};
1361
+ try (Cursor cursor = resolver.query(contentUri, sizeProjection, null, null, null)) {
1362
+ if (cursor != null && cursor.moveToFirst()) {
1363
+ int sizeColumn = cursor.getColumnIndex(MediaStore.Video.Media.SIZE);
1364
+ if (sizeColumn >= 0) {
1365
+ fileSize = cursor.getLong(sizeColumn);
1366
+ }
1367
+ }
1368
+ }
1369
+
1370
+ // Skip base64 for large files to avoid OutOfMemoryError (limit to 10MB)
1371
+ if (fileSize > 10 * 1024 * 1024) { // 10MB
1372
+ Log.w(TAG, "Video file too large for base64 conversion: " + fileSize + " bytes. Skipping base64.");
1373
+ return ""; // Return empty string for large files
1374
+ }
1375
+
1376
+ // Read the video file into memory (only for smaller files)
1377
+ try (InputStream inputStream = resolver.openInputStream(contentUri);
1378
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
1379
+
1380
+ if (inputStream == null) {
1381
+ throw new Exception("Could not open video stream for URI: " + videoUri);
1382
+ }
1383
+
1384
+ byte[] buffer = new byte[8192];
1385
+ int bytesRead;
1386
+ long totalBytesRead = 0;
1387
+
1388
+ // Add safety check during reading
1389
+ while ((bytesRead = inputStream.read(buffer)) != -1) {
1390
+ totalBytesRead += bytesRead;
1391
+
1392
+ // Additional safety check during read
1393
+ if (totalBytesRead > 10 * 1024 * 1024) {
1394
+ Log.w(TAG, "Video file exceeded size limit during reading. Stopping base64 conversion.");
1395
+ return "";
1396
+ }
1397
+
1398
+ outputStream.write(buffer, 0, bytesRead);
1399
+ }
1400
+
1401
+ byte[] videoBytes = outputStream.toByteArray();
1402
+ return "data:video/mp4;base64," + Base64.encodeToString(videoBytes, Base64.NO_WRAP);
1403
+ }
1404
+ }
1405
+
1406
+ private void processImage(File imageFile) throws IOException {
1407
+ Bitmap bitmap = BitmapFactory.decodeFile(imageFile.getAbsolutePath());
1408
+
1409
+ if (bitmap == null) {
1410
+ rejectPromise("DECODE_ERROR", "Failed to decode image");
1411
+ return;
1412
+ }
1413
+
1414
+ // Apply options
1415
+ if (currentOptions != null) {
1416
+ if (currentOptions.hasKey("maxWidth") || currentOptions.hasKey("maxHeight")) {
1417
+ int maxWidth = currentOptions.hasKey("maxWidth") ?
1418
+ currentOptions.getInt("maxWidth") : bitmap.getWidth();
1419
+ int maxHeight = currentOptions.hasKey("maxHeight") ?
1420
+ currentOptions.getInt("maxHeight") : bitmap.getHeight();
1421
+
1422
+ bitmap = resizeBitmap(bitmap, maxWidth, maxHeight);
1423
+ }
1424
+ }
1425
+
1426
+ // Save processed image
1427
+ File processedFile = createImageFile();
1428
+ saveBitmapToFile(bitmap, processedFile);
1429
+
1430
+ // Create result
1431
+ WritableMap result = new WritableNativeMap();
1432
+ result.putBoolean("success", true);
1433
+ result.putString("uri", "file://" + processedFile.getAbsolutePath());
1434
+ result.putString("fileName", processedFile.getName());
1435
+ result.putDouble("fileSize", processedFile.length());
1436
+ result.putInt("width", bitmap.getWidth());
1437
+ result.putInt("height", bitmap.getHeight());
1438
+ result.putString("type", "image/jpeg");
1439
+
1440
+ currentPromise.resolve(result);
1441
+ currentPromise = null;
1442
+ }
1443
+
1444
+ private Bitmap resizeBitmap(Bitmap bitmap, int maxWidth, int maxHeight) {
1445
+ int width = bitmap.getWidth();
1446
+ int height = bitmap.getHeight();
1447
+
1448
+ float widthRatio = (float) maxWidth / width;
1449
+ float heightRatio = (float) maxHeight / height;
1450
+ float ratio = Math.min(widthRatio, heightRatio);
1451
+
1452
+ if (ratio >= 1.0f) {
1453
+ return bitmap;
1454
+ }
1455
+
1456
+ int newWidth = Math.round(width * ratio);
1457
+ int newHeight = Math.round(height * ratio);
1458
+
1459
+ return Bitmap.createScaledBitmap(bitmap, newWidth, newHeight, true);
1460
+ }
1461
+
1462
+ private void saveBitmapToFile(Bitmap bitmap, File file) throws IOException {
1463
+ float quality = 80f;
1464
+ if (currentOptions != null && currentOptions.hasKey("quality")) {
1465
+ quality = (float) (currentOptions.getDouble("quality") * 100);
1466
+ }
1467
+
1468
+ try (FileOutputStream outputStream = new FileOutputStream(file)) {
1469
+ bitmap.compress(Bitmap.CompressFormat.JPEG, Math.round(quality), outputStream);
1470
+ }
1471
+ }
1472
+
1473
+ private void rejectPromise(String code, String message) {
1474
+ if (currentPromise != null) {
1475
+ currentPromise.reject(code, message);
1476
+ currentPromise = null;
1477
+ }
1478
+ }
1479
+
1480
+ private void setupPermissionListeners() {
1481
+ ReactApplicationContext context = getReactApplicationContext();
1482
+ if (context != null) {
1483
+ context.addLifecycleEventListener(new LifecycleEventListener() {
1484
+ @Override
1485
+ public void onHostResume() {
1486
+ // Register event listeners when app resumes
1487
+ Log.d(TAG, "App resumed - checking if permission status changed");
1488
+
1489
+ // Emit event to React Native to refresh permission status and gallery
1490
+ sendEvent("onAppResume");
1491
+ }
1492
+
1493
+ @Override
1494
+ public void onHostPause() {
1495
+ // Cleanup if needed - clear any pending operations
1496
+ Log.d(TAG, "App paused during permission flow");
1497
+ }
1498
+
1499
+ @Override
1500
+ public void onHostDestroy() {
1501
+ // Cleanup if needed
1502
+ Log.d(TAG, "App destroyed during permission flow");
1503
+ // Clear any pending promises to prevent crashes
1504
+ if (currentPromise != null) {
1505
+ currentPromise = null;
1506
+ }
1507
+ waitingForCameraPermission = false;
1508
+ waitingForStoragePermission = false;
1509
+ }
1510
+ });
1511
+ }
1512
+ }
1513
+
1514
+ @ReactMethod
1515
+ public void onCameraPermissionResult(boolean granted) {
1516
+ try {
1517
+ if (waitingForCameraPermission) {
1518
+ waitingForCameraPermission = false;
1519
+ if (granted) {
1520
+ // Check if we also need storage permission for camera
1521
+ if (!hasStoragePermission()) {
1522
+ waitingForStoragePermission = true;
1523
+ requestStoragePermissionInternal();
1524
+ return;
1525
+ }
1526
+ launchCamera();
1527
+ } else {
1528
+ rejectPromise("CAMERA_PERMISSION_DENIED", "Camera permission was denied");
1529
+ }
1530
+ }
1531
+ } catch (Exception e) {
1532
+ Log.e(TAG, "Error handling camera permission result: " + e.getMessage());
1533
+ rejectPromise("PERMISSION_ERROR", "Error handling camera permission result");
1534
+ }
1535
+ }
1536
+
1537
+ @ReactMethod
1538
+ public void onStoragePermissionResult(boolean granted) {
1539
+ try {
1540
+ Log.d(TAG, "Storage permission result: " + granted + ", waitingForStoragePermission: " + waitingForStoragePermission);
1541
+
1542
+ if (waitingForStoragePermission) {
1543
+ waitingForStoragePermission = false;
1544
+ if (granted) {
1545
+ if (waitingForCameraPermission) {
1546
+ // This was for camera, now launch camera
1547
+ launchCamera();
1548
+ } else {
1549
+ // This was for gallery or manage photo selection - just resolve with success
1550
+ // The React side will handle refreshing the gallery
1551
+ Log.d(TAG, "Storage permission granted for gallery/manage - no action needed");
1552
+ }
1553
+ } else {
1554
+ rejectPromise("STORAGE_PERMISSION_DENIED", "Storage permission was denied");
1555
+ }
1556
+ }
1557
+ } catch (Exception e) {
1558
+ Log.e(TAG, "Error handling storage permission result: " + e.getMessage());
1559
+ rejectPromise("PERMISSION_ERROR", "Error handling storage permission result");
1560
+ }
1561
+ }
1562
+
1563
+ private void sendEvent(String eventName, WritableMap params) {
1564
+ ReactApplicationContext context = getReactApplicationContext();
1565
+ if (context != null && context.hasActiveCatalystInstance()) {
1566
+ context.getJSModule(RCTNativeAppEventEmitter.class)
1567
+ .emit(eventName, params);
1568
+ }
1569
+ }
1570
+
1571
+ private void sendEvent(String eventName) {
1572
+ sendEvent(eventName, null);
1573
+ }
1574
+
1575
+ private Bitmap loadAndResizeBitmapSafely(Uri contentUri, int targetWidth, int targetHeight) {
1576
+ try {
1577
+ return loadAndResizeBitmap(contentUri, targetWidth, targetHeight);
1578
+ } catch (Exception e) {
1579
+ Log.e(TAG, "Error loading bitmap from URI: " + contentUri, e);
1580
+ return null;
1581
+ }
1582
+ }
1583
+
1584
+ private Bitmap loadAndResizeBitmap(Uri contentUri, int targetWidth, int targetHeight) throws IOException {
1585
+ InputStream inputStream = null;
1586
+ try {
1587
+ inputStream = getReactApplicationContext().getContentResolver().openInputStream(contentUri);
1588
+ if (inputStream == null) {
1589
+ Log.e(TAG, "Failed to open input stream for URI: " + contentUri);
1590
+ return null;
1591
+ }
1592
+
1593
+ // First, get image dimensions without loading the full image
1594
+ BitmapFactory.Options options = new BitmapFactory.Options();
1595
+ options.inJustDecodeBounds = true;
1596
+ BitmapFactory.decodeStream(inputStream, null, options);
1597
+ inputStream.close();
1598
+
1599
+ if (options.outWidth <= 0 || options.outHeight <= 0) {
1600
+ Log.e(TAG, "Invalid image dimensions: " + options.outWidth + "x" + options.outHeight);
1601
+ return null;
1602
+ }
1603
+
1604
+ // Calculate sample size for efficient loading
1605
+ int sampleSize = calculateSampleSize(options.outWidth, options.outHeight, targetWidth, targetHeight);
1606
+ Log.d(TAG, "Original size: " + options.outWidth + "x" + options.outHeight + ", sample size: " + sampleSize);
1607
+
1608
+ // Load the image with sample size
1609
+ inputStream = getReactApplicationContext().getContentResolver().openInputStream(contentUri);
1610
+ if (inputStream == null) {
1611
+ Log.e(TAG, "Failed to reopen input stream for URI: " + contentUri);
1612
+ return null;
1613
+ }
1614
+
1615
+ options.inJustDecodeBounds = false;
1616
+ options.inSampleSize = sampleSize;
1617
+ options.inPreferredConfig = Bitmap.Config.RGB_565; // Use less memory
1618
+
1619
+ Bitmap bitmap = BitmapFactory.decodeStream(inputStream, null, options);
1620
+ inputStream.close();
1621
+
1622
+ if (bitmap == null) {
1623
+ Log.e(TAG, "BitmapFactory.decodeStream returned null for URI: " + contentUri);
1624
+ return null;
1625
+ }
1626
+
1627
+ Log.d(TAG, "Loaded bitmap size: " + bitmap.getWidth() + "x" + bitmap.getHeight());
1628
+
1629
+ // Further resize if needed to match exact target dimensions
1630
+ return resizeBitmap(bitmap, targetWidth, targetHeight);
1631
+
1632
+ } finally {
1633
+ if (inputStream != null) {
1634
+ try {
1635
+ inputStream.close();
1636
+ } catch (IOException e) {
1637
+ Log.e(TAG, "Error closing input stream", e);
1638
+ }
1639
+ }
1640
+ }
1641
+ }
1642
+
1643
+ private int calculateSampleSize(int width, int height, int targetWidth, int targetHeight) {
1644
+ int sampleSize = 1;
1645
+
1646
+ if (height > targetHeight || width > targetWidth) {
1647
+ final int halfHeight = height / 2;
1648
+ final int halfWidth = width / 2;
1649
+
1650
+ while ((halfHeight / sampleSize) >= targetHeight && (halfWidth / sampleSize) >= targetWidth) {
1651
+ sampleSize *= 2;
1652
+ }
1653
+ }
1654
+
1655
+ return sampleSize;
1656
+ }
1657
+
1658
+ private void showPermissionRationale(String permissionType, String message) {
1659
+ // For now, just log the rationale. In a production app, you might want to show a dialog
1660
+ Log.i(TAG, permissionType + " Permission Rationale: " + message);
1661
+
1662
+ // You could emit an event to React Native to show a custom dialog
1663
+ WritableMap params = new WritableNativeMap();
1664
+ params.putString("type", permissionType.toLowerCase());
1665
+ params.putString("message", message);
1666
+
1667
+ ReactApplicationContext context = getReactApplicationContext();
1668
+ if (context != null && context.hasActiveCatalystInstance()) {
1669
+ context.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
1670
+ .emit("onPermissionRationale", params);
1671
+ }
1672
+ }
1673
+
1674
+ private boolean isValidImageFile(Uri uri) {
1675
+ try {
1676
+ InputStream inputStream = getReactApplicationContext().getContentResolver().openInputStream(uri);
1677
+ if (inputStream != null) {
1678
+ inputStream.close();
1679
+ return true;
1680
+ }
1681
+ } catch (Exception e) {
1682
+ Log.w(TAG, "Invalid image file: " + uri + " - " + e.getMessage());
1683
+ }
1684
+ return false;
1685
+ }
1686
+
1687
+ private WritableArray fetchImageAssets(int offset, int limit) {
1688
+ WritableArray assets = new WritableNativeArray();
1689
+ Log.d(TAG, "fetchImageAssets called with offset=" + offset + ", limit=" + limit);
1690
+
1691
+ String[] projection = {
1692
+ MediaStore.Images.Media._ID,
1693
+ MediaStore.Images.Media.DISPLAY_NAME,
1694
+ MediaStore.Images.Media.DATE_ADDED,
1695
+ MediaStore.Images.Media.WIDTH,
1696
+ MediaStore.Images.Media.HEIGHT,
1697
+ MediaStore.Images.Media.SIZE,
1698
+ MediaStore.Images.Media.MIME_TYPE
1699
+ };
1700
+
1701
+ // Use standard SQL LIMIT OFFSET syntax - some Android versions might not support this in MediaStore
1702
+ String sortOrder = MediaStore.Images.Media.DATE_ADDED + " DESC";
1703
+ Log.d(TAG, "Using sortOrder: " + sortOrder + " with manual offset/limit");
1704
+
1705
+ try (Cursor cursor = getReactApplicationContext().getContentResolver().query(
1706
+ MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
1707
+ projection,
1708
+ null,
1709
+ null,
1710
+ sortOrder)) {
1711
+
1712
+ Log.d(TAG, "Images query result: " + (cursor != null ? cursor.getCount() + " items" : "null"));
1713
+
1714
+ if (cursor != null && cursor.getCount() > 0) {
1715
+ // Manual offset/limit handling
1716
+ int currentIndex = 0;
1717
+ int addedCount = 0;
1718
+
1719
+ while (cursor.moveToNext() && addedCount < limit) {
1720
+ // Skip items before offset
1721
+ if (currentIndex < offset) {
1722
+ currentIndex++;
1723
+ continue;
1724
+ }
1725
+
1726
+ try {
1727
+ long id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID));
1728
+ String displayName = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME));
1729
+ long dateAdded = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATE_ADDED));
1730
+ int width = cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.WIDTH));
1731
+ int height = cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.HEIGHT));
1732
+
1733
+ Uri contentUri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id);
1734
+
1735
+ WritableMap asset = new WritableNativeMap();
1736
+ asset.putString("uri", contentUri.toString()); // Use actual content URI
1737
+ asset.putString("id", String.valueOf(id));
1738
+ asset.putString("filename", displayName != null ? displayName : "Unknown");
1739
+ asset.putInt("width", width > 0 ? width : 0);
1740
+ asset.putInt("height", height > 0 ? height : 0);
1741
+ asset.putDouble("creationDate", dateAdded * 1000L);
1742
+ asset.putString("mediaType", "image");
1743
+
1744
+ assets.pushMap(asset);
1745
+ addedCount++;
1746
+ Log.d(TAG, "Added image: " + displayName + " (ID: " + id + ", index: " + currentIndex + ")");
1747
+
1748
+ } catch (Exception e) {
1749
+ Log.w(TAG, "Error processing image row: " + e.getMessage());
1750
+ }
1751
+
1752
+ currentIndex++;
1753
+ }
1754
+
1755
+ Log.d(TAG, "Final result: added " + addedCount + " images starting from offset " + offset);
1756
+ } else {
1757
+ Log.w(TAG, "No images found in MediaStore or cursor is null");
1758
+ }
1759
+ } catch (Exception e) {
1760
+ Log.e(TAG, "Error fetching images: " + e.getMessage(), e);
1761
+ }
1762
+
1763
+ return assets;
1764
+ }
1765
+
1766
+ private WritableArray fetchVideoAssets(int offset, int limit) {
1767
+ WritableArray assets = new WritableNativeArray();
1768
+ Log.d(TAG, "🎬 fetchVideoAssets called with offset=" + offset + ", limit=" + limit);
1769
+
1770
+ String[] projection = {
1771
+ MediaStore.Video.Media._ID,
1772
+ MediaStore.Video.Media.DISPLAY_NAME,
1773
+ MediaStore.Video.Media.DATE_ADDED,
1774
+ MediaStore.Video.Media.WIDTH,
1775
+ MediaStore.Video.Media.HEIGHT,
1776
+ MediaStore.Video.Media.SIZE,
1777
+ MediaStore.Video.Media.MIME_TYPE,
1778
+ MediaStore.Video.Media.DURATION
1779
+ };
1780
+
1781
+ String sortOrder = MediaStore.Video.Media.DATE_ADDED + " DESC";
1782
+ Log.d(TAG, "🎬 Using simple sortOrder: " + sortOrder);
1783
+
1784
+ try (Cursor cursor = getReactApplicationContext().getContentResolver().query(
1785
+ MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
1786
+ projection,
1787
+ null,
1788
+ null,
1789
+ sortOrder)) {
1790
+
1791
+ Log.d(TAG, "🎬 Videos query result: " + (cursor != null ? cursor.getCount() + " items" : "null"));
1792
+
1793
+ if (cursor != null && cursor.getCount() > 0) {
1794
+ int addedCount = 0;
1795
+ int currentIndex = 0;
1796
+
1797
+ // Skip to offset position
1798
+ while (currentIndex < offset && cursor.moveToNext()) {
1799
+ currentIndex++;
1800
+ }
1801
+
1802
+ // Now process items up to limit
1803
+ while (addedCount < limit && cursor.moveToNext()) {
1804
+ try {
1805
+ long id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Video.Media._ID));
1806
+ String displayName = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DISPLAY_NAME));
1807
+ long dateAdded = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DATE_ADDED));
1808
+ int width = cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.WIDTH));
1809
+ int height = cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.HEIGHT));
1810
+ long duration = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATION));
1811
+
1812
+ Uri contentUri = ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, id);
1813
+
1814
+ WritableMap asset = new WritableNativeMap();
1815
+ asset.putString("uri", contentUri.toString());
1816
+ asset.putString("id", String.valueOf(id));
1817
+ asset.putString("filename", displayName != null ? displayName : "Unknown");
1818
+ asset.putInt("width", width > 0 ? width : 0);
1819
+ asset.putInt("height", height > 0 ? height : 0);
1820
+ asset.putDouble("creationDate", dateAdded * 1000L);
1821
+ asset.putString("mediaType", "video");
1822
+ asset.putDouble("duration", duration > 0 ? duration / 1000.0 : 0.0);
1823
+
1824
+ assets.pushMap(asset);
1825
+ addedCount++;
1826
+ Log.d(TAG, "🎬 Added video: " + displayName + " (ID: " + id + ")");
1827
+
1828
+ } catch (Exception e) {
1829
+ Log.w(TAG, "🎬 Error processing video row: " + e.getMessage());
1830
+ }
1831
+ }
1832
+
1833
+ Log.d(TAG, "🎬 Successfully added " + addedCount + " videos with SQL LIMIT/OFFSET");
1834
+ } else {
1835
+ Log.w(TAG, "🎬 No videos found in MediaStore or cursor is null");
1836
+ }
1837
+ } catch (Exception e) {
1838
+ Log.e(TAG, "🎬 Error fetching videos: " + e.getMessage(), e);
1839
+ }
1840
+
1841
+ Log.d(TAG, "🎬 Final result: " + assets.size() + " videos");
1842
+ return assets;
1843
+ }
1844
+
1845
+ private WritableArray fetchMixedMediaAssets(int offset, int limit) {
1846
+ WritableArray allAssets = new WritableNativeArray();
1847
+ Log.d(TAG, "fetchMixedMediaAssets called with offset=" + offset + ", limit=" + limit);
1848
+
1849
+ // For mixed media, we need to query both image and video stores and combine results
1850
+ // This is more complex but more efficient than fetching everything then sorting
1851
+
1852
+ java.util.List<WritableMap> combinedList = new java.util.ArrayList<>();
1853
+
1854
+ // For mixed media, fetch ALL images and videos, then sort and apply pagination
1855
+ // This ensures proper chronological mixing across all media
1856
+
1857
+ // Fetch ALL images
1858
+ WritableArray imageAssets = fetchImageAssets(0, Integer.MAX_VALUE);
1859
+ for (int i = 0; i < imageAssets.size(); i++) {
1860
+ ReadableMap readableMap = imageAssets.getMap(i);
1861
+ WritableMap writableMap = new WritableNativeMap();
1862
+ writableMap.merge(readableMap);
1863
+ combinedList.add(writableMap);
1864
+ }
1865
+
1866
+ // Fetch ALL videos
1867
+ WritableArray videoAssets = fetchVideoAssets(0, Integer.MAX_VALUE);
1868
+ for (int i = 0; i < videoAssets.size(); i++) {
1869
+ ReadableMap readableMap = videoAssets.getMap(i);
1870
+ WritableMap writableMap = new WritableNativeMap();
1871
+ writableMap.merge(readableMap);
1872
+ combinedList.add(writableMap);
1873
+ }
1874
+
1875
+ // Sort by creation date (descending)
1876
+ combinedList.sort((a, b) -> {
1877
+ try {
1878
+ double dateA = a.getDouble("creationDate");
1879
+ double dateB = b.getDouble("creationDate");
1880
+ return Double.compare(dateB, dateA);
1881
+ } catch (Exception e) {
1882
+ Log.w(TAG, "Error comparing dates for sorting: " + e.getMessage());
1883
+ return 0;
1884
+ }
1885
+ });
1886
+
1887
+ // Apply offset and limit
1888
+ int startIndex = Math.min(offset, combinedList.size());
1889
+ int endIndex = Math.min(offset + limit, combinedList.size());
1890
+
1891
+ for (int i = startIndex; i < endIndex; i++) {
1892
+ allAssets.pushMap(combinedList.get(i));
1893
+ }
1894
+
1895
+ Log.d(TAG, "Mixed media result: " + imageAssets.size() + " images + " + videoAssets.size() +
1896
+ " videos = " + combinedList.size() + " total, returning " + allAssets.size() + " items (offset: " + offset + ")");
1897
+
1898
+ return allAssets;
1899
+ }
1900
+
1901
+ private int getTotalMediaCount(String mediaType) {
1902
+ try {
1903
+ int totalCount = 0;
1904
+
1905
+ if ("mixed".equals(mediaType)) {
1906
+ // Count both images and videos
1907
+ totalCount += getTotalImagesCount();
1908
+ totalCount += getTotalVideosCount();
1909
+ } else if ("video".equals(mediaType)) {
1910
+ totalCount = getTotalVideosCount();
1911
+ } else {
1912
+ // Default to images
1913
+ totalCount = getTotalImagesCount();
1914
+ }
1915
+
1916
+ Log.d(TAG, "Total " + mediaType + " count: " + totalCount);
1917
+ return totalCount;
1918
+ } catch (Exception e) {
1919
+ Log.e(TAG, "Error getting total " + mediaType + " count: " + e.getMessage(), e);
1920
+ }
1921
+ return 0;
1922
+ }
1923
+
1924
+ private int getTotalImagesCount() {
1925
+ try (Cursor cursor = getReactApplicationContext().getContentResolver().query(
1926
+ MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
1927
+ new String[]{MediaStore.Images.Media._ID},
1928
+ null,
1929
+ null,
1930
+ null)) {
1931
+
1932
+ return cursor != null ? cursor.getCount() : 0;
1933
+ } catch (Exception e) {
1934
+ Log.e(TAG, "Error getting images count: " + e.getMessage());
1935
+ return 0;
1936
+ }
1937
+ }
1938
+
1939
+ private int getTotalVideosCount() {
1940
+ try (Cursor cursor = getReactApplicationContext().getContentResolver().query(
1941
+ MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
1942
+ new String[]{MediaStore.Video.Media._ID},
1943
+ null,
1944
+ null,
1945
+ null)) {
1946
+
1947
+ return cursor != null ? cursor.getCount() : 0;
1948
+ } catch (Exception e) {
1949
+ Log.e(TAG, "Error getting videos count: " + e.getMessage());
1950
+ return 0;
1951
+ }
1952
+ }
1953
+
1954
+ private boolean assetExists(Uri uri) {
1955
+ try (Cursor cursor = getReactApplicationContext().getContentResolver().query(
1956
+ uri,
1957
+ new String[]{"1"},
1958
+ null,
1959
+ null,
1960
+ null)) {
1961
+
1962
+ return cursor != null && cursor.getCount() > 0;
1963
+ } catch (Exception e) {
1964
+ Log.d(TAG, "Asset does not exist: " + uri);
1965
+ return false;
1966
+ }
1967
+ }
1968
+ }