@capgo/capacitor-file-sharer 8.1.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,59 @@
1
+ ext {
2
+ junitVersion = project.hasProperty('junitVersion') ? rootProject.ext.junitVersion : '4.13.2'
3
+ androidxAppCompatVersion = project.hasProperty('androidxAppCompatVersion') ? rootProject.ext.androidxAppCompatVersion : '1.7.1'
4
+ androidxJunitVersion = project.hasProperty('androidxJunitVersion') ? rootProject.ext.androidxJunitVersion : '1.3.0'
5
+ androidxEspressoCoreVersion = project.hasProperty('androidxEspressoCoreVersion') ? rootProject.ext.androidxEspressoCoreVersion : '3.7.0'
6
+ }
7
+
8
+ buildscript {
9
+ repositories {
10
+ google()
11
+ mavenCentral()
12
+ }
13
+ dependencies {
14
+ classpath 'com.android.tools.build:gradle:8.13.0'
15
+ }
16
+ }
17
+
18
+ apply plugin: 'com.android.library'
19
+
20
+ android {
21
+ namespace = "app.capgo.filesharer"
22
+ compileSdk = project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 36
23
+ defaultConfig {
24
+ minSdkVersion project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion : 24
25
+ targetSdkVersion project.hasProperty('targetSdkVersion') ? rootProject.ext.targetSdkVersion : 36
26
+ versionCode 1
27
+ versionName "1.0"
28
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
29
+ }
30
+ buildTypes {
31
+ release {
32
+ minifyEnabled false
33
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
34
+ }
35
+ }
36
+ lint {
37
+ abortOnError = false
38
+ }
39
+ compileOptions {
40
+ sourceCompatibility JavaVersion.VERSION_21
41
+ targetCompatibility JavaVersion.VERSION_21
42
+ }
43
+ }
44
+
45
+ repositories {
46
+ google()
47
+ mavenCentral()
48
+ }
49
+
50
+
51
+ dependencies {
52
+ implementation fileTree(dir: 'libs', include: ['*.jar'])
53
+ implementation project(':capacitor-android')
54
+ annotationProcessor project(':capacitor-android')
55
+ implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
56
+ testImplementation "junit:junit:$junitVersion"
57
+ androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
58
+ androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
59
+ }
@@ -0,0 +1,15 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" />
3
+
4
+ <application>
5
+ <provider
6
+ android:name="app.capgo.filesharer.FileSharerProvider"
7
+ android:authorities="${applicationId}.filesharer.fileprovider"
8
+ android:exported="false"
9
+ android:grantUriPermissions="true">
10
+ <meta-data
11
+ android:name="android.support.FILE_PROVIDER_PATHS"
12
+ android:resource="@xml/sharing_paths" />
13
+ </provider>
14
+ </application>
15
+ </manifest>
@@ -0,0 +1,68 @@
1
+ package app.capgo.filesharer;
2
+
3
+ import android.util.Base64;
4
+ import android.util.Base64InputStream;
5
+ import java.io.ByteArrayInputStream;
6
+ import java.io.InputStream;
7
+ import java.net.URLDecoder;
8
+ import java.nio.charset.StandardCharsets;
9
+
10
+ public class FileSharer {
11
+
12
+ public String getPluginVersion() {
13
+ return "8.0.0";
14
+ }
15
+
16
+ public String safeFilename(String filename) {
17
+ if (filename == null) {
18
+ return null;
19
+ }
20
+
21
+ String trimmed = filename.trim().replace('\\', '/');
22
+ int lastSlash = trimmed.lastIndexOf('/');
23
+ String safeName = lastSlash >= 0 ? trimmed.substring(lastSlash + 1) : trimmed;
24
+ return safeName.isEmpty() ? null : safeName;
25
+ }
26
+
27
+ public String normalizeBase64(String base64Data) {
28
+ String trimmed = base64Data.trim();
29
+ int commaIndex = trimmed.indexOf(',');
30
+
31
+ if (commaIndex > -1 && trimmed.substring(0, commaIndex).toLowerCase().contains("base64")) {
32
+ trimmed = trimmed.substring(commaIndex + 1);
33
+ }
34
+
35
+ return trimmed.replaceAll("\\s", "");
36
+ }
37
+
38
+ public InputStream base64InputStream(String base64Data) {
39
+ String payload = normalizeBase64(base64Data);
40
+ return new Base64InputStream(new ByteArrayInputStream(payload.getBytes(StandardCharsets.US_ASCII)), Base64.DEFAULT);
41
+ }
42
+
43
+ public String contentTypeFromDataUrl(String base64Data) {
44
+ String trimmed = base64Data.trim();
45
+ int commaIndex = trimmed.indexOf(',');
46
+
47
+ if (commaIndex > -1) {
48
+ String header = trimmed.substring(0, commaIndex).toLowerCase();
49
+ if (header.startsWith("data:") && header.contains(";base64")) {
50
+ return trimmed.substring(5, header.indexOf(";base64"));
51
+ }
52
+ }
53
+
54
+ return null;
55
+ }
56
+
57
+ public String normalizePath(String path) {
58
+ String trimmed = path.trim();
59
+ int markerIndex = trimmed.indexOf("_capacitor_file_");
60
+
61
+ if (markerIndex > -1) {
62
+ String rawPath = trimmed.substring(markerIndex + "_capacitor_file_".length());
63
+ return URLDecoder.decode(rawPath, StandardCharsets.UTF_8);
64
+ }
65
+
66
+ return trimmed;
67
+ }
68
+ }
@@ -0,0 +1,413 @@
1
+ package app.capgo.filesharer;
2
+
3
+ import android.content.ActivityNotFoundException;
4
+ import android.content.ClipData;
5
+ import android.content.ContentResolver;
6
+ import android.content.ContentValues;
7
+ import android.content.Intent;
8
+ import android.content.pm.PackageManager;
9
+ import android.content.pm.ResolveInfo;
10
+ import android.net.Uri;
11
+ import android.os.Build;
12
+ import android.os.Environment;
13
+ import android.provider.MediaStore;
14
+ import android.webkit.MimeTypeMap;
15
+ import com.getcapacitor.JSObject;
16
+ import com.getcapacitor.Plugin;
17
+ import com.getcapacitor.PluginCall;
18
+ import com.getcapacitor.PluginMethod;
19
+ import com.getcapacitor.annotation.CapacitorPlugin;
20
+ import java.io.File;
21
+ import java.io.FileInputStream;
22
+ import java.io.FileOutputStream;
23
+ import java.io.IOException;
24
+ import java.io.InputStream;
25
+ import java.io.OutputStream;
26
+ import java.util.List;
27
+ import java.util.Locale;
28
+ import java.util.Objects;
29
+
30
+ @CapacitorPlugin(name = "FileSharer")
31
+ public class FileSharerPlugin extends Plugin {
32
+
33
+ private static final String CAP_FILE_SHARER_TEMP = "capfilesharer";
34
+ private static final String DEFAULT_CONTENT_TYPE = "application/octet-stream";
35
+ private static final String ERR_PARAM_NO_FILENAME = "ERR_PARAM_NO_FILENAME";
36
+ private static final String ERR_PARAM_NO_DATA = "ERR_PARAM_NO_DATA";
37
+ private static final String ERR_PARAM_DATA_INVALID = "ERR_PARAM_DATA_INVALID";
38
+ private static final String ERR_LOCAL_FILE_NOT_FOUND = "ERR_LOCAL_FILE_NOT_FOUND";
39
+ private static final String ERR_FILE_CACHING_FAILED = "ERR_FILE_CACHING_FAILED";
40
+ private static final String ERR_FILE_SAVE_FAILED = "ERR_FILE_SAVE_FAILED";
41
+ private static final String ERR_ACTIVITY_NOT_FOUND = "ERR_ACTIVITY_NOT_FOUND";
42
+ private static final int URI_GRANT_FLAGS = Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
43
+
44
+ private final FileSharer implementation = new FileSharer();
45
+
46
+ @PluginMethod
47
+ public void share(PluginCall call) {
48
+ final String filename;
49
+ final File cachedFile;
50
+ final String contentType;
51
+
52
+ try {
53
+ filename = requireFilename(call);
54
+ cachedFile = new File(getCacheDir(), filename);
55
+
56
+ try (Source source = openSource(call); OutputStream outputStream = new FileOutputStream(cachedFile)) {
57
+ contentType = source.contentType;
58
+ copy(source.inputStream, outputStream);
59
+ }
60
+ } catch (FileSharerException exception) {
61
+ reject(call, exception.code, exception);
62
+ return;
63
+ } catch (IllegalArgumentException exception) {
64
+ reject(call, ERR_PARAM_DATA_INVALID, exception);
65
+ return;
66
+ } catch (IOException exception) {
67
+ reject(call, ERR_FILE_CACHING_FAILED, exception);
68
+ return;
69
+ }
70
+
71
+ Uri contentUri = FileSharerProvider.getUriForFile(getContext(), getProviderAuthority(), cachedFile);
72
+ Intent sendIntent = buildShareIntent(call, contentUri, contentType, filename);
73
+ Intent chooser = Intent.createChooser(sendIntent, chooserTitle(call, filename));
74
+ chooser.addFlags(URI_GRANT_FLAGS);
75
+ grantUriPermissions(sendIntent, contentUri);
76
+
77
+ try {
78
+ getActivity().startActivity(chooser);
79
+ call.resolve();
80
+ } catch (ActivityNotFoundException exception) {
81
+ reject(call, ERR_ACTIVITY_NOT_FOUND, exception);
82
+ }
83
+ }
84
+
85
+ @PluginMethod
86
+ public void save(PluginCall call) {
87
+ final String filename;
88
+ final Uri uri;
89
+
90
+ try {
91
+ filename = requireFilename(call);
92
+
93
+ try (Source source = openSource(call)) {
94
+ uri = saveToPublicCollection(call, filename, source);
95
+ }
96
+ } catch (FileSharerException exception) {
97
+ reject(call, exception.code, exception);
98
+ return;
99
+ } catch (IllegalArgumentException exception) {
100
+ reject(call, ERR_PARAM_DATA_INVALID, exception);
101
+ return;
102
+ } catch (IOException exception) {
103
+ reject(call, ERR_FILE_SAVE_FAILED, exception);
104
+ return;
105
+ }
106
+
107
+ JSObject result = new JSObject();
108
+ result.put("uri", uri.toString());
109
+ call.resolve(result);
110
+ }
111
+
112
+ @PluginMethod
113
+ public void getPluginVersion(PluginCall call) {
114
+ JSObject result = new JSObject();
115
+ result.put("version", implementation.getPluginVersion());
116
+ call.resolve(result);
117
+ }
118
+
119
+ private Intent buildShareIntent(PluginCall call, Uri contentUri, String contentType, String filename) {
120
+ Intent sendIntent = new Intent(Intent.ACTION_SEND);
121
+ sendIntent.setTypeAndNormalize(contentType);
122
+ sendIntent.putExtra(Intent.EXTRA_STREAM, contentUri);
123
+ sendIntent.putExtra(Intent.EXTRA_TITLE, firstNonEmpty(call.getString("title"), filename));
124
+ sendIntent.setClipData(ClipData.newUri(getContext().getContentResolver(), filename, contentUri));
125
+ sendIntent.addFlags(URI_GRANT_FLAGS);
126
+ sendIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
127
+
128
+ String subject = firstNonEmpty(call.getString("subject"), call.getString("title"));
129
+ if (subject != null) {
130
+ sendIntent.putExtra(Intent.EXTRA_SUBJECT, subject);
131
+ }
132
+
133
+ String text = trimToNull(call.getString("text"));
134
+ if (text != null) {
135
+ sendIntent.putExtra(Intent.EXTRA_TEXT, text);
136
+ }
137
+
138
+ return sendIntent;
139
+ }
140
+
141
+ private Source openSource(PluginCall call) throws FileSharerException, IOException {
142
+ String requestedContentType = trimToNull(call.getString("contentType"));
143
+ String base64Data = trimToNull(call.getString("base64Data"));
144
+
145
+ if (base64Data != null) {
146
+ String contentType = firstNonEmpty(requestedContentType, implementation.contentTypeFromDataUrl(base64Data));
147
+ return new Source(implementation.base64InputStream(base64Data), firstNonEmpty(contentType, DEFAULT_CONTENT_TYPE));
148
+ }
149
+
150
+ String path = trimToNull(call.getString("path"));
151
+ if (path == null) {
152
+ throw new FileSharerException(ERR_PARAM_NO_DATA);
153
+ }
154
+
155
+ Uri uri = Uri.parse(implementation.normalizePath(path));
156
+ if (uri.getScheme() == null) {
157
+ uri = Uri.fromFile(new File(uri.toString()));
158
+ }
159
+
160
+ if (ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) {
161
+ InputStream inputStream = getContext().getContentResolver().openInputStream(uri);
162
+ if (inputStream == null) {
163
+ throw new FileSharerException(ERR_LOCAL_FILE_NOT_FOUND);
164
+ }
165
+
166
+ String contentType = firstNonEmpty(requestedContentType, getContext().getContentResolver().getType(uri));
167
+ return new Source(inputStream, firstNonEmpty(contentType, contentTypeFromFilename(uri.getLastPathSegment())));
168
+ }
169
+
170
+ File file = fileFromUri(uri);
171
+ if (!file.exists() || !file.isFile()) {
172
+ throw new FileSharerException(ERR_LOCAL_FILE_NOT_FOUND);
173
+ }
174
+
175
+ String contentType = firstNonEmpty(requestedContentType, contentTypeFromFilename(file.getName()));
176
+ return new Source(new FileInputStream(file), firstNonEmpty(contentType, DEFAULT_CONTENT_TYPE));
177
+ }
178
+
179
+ private Uri saveToPublicCollection(PluginCall call, String filename, Source source) throws IOException, FileSharerException {
180
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
181
+ return saveWithMediaStore(call, filename, source);
182
+ }
183
+
184
+ return saveLegacy(call, filename, source);
185
+ }
186
+
187
+ private Uri saveWithMediaStore(PluginCall call, String filename, Source source) throws IOException, FileSharerException {
188
+ ContentResolver resolver = getContext().getContentResolver();
189
+ ContentValues values = new ContentValues();
190
+ values.put(MediaStore.MediaColumns.DISPLAY_NAME, filename);
191
+ values.put(MediaStore.MediaColumns.MIME_TYPE, source.contentType);
192
+ values.put(MediaStore.MediaColumns.RELATIVE_PATH, relativePath(call, source.contentType));
193
+ values.put(MediaStore.MediaColumns.IS_PENDING, 1);
194
+
195
+ Uri collectionUri = collectionUri(call, source.contentType);
196
+ Uri itemUri = resolver.insert(collectionUri, values);
197
+ if (itemUri == null) {
198
+ throw new FileSharerException(ERR_FILE_SAVE_FAILED);
199
+ }
200
+
201
+ try (OutputStream outputStream = resolver.openOutputStream(itemUri)) {
202
+ if (outputStream == null) {
203
+ throw new FileSharerException(ERR_FILE_SAVE_FAILED);
204
+ }
205
+ copy(source.inputStream, outputStream);
206
+ } catch (IOException | FileSharerException exception) {
207
+ resolver.delete(itemUri, null, null);
208
+ throw exception;
209
+ }
210
+
211
+ values.clear();
212
+ values.put(MediaStore.MediaColumns.IS_PENDING, 0);
213
+ resolver.update(itemUri, values, null, null);
214
+ return itemUri;
215
+ }
216
+
217
+ @SuppressWarnings("deprecation")
218
+ private Uri saveLegacy(PluginCall call, String filename, Source source) throws IOException {
219
+ File directory = Environment.getExternalStoragePublicDirectory(legacyDirectory(call, source.contentType));
220
+ if (!directory.exists() && !directory.mkdirs()) {
221
+ throw new IOException("Unable to create save directory");
222
+ }
223
+
224
+ File outputFile = new File(directory, filename);
225
+ try (OutputStream outputStream = new FileOutputStream(outputFile)) {
226
+ copy(source.inputStream, outputStream);
227
+ }
228
+ return Uri.fromFile(outputFile);
229
+ }
230
+
231
+ private Uri collectionUri(PluginCall call, String contentType) {
232
+ String saveDirectory = androidOption(call, "saveDirectory");
233
+ String normalizedDirectory = saveDirectory == null ? "" : saveDirectory.toLowerCase(Locale.US);
234
+
235
+ if ("pictures".equals(normalizedDirectory) || contentType.startsWith("image/")) {
236
+ return MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
237
+ }
238
+ if ("movies".equals(normalizedDirectory) || contentType.startsWith("video/")) {
239
+ return MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
240
+ }
241
+ if ("music".equals(normalizedDirectory) || contentType.startsWith("audio/")) {
242
+ return MediaStore.Audio.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
243
+ }
244
+ return MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
245
+ }
246
+
247
+ private String relativePath(PluginCall call, String contentType) {
248
+ String customRelativePath = trimToNull(androidOption(call, "relativePath"));
249
+ if (customRelativePath != null) {
250
+ return sanitizeRelativePath(customRelativePath);
251
+ }
252
+
253
+ return legacyDirectory(call, contentType);
254
+ }
255
+
256
+ private String legacyDirectory(PluginCall call, String contentType) {
257
+ String saveDirectory = androidOption(call, "saveDirectory");
258
+ String normalizedDirectory = saveDirectory == null ? "" : saveDirectory.toLowerCase(Locale.US);
259
+
260
+ if ("pictures".equals(normalizedDirectory) || contentType.startsWith("image/")) {
261
+ return Environment.DIRECTORY_PICTURES;
262
+ }
263
+ if ("movies".equals(normalizedDirectory) || contentType.startsWith("video/")) {
264
+ return Environment.DIRECTORY_MOVIES;
265
+ }
266
+ if ("music".equals(normalizedDirectory) || contentType.startsWith("audio/")) {
267
+ return Environment.DIRECTORY_MUSIC;
268
+ }
269
+ if ("documents".equals(normalizedDirectory)) {
270
+ return Environment.DIRECTORY_DOCUMENTS;
271
+ }
272
+ return Environment.DIRECTORY_DOWNLOADS;
273
+ }
274
+
275
+ private File getCacheDir() throws FileSharerException {
276
+ File cacheDir = new File(getContext().getCacheDir(), CAP_FILE_SHARER_TEMP);
277
+ if (!cacheDir.exists() && !cacheDir.mkdirs()) {
278
+ throw new FileSharerException(ERR_FILE_CACHING_FAILED);
279
+ }
280
+
281
+ File[] cachedFiles = cacheDir.listFiles();
282
+ if (cachedFiles != null) {
283
+ for (File cachedFile : cachedFiles) {
284
+ if (cachedFile.isFile()) {
285
+ cachedFile.delete();
286
+ }
287
+ }
288
+ }
289
+
290
+ return cacheDir;
291
+ }
292
+
293
+ private String requireFilename(PluginCall call) throws FileSharerException {
294
+ String filename = implementation.safeFilename(call.getString("filename"));
295
+ if (filename == null) {
296
+ throw new FileSharerException(ERR_PARAM_NO_FILENAME);
297
+ }
298
+ return filename;
299
+ }
300
+
301
+ private File fileFromUri(Uri uri) {
302
+ if (ContentResolver.SCHEME_FILE.equals(uri.getScheme())) {
303
+ return new File(Objects.requireNonNull(uri.getPath()));
304
+ }
305
+ return new File(uri.toString());
306
+ }
307
+
308
+ private String contentTypeFromFilename(String filename) {
309
+ String safeFilename = filename == null ? "" : filename;
310
+ String extension = MimeTypeMap.getFileExtensionFromUrl(safeFilename);
311
+ if (extension == null || extension.isEmpty()) {
312
+ int dotIndex = safeFilename.lastIndexOf('.');
313
+ extension = dotIndex > -1 ? safeFilename.substring(dotIndex + 1) : "";
314
+ }
315
+
316
+ String contentType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension.toLowerCase(Locale.US));
317
+ return firstNonEmpty(contentType, DEFAULT_CONTENT_TYPE);
318
+ }
319
+
320
+ private void grantUriPermissions(Intent sendIntent, Uri contentUri) {
321
+ PackageManager packageManager = getContext().getPackageManager();
322
+ List<ResolveInfo> resolveInfos = packageManager.queryIntentActivities(sendIntent, PackageManager.MATCH_DEFAULT_ONLY);
323
+ for (ResolveInfo resolveInfo : resolveInfos) {
324
+ getContext().grantUriPermission(resolveInfo.activityInfo.packageName, contentUri, URI_GRANT_FLAGS);
325
+ }
326
+ }
327
+
328
+ private String chooserTitle(PluginCall call, String filename) {
329
+ return firstNonEmpty(androidOption(call, "chooserTitle"), call.getString("title"), filename);
330
+ }
331
+
332
+ private String androidOption(PluginCall call, String key) {
333
+ JSObject androidOptions = call.getObject("android", new JSObject());
334
+ return trimToNull(androidOptions.getString(key));
335
+ }
336
+
337
+ private String sanitizeRelativePath(String relativePath) {
338
+ StringBuilder sanitized = new StringBuilder();
339
+ for (String segment : relativePath.replace('\\', '/').split("/")) {
340
+ String trimmed = segment.trim();
341
+ if (trimmed.isEmpty() || ".".equals(trimmed) || "..".equals(trimmed)) {
342
+ continue;
343
+ }
344
+ if (sanitized.length() > 0) {
345
+ sanitized.append('/');
346
+ }
347
+ sanitized.append(trimmed);
348
+ }
349
+ return sanitized.length() == 0 ? Environment.DIRECTORY_DOWNLOADS : sanitized.toString();
350
+ }
351
+
352
+ private void copy(InputStream inputStream, OutputStream outputStream) throws IOException {
353
+ byte[] buffer = new byte[1024 * 32];
354
+ int bytesRead;
355
+ while ((bytesRead = inputStream.read(buffer)) != -1) {
356
+ outputStream.write(buffer, 0, bytesRead);
357
+ }
358
+ outputStream.flush();
359
+ }
360
+
361
+ private String getProviderAuthority() {
362
+ return getContext().getPackageName() + ".filesharer.fileprovider";
363
+ }
364
+
365
+ private String trimToNull(String value) {
366
+ if (value == null) {
367
+ return null;
368
+ }
369
+
370
+ String trimmed = value.trim();
371
+ return trimmed.isEmpty() ? null : trimmed;
372
+ }
373
+
374
+ private String firstNonEmpty(String... values) {
375
+ for (String value : values) {
376
+ String trimmed = trimToNull(value);
377
+ if (trimmed != null) {
378
+ return trimmed;
379
+ }
380
+ }
381
+ return null;
382
+ }
383
+
384
+ private void reject(PluginCall call, String code, Exception exception) {
385
+ call.reject(code, code, exception);
386
+ }
387
+
388
+ private static class Source implements AutoCloseable {
389
+
390
+ private final InputStream inputStream;
391
+ private final String contentType;
392
+
393
+ private Source(InputStream inputStream, String contentType) {
394
+ this.inputStream = inputStream;
395
+ this.contentType = contentType;
396
+ }
397
+
398
+ @Override
399
+ public void close() throws IOException {
400
+ inputStream.close();
401
+ }
402
+ }
403
+
404
+ private static class FileSharerException extends Exception {
405
+
406
+ private final String code;
407
+
408
+ private FileSharerException(String code) {
409
+ super(code);
410
+ this.code = code;
411
+ }
412
+ }
413
+ }
@@ -0,0 +1,5 @@
1
+ package app.capgo.filesharer;
2
+
3
+ import androidx.core.content.FileProvider;
4
+
5
+ public class FileSharerProvider extends FileProvider {}
File without changes
@@ -0,0 +1,4 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <paths>
3
+ <cache-path name="capfilesharer" path="capfilesharer/" />
4
+ </paths>