@capgo/capacitor-file-picker 8.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,473 @@
1
+ package app.capgo.capacitor.filepicker;
2
+
3
+ import android.Manifest;
4
+ import android.app.Activity;
5
+ import android.content.ClipData;
6
+ import android.content.Intent;
7
+ import android.database.Cursor;
8
+ import android.graphics.BitmapFactory;
9
+ import android.media.MediaMetadataRetriever;
10
+ import android.net.Uri;
11
+ import android.os.Build;
12
+ import android.os.Environment;
13
+ import android.provider.DocumentsContract;
14
+ import android.provider.MediaStore;
15
+ import android.provider.OpenableColumns;
16
+ import android.util.Base64;
17
+ import android.webkit.MimeTypeMap;
18
+ import androidx.activity.result.ActivityResult;
19
+ import androidx.annotation.Nullable;
20
+ import com.getcapacitor.JSArray;
21
+ import com.getcapacitor.JSObject;
22
+ import com.getcapacitor.Plugin;
23
+ import com.getcapacitor.PluginCall;
24
+ import com.getcapacitor.PluginMethod;
25
+ import com.getcapacitor.annotation.ActivityCallback;
26
+ import com.getcapacitor.annotation.CapacitorPlugin;
27
+ import com.getcapacitor.annotation.Permission;
28
+ import com.getcapacitor.annotation.PermissionCallback;
29
+ import java.io.ByteArrayOutputStream;
30
+ import java.io.File;
31
+ import java.io.FileInputStream;
32
+ import java.io.FileOutputStream;
33
+ import java.io.InputStream;
34
+ import java.io.OutputStream;
35
+ import java.util.ArrayList;
36
+ import java.util.List;
37
+ import org.json.JSONException;
38
+
39
+ @CapacitorPlugin(
40
+ name = "CapgoFilePicker",
41
+ permissions = {
42
+ @Permission(
43
+ alias = "readExternalStorage",
44
+ strings = {
45
+ Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.READ_MEDIA_IMAGES, Manifest.permission.READ_MEDIA_VIDEO
46
+ }
47
+ ),
48
+ @Permission(alias = "accessMediaLocation", strings = { Manifest.permission.ACCESS_MEDIA_LOCATION })
49
+ }
50
+ )
51
+ public class CapgoFilePickerPlugin extends Plugin {
52
+
53
+ private final String pluginVersion = "8.0.0";
54
+ private boolean readData = false;
55
+
56
+ // MARK: - Pick Files
57
+
58
+ @PluginMethod
59
+ public void pickFiles(PluginCall call) {
60
+ readData = call.getBoolean("readData", false);
61
+ int limit = call.getInt("limit", 0);
62
+
63
+ Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
64
+ intent.setType("*/*");
65
+
66
+ JSArray types = call.getArray("types");
67
+ if (types != null && types.length() > 0) {
68
+ try {
69
+ String[] mimeTypes = new String[types.length()];
70
+ for (int i = 0; i < types.length(); i++) {
71
+ String type = types.getString(i);
72
+ if (type.startsWith(".")) {
73
+ String ext = type.substring(1);
74
+ String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext);
75
+ mimeTypes[i] = mimeType != null ? mimeType : "*/*";
76
+ } else {
77
+ mimeTypes[i] = type;
78
+ }
79
+ }
80
+ intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes);
81
+ } catch (JSONException e) {
82
+ // Use default type
83
+ }
84
+ }
85
+
86
+ intent.addCategory(Intent.CATEGORY_OPENABLE);
87
+ intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, limit != 1);
88
+
89
+ startActivityForResult(call, intent, "pickFilesResult");
90
+ }
91
+
92
+ // MARK: - Pick Images
93
+
94
+ @PluginMethod
95
+ public void pickImages(PluginCall call) {
96
+ readData = call.getBoolean("readData", false);
97
+ int limit = call.getInt("limit", 0);
98
+
99
+ Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
100
+ intent.setType("image/*");
101
+ intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, limit != 1);
102
+
103
+ startActivityForResult(call, intent, "pickMediaResult");
104
+ }
105
+
106
+ // MARK: - Pick Videos
107
+
108
+ @PluginMethod
109
+ public void pickVideos(PluginCall call) {
110
+ readData = call.getBoolean("readData", false);
111
+ int limit = call.getInt("limit", 0);
112
+
113
+ Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Video.Media.EXTERNAL_CONTENT_URI);
114
+ intent.setType("video/*");
115
+ intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, limit != 1);
116
+
117
+ startActivityForResult(call, intent, "pickMediaResult");
118
+ }
119
+
120
+ // MARK: - Pick Media
121
+
122
+ @PluginMethod
123
+ public void pickMedia(PluginCall call) {
124
+ readData = call.getBoolean("readData", false);
125
+ int limit = call.getInt("limit", 0);
126
+
127
+ Intent intent;
128
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
129
+ intent = new Intent(MediaStore.ACTION_PICK_IMAGES);
130
+ intent.setType("*/*");
131
+ intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[] { "image/*", "video/*" });
132
+ if (limit > 1) {
133
+ intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, limit);
134
+ }
135
+ } else {
136
+ intent = new Intent(Intent.ACTION_GET_CONTENT);
137
+ intent.setType("*/*");
138
+ intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[] { "image/*", "video/*" });
139
+ intent.addCategory(Intent.CATEGORY_OPENABLE);
140
+ intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, limit != 1);
141
+ }
142
+
143
+ startActivityForResult(call, intent, "pickMediaResult");
144
+ }
145
+
146
+ // MARK: - Pick Directory
147
+
148
+ @PluginMethod
149
+ public void pickDirectory(PluginCall call) {
150
+ Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
151
+ intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
152
+
153
+ startActivityForResult(call, intent, "pickDirectoryResult");
154
+ }
155
+
156
+ // MARK: - Convert HEIC to JPEG
157
+
158
+ @PluginMethod
159
+ public void convertHeicToJpeg(PluginCall call) {
160
+ call.reject("convertHeicToJpeg is not supported on Android");
161
+ }
162
+
163
+ // MARK: - Copy File
164
+
165
+ @PluginMethod
166
+ public void copyFile(PluginCall call) {
167
+ String from = call.getString("from");
168
+ String to = call.getString("to");
169
+ boolean overwrite = call.getBoolean("overwrite", false);
170
+
171
+ if (from == null || to == null) {
172
+ call.reject("from and to are required");
173
+ return;
174
+ }
175
+
176
+ try {
177
+ File fromFile = new File(from);
178
+ File toFile = new File(to);
179
+
180
+ if (!fromFile.exists()) {
181
+ call.reject("Source file does not exist");
182
+ return;
183
+ }
184
+
185
+ if (toFile.exists() && !overwrite) {
186
+ call.reject("Destination file already exists");
187
+ return;
188
+ }
189
+
190
+ // Create parent directories if needed
191
+ File parentDir = toFile.getParentFile();
192
+ if (parentDir != null && !parentDir.exists()) {
193
+ parentDir.mkdirs();
194
+ }
195
+
196
+ try (InputStream in = new FileInputStream(fromFile); OutputStream out = new FileOutputStream(toFile)) {
197
+ byte[] buffer = new byte[8192];
198
+ int length;
199
+ while ((length = in.read(buffer)) > 0) {
200
+ out.write(buffer, 0, length);
201
+ }
202
+ }
203
+
204
+ call.resolve();
205
+ } catch (Exception e) {
206
+ call.reject("Failed to copy file: " + e.getMessage());
207
+ }
208
+ }
209
+
210
+ // MARK: - Permissions
211
+
212
+ @PluginMethod
213
+ public void checkPermissions(PluginCall call) {
214
+ JSObject result = new JSObject();
215
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
216
+ result.put("readExternalStorage", "granted");
217
+ } else {
218
+ String state = getPermissionState("readExternalStorage").toString().toLowerCase();
219
+ result.put("readExternalStorage", state);
220
+ }
221
+ call.resolve(result);
222
+ }
223
+
224
+ @PluginMethod
225
+ public void requestPermissions(PluginCall call) {
226
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
227
+ JSObject result = new JSObject();
228
+ result.put("readExternalStorage", "granted");
229
+ call.resolve(result);
230
+ } else {
231
+ requestPermissionForAlias("readExternalStorage", call, "permissionCallback");
232
+ }
233
+ }
234
+
235
+ @PermissionCallback
236
+ private void permissionCallback(PluginCall call) {
237
+ JSObject result = new JSObject();
238
+ String state = getPermissionState("readExternalStorage").toString().toLowerCase();
239
+ result.put("readExternalStorage", state);
240
+ call.resolve(result);
241
+ }
242
+
243
+ // MARK: - Plugin Version
244
+
245
+ @PluginMethod
246
+ public void getPluginVersion(PluginCall call) {
247
+ JSObject ret = new JSObject();
248
+ ret.put("version", this.pluginVersion);
249
+ call.resolve(ret);
250
+ }
251
+
252
+ // MARK: - Activity Results
253
+
254
+ @ActivityCallback
255
+ private void pickFilesResult(PluginCall call, ActivityResult result) {
256
+ handleFilePickerResult(call, result);
257
+ }
258
+
259
+ @ActivityCallback
260
+ private void pickMediaResult(PluginCall call, ActivityResult result) {
261
+ handleFilePickerResult(call, result);
262
+ }
263
+
264
+ private void handleFilePickerResult(PluginCall call, ActivityResult result) {
265
+ if (call == null) {
266
+ return;
267
+ }
268
+
269
+ if (result.getResultCode() != Activity.RESULT_OK) {
270
+ notifyListeners("pickerDismissed", new JSObject());
271
+ JSObject ret = new JSObject();
272
+ ret.put("files", new JSArray());
273
+ call.resolve(ret);
274
+ return;
275
+ }
276
+
277
+ Intent data = result.getData();
278
+ if (data == null) {
279
+ JSObject ret = new JSObject();
280
+ ret.put("files", new JSArray());
281
+ call.resolve(ret);
282
+ return;
283
+ }
284
+
285
+ List<Uri> uris = new ArrayList<>();
286
+
287
+ ClipData clipData = data.getClipData();
288
+ if (clipData != null) {
289
+ for (int i = 0; i < clipData.getItemCount(); i++) {
290
+ uris.add(clipData.getItemAt(i).getUri());
291
+ }
292
+ } else if (data.getData() != null) {
293
+ uris.add(data.getData());
294
+ }
295
+
296
+ JSArray files = new JSArray();
297
+ for (Uri uri : uris) {
298
+ JSObject fileInfo = getFileInfo(uri);
299
+ if (fileInfo != null) {
300
+ files.put(fileInfo);
301
+ }
302
+ }
303
+
304
+ JSObject ret = new JSObject();
305
+ ret.put("files", files);
306
+ call.resolve(ret);
307
+ }
308
+
309
+ @ActivityCallback
310
+ private void pickDirectoryResult(PluginCall call, ActivityResult result) {
311
+ if (call == null) {
312
+ return;
313
+ }
314
+
315
+ if (result.getResultCode() != Activity.RESULT_OK) {
316
+ notifyListeners("pickerDismissed", new JSObject());
317
+ call.reject("User cancelled");
318
+ return;
319
+ }
320
+
321
+ Intent data = result.getData();
322
+ if (data == null || data.getData() == null) {
323
+ call.reject("No directory selected");
324
+ return;
325
+ }
326
+
327
+ Uri uri = data.getData();
328
+ String path = getPathFromUri(uri);
329
+
330
+ JSObject ret = new JSObject();
331
+ ret.put("path", path != null ? path : uri.toString());
332
+ call.resolve(ret);
333
+ }
334
+
335
+ // MARK: - Helpers
336
+
337
+ @Nullable
338
+ private JSObject getFileInfo(Uri uri) {
339
+ try {
340
+ JSObject result = new JSObject();
341
+
342
+ // Get name and size from content resolver
343
+ try (Cursor cursor = getContext().getContentResolver().query(uri, null, null, null, null)) {
344
+ if (cursor != null && cursor.moveToFirst()) {
345
+ int nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
346
+ int sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE);
347
+
348
+ if (nameIndex >= 0) {
349
+ result.put("name", cursor.getString(nameIndex));
350
+ }
351
+ if (sizeIndex >= 0) {
352
+ result.put("size", cursor.getLong(sizeIndex));
353
+ }
354
+ }
355
+ }
356
+
357
+ // Get MIME type
358
+ String mimeType = getContext().getContentResolver().getType(uri);
359
+ result.put("mimeType", mimeType != null ? mimeType : "application/octet-stream");
360
+
361
+ // Get path
362
+ String path = getPathFromUri(uri);
363
+ result.put("path", path != null ? path : uri.toString());
364
+
365
+ // Get modified date
366
+ Long modifiedAt = getModifiedAt(uri);
367
+ if (modifiedAt != null) {
368
+ result.put("modifiedAt", modifiedAt);
369
+ }
370
+
371
+ // Get dimensions for images
372
+ if (mimeType != null && mimeType.startsWith("image/")) {
373
+ try (InputStream is = getContext().getContentResolver().openInputStream(uri)) {
374
+ BitmapFactory.Options options = new BitmapFactory.Options();
375
+ options.inJustDecodeBounds = true;
376
+ BitmapFactory.decodeStream(is, null, options);
377
+ result.put("width", options.outWidth);
378
+ result.put("height", options.outHeight);
379
+ } catch (Exception e) {
380
+ // Ignore dimension errors
381
+ }
382
+ }
383
+
384
+ // Get dimensions and duration for videos
385
+ if (mimeType != null && mimeType.startsWith("video/")) {
386
+ try {
387
+ MediaMetadataRetriever retriever = new MediaMetadataRetriever();
388
+ retriever.setDataSource(getContext(), uri);
389
+
390
+ String width = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH);
391
+ String height = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT);
392
+ String duration = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION);
393
+ String rotation = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION);
394
+
395
+ int w = width != null ? Integer.parseInt(width) : 0;
396
+ int h = height != null ? Integer.parseInt(height) : 0;
397
+ int rot = rotation != null ? Integer.parseInt(rotation) : 0;
398
+
399
+ // Swap dimensions if rotated 90 or 270 degrees
400
+ if (rot == 90 || rot == 270) {
401
+ result.put("width", h);
402
+ result.put("height", w);
403
+ } else {
404
+ result.put("width", w);
405
+ result.put("height", h);
406
+ }
407
+
408
+ if (duration != null) {
409
+ result.put("duration", Long.parseLong(duration) / 1000.0);
410
+ }
411
+
412
+ retriever.release();
413
+ } catch (Exception e) {
414
+ // Ignore metadata errors
415
+ }
416
+ }
417
+
418
+ // Read data if requested
419
+ if (readData) {
420
+ try (InputStream is = getContext().getContentResolver().openInputStream(uri)) {
421
+ if (is != null) {
422
+ ByteArrayOutputStream buffer = new ByteArrayOutputStream();
423
+ byte[] data = new byte[8192];
424
+ int length;
425
+ while ((length = is.read(data)) != -1) {
426
+ buffer.write(data, 0, length);
427
+ }
428
+ result.put("data", Base64.encodeToString(buffer.toByteArray(), Base64.NO_WRAP));
429
+ }
430
+ } catch (Exception e) {
431
+ // Ignore data read errors
432
+ }
433
+ }
434
+
435
+ return result;
436
+ } catch (Exception e) {
437
+ return null;
438
+ }
439
+ }
440
+
441
+ @Nullable
442
+ private String getPathFromUri(Uri uri) {
443
+ if (uri == null) {
444
+ return null;
445
+ }
446
+
447
+ // Try to get actual file path
448
+ if ("file".equals(uri.getScheme())) {
449
+ return uri.getPath();
450
+ }
451
+
452
+ // For content URIs, return the URI string
453
+ return uri.toString();
454
+ }
455
+
456
+ @Nullable
457
+ private Long getModifiedAt(Uri uri) {
458
+ try {
459
+ String[] projection = { MediaStore.MediaColumns.DATE_MODIFIED };
460
+ try (Cursor cursor = getContext().getContentResolver().query(uri, projection, null, null, null)) {
461
+ if (cursor != null && cursor.moveToFirst()) {
462
+ int columnIndex = cursor.getColumnIndex(MediaStore.MediaColumns.DATE_MODIFIED);
463
+ if (columnIndex >= 0) {
464
+ return cursor.getLong(columnIndex) * 1000; // Convert to milliseconds
465
+ }
466
+ }
467
+ }
468
+ } catch (Exception e) {
469
+ // Ignore
470
+ }
471
+ return null;
472
+ }
473
+ }