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.
- package/README.md +236 -0
- package/android/build.gradle +22 -0
- package/android/src/main/AndroidManifest.xml +10 -0
- package/android/src/main/java/com/gallerypicker/imagepicker/ImagePickerModule.java +1968 -0
- package/android/src/main/java/com/gallerypicker/imagepicker/ImagePickerPackage.java +24 -0
- package/index.d.ts +129 -0
- package/index.js +18 -0
- package/ios/ImagePickerModule.h +8 -0
- package/ios/ImagePickerModule.m +876 -0
- package/motorinc-gallery-picker-pro.podspec +21 -0
- package/package.json +63 -0
- package/react-native.config.js +13 -0
- package/src/components/ImageCropper.tsx +433 -0
- package/src/components/MainPhotoGallery.tsx +2639 -0
- package/src/components/PhotoAssetImage.tsx +121 -0
- package/src/modules/ImagePickerModule.ts +117 -0
|
@@ -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
|
+
}
|