Commit fa89f65

derdilla <82763757+derdilla@users.noreply.github.com>
2025-01-26 21:41:28
Use new library for exporting data (#515)
* use platform client for file saving * delete old file saving code * add missing default file names on error reporting screen
1 parent 6e3d1eb
Changed files (8)
app
android
app
src
lib
app/android/app/src/main/kotlin/com/derdilla/blood_pressure_app/FileSharer.java
@@ -1,10 +0,0 @@
-package com.derdilla.blood_pressure_app;
-
-
-import androidx.core.content.FileProvider;
-
-public class FileSharer extends FileProvider {
-    public FileSharer() {
-        super(R.xml.file_paths);
-    }
-}
app/android/app/src/main/kotlin/com/derdilla/blood_pressure_app/MainActivity.kt
@@ -8,10 +8,5 @@ import java.io.File
 class MainActivity: FlutterActivity() {
     override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
         super.configureFlutterEngine(flutterEngine)
-        val shareFolder = File(context.cacheDir, "share")
-        if (shareFolder.exists()) shareFolder.deleteRecursively();
-
-        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, StorageProvider.CHANNEL)
-            .setMethodCallHandler(StorageProvider(context, shareFolder))
     }
 }
app/android/app/src/main/kotlin/com/derdilla/blood_pressure_app/StorageProvider.kt
@@ -1,87 +0,0 @@
-package com.derdilla.blood_pressure_app
-
-import android.content.Context
-import android.content.Intent
-import android.net.Uri
-import androidx.core.content.ContextCompat.startActivity
-import androidx.core.content.FileProvider.getUriForFile
-import io.flutter.plugin.common.MethodCall
-import io.flutter.plugin.common.MethodChannel
-import java.io.File
-
-class StorageProvider(private var context: Context,
-    private var shareFolder: File
-    ): MethodChannel.MethodCallHandler {
-    companion object {
-        const val CHANNEL = "bloodPressureApp.derdilla.com/storage"
-    }
-
-    override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
-         when (call.method) {
-             "shareFile" -> shareFile(call.argument<String>("path")!!,
-                 call.argument<String>("mimeType")!!, call.argument<String>("name"))
-             "shareData" -> shareData(call.argument<ByteArray>("data")!!,
-                 call.argument<String>("mimeType")!!, call.argument<String>("name")!!)
-        }
-    }
-
-    /**
-     * Show the share sheet for saving a file.
-     *
-     * @param path The path to the file in internal storage
-     * @param mimeType The mime type that gets send with the intend
-     * @param name Used to make the name of the shared file different from the original name.
-     */
-    private fun shareFile(path: String, mimeType: String, name: String?) {
-        val uri = sharableUriFromPath(path, name)
-
-        val sendIntent: Intent = Intent().apply {
-            action = Intent.ACTION_SEND
-            putExtra(Intent.EXTRA_STREAM, uri)
-            type = mimeType
-        }
-
-        val shareIntent = Intent.createChooser(sendIntent, null)
-        startActivity(context, shareIntent, null)
-    }
-
-    /**
-     * Create a file from data and show start Intent to show a share sheet.
-     *
-     * @param data The data that gets shared
-     * @param mimeType The mime type that gets send with the intend
-     * @param name The name of the shared file
-     */
-    private fun shareData(data: ByteArray, mimeType: String, name: String) {
-        val uri = sharableUriFromData(data, name)
-
-        val sendIntent: Intent = Intent().apply {
-            action = Intent.ACTION_SEND
-            putExtra(Intent.EXTRA_STREAM, uri)
-            type = mimeType
-        }
-
-        val shareIntent = Intent.createChooser(sendIntent, null)
-        startActivity(context, shareIntent, null)
-    }
-
-
-    private fun sharableUriFromPath(path: String, name: String?): Uri {
-        val initialFile = File(path)
-        if (!initialFile.exists()) throw IllegalArgumentException("Tried to create URI from nonexistent file")
-
-        val sharablePath = File(shareFolder, name ?: initialFile.name)
-        if (sharablePath.exists()) sharablePath.delete()
-        initialFile.copyTo(sharablePath)
-        return getUriForFile(context, "com.derdilla.bloodPressureApp.share", sharablePath)
-    }
-
-    private fun sharableUriFromData(data: ByteArray, name: String): Uri {
-        if (!shareFolder.exists()) shareFolder.mkdirs()
-        val sharablePath = File(shareFolder, name)
-        if (sharablePath.exists()) sharablePath.delete()
-        sharablePath.createNewFile()
-        sharablePath.writeBytes(data)
-        return getUriForFile(context, "com.derdilla.bloodPressureApp.share", sharablePath)
-    }
-}
\ No newline at end of file
app/android/app/src/main/AndroidManifest.xml
@@ -55,15 +55,5 @@
         <meta-data
             android:name="flutterEmbedding"
             android:value="2" />
-       <provider
-           android:name="com.derdilla.blood_pressure_app.FileSharer"
-           android:authorities="com.derdilla.bloodPressureApp.share"
-           android:exported="false"
-           android:grantUriPermissions="true">
-           <meta-data
-               android:name="android.support.FILE_PROVIDER_PATHS"
-               android:resource="@xml/file_paths" />
-       </provider>
-
    </application>
 </manifest>
app/lib/features/export_import/export_button_bar.dart
@@ -16,7 +16,6 @@ import 'package:blood_pressure_app/model/storage/export_pdf_settings_store.dart'
 import 'package:blood_pressure_app/model/storage/export_settings_store.dart';
 import 'package:blood_pressure_app/model/storage/interval_store.dart';
 import 'package:blood_pressure_app/model/storage/settings_store.dart';
-import 'package:blood_pressure_app/platform_integration/platform_client.dart';
 import 'package:file_picker/file_picker.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
@@ -231,7 +230,11 @@ Future<List<FullEntry>> _getEntries(BuildContext context) async {
 Future<void> _exportData(BuildContext context, Uint8List data, String fullFileName, String mimeType) async {
   final settings = Provider.of<ExportSettings>(context, listen: false);
   if (settings.defaultExportDir.isEmpty || !Platform.isAndroid) {
-    await PlatformClient.shareData(data, mimeType, fullFileName);
+    await FilePicker.platform.saveFile(
+      type: FileType.any, // mimeType
+      fileName: fullFileName,
+      bytes: data,
+    );
   } else {
     const userDir = PersistentUserDirAccessAndroid();
     await userDir.writeFile(settings.defaultExportDir, fullFileName, mimeType, data);
app/lib/platform_integration/platform_client.dart
@@ -1,68 +0,0 @@
-import 'package:flutter/services.dart';
-
-/// Class that hosts platform specific functions for sharing files, loading files, saving files and asking for
-/// directory permissions.
-///
-/// Steps for expanding this class:
-/// - If the purpose of the class is not related to storage create a new class with a different [_platformChannel] path.
-/// - Open the android folder in Android Studio.
-/// - Implement the new method in `StorageProvider.kt`.
-/// - Add method name and arguments as a condition to the `onMethodCall` in the same class.
-/// - Implement a helper function to this class that calls uses the platform channel (like [shareFile]).
-class PlatformClient {
-
-  PlatformClient._create();
-  /// Platform channel for sharing files, loading files, saving files and asking for directory permissions.
-  static const _platformChannel = MethodChannel('bloodPressureApp.derdilla.com/storage');
-
-  /// Share a file from application storage.
-  ///
-  /// The file present at the [path] specified will be copied to a sharable location. The file in the sharable location
-  /// will be shared with the specified [mimeType] through a
-  /// [Android Sharesheet](https://developer.android.com/training/sharing/send#using-android-system-sharesheet).
-  ///
-  /// The [mimeType] can be any string but should generally follow the `*/*` pattern. All official mime types can be
-  /// found here: https://mimetype.io/all-types
-  ///
-  /// When [name] is set to a non-null value the file will be shared with this name instead of the original file name.
-  /// 
-  /// The returned value indicates whether a [PlatformException] was thrown.
-  static Future<bool> shareFile(String path, String mimeType, [String? name]) async {
-    try {
-      await _platformChannel.invokeMethod('shareFile', {
-        'path': path,
-        'mimeType': mimeType,
-        'name': name,
-      });
-      return true;
-    } on PlatformException {
-      return false;
-    }
-  }
-
-  /// Share binary data like a file.
-  ///
-  /// A file with the [data] will be created and shared with the specified [mimeType] through a
-  /// [Android Sharesheet](https://developer.android.com/training/sharing/send#using-android-system-sharesheet).
-  ///
-  /// The [mimeType] can be any string but should generally follow the `*/*` pattern. All official mime types can be
-  /// found here: https://mimetype.io/all-types
-  ///
-  /// The resulting file name is specified by [name].
-  ///
-  /// The returned value indicates whether a [PlatformException] was thrown.
-  ///
-  /// Using this over [shareFile] is more saves a file copy, when the data is present as binary data but not as a file.
-  static Future<bool> shareData(Uint8List data, String mimeType, String name) async {
-    try {
-      await _platformChannel.invokeMethod('shareData', {
-        'data': data,
-        'mimeType': mimeType,
-        'name': name,
-      });
-      return true;
-    } on PlatformException {
-      return false;
-    }
-  }
-}
app/lib/screens/error_reporting_screen.dart
@@ -1,4 +1,6 @@
-import 'package:blood_pressure_app/platform_integration/platform_client.dart';
+import 'dart:io';
+
+import 'package:file_picker/file_picker.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 import 'package:package_info_plus/package_info_plus.dart';
@@ -107,7 +109,11 @@ class ErrorScreen extends StatelessWidget {
 
                         assert(dbPath != inMemoryDatabasePath);
                         dbPath = join(dbPath, 'blood_pressure.db');
-                        await PlatformClient.shareFile(dbPath, 'application/vnd.sqlite3');
+                        await FilePicker.platform.saveFile(
+                          fileName: 'blood_pressure.db',
+                          bytes: File(dbPath).readAsBytesSync(),
+                          type: FileType.any, // application/vnd.sqlite3
+                        );
                       } catch(e) {
                         scaffoldMessenger.showSnackBar(SnackBar(
                             content: Text('ERR: $e'),),);
@@ -122,7 +128,12 @@ class ErrorScreen extends StatelessWidget {
 
                         assert(dbPath != inMemoryDatabasePath);
                         dbPath = join(dbPath, 'config.db');
-                        await PlatformClient.shareFile(dbPath, 'application/vnd.sqlite3');
+
+                        await FilePicker.platform.saveFile(
+                          fileName: 'config.db',
+                          bytes: File(dbPath).readAsBytesSync(),
+                          type: FileType.any, // application/vnd.sqlite3
+                        );
                       } catch(e) {
                         scaffoldMessenger.showSnackBar(SnackBar(
                             content: Text('ERR: $e'),),);
@@ -137,7 +148,11 @@ class ErrorScreen extends StatelessWidget {
 
                         assert(dbPath != inMemoryDatabasePath);
                         dbPath = join(dbPath, 'medicine.intakes');
-                        await PlatformClient.shareFile(dbPath, 'application/octet-stream');
+                        await FilePicker.platform.saveFile(
+                          fileName: 'medicine.intakes',
+                          bytes: File(dbPath).readAsBytesSync(),
+                          type: FileType.any, // application/octet-stream
+                        );
                       } catch(e) {
                         scaffoldMessenger.showSnackBar(SnackBar(
                           content: Text('ERR: $e'),),);
@@ -152,7 +167,11 @@ class ErrorScreen extends StatelessWidget {
 
                         assert(dbPath != inMemoryDatabasePath);
                         dbPath = join(dbPath, 'bp.db');
-                        await PlatformClient.shareFile(dbPath, 'application/vnd.sqlite3');
+                        await FilePicker.platform.saveFile(
+                          fileName: 'bp.db',
+                          bytes: File(dbPath).readAsBytesSync(),
+                          type: FileType.any, // application/vnd.sqlite3
+                        );
                       } catch(e) {
                         scaffoldMessenger.showSnackBar(SnackBar(
                           content: Text('ERR: $e'),),);
app/lib/screens/settings_screen.dart
@@ -3,7 +3,6 @@ import 'dart:typed_data';
 
 import 'package:archive/archive_io.dart';
 import 'package:blood_pressure_app/components/input_dialoge.dart';
-import 'package:blood_pressure_app/config.dart';
 import 'package:blood_pressure_app/data_util/consistent_future_builder.dart';
 import 'package:blood_pressure_app/features/settings/delete_data_screen.dart';
 import 'package:blood_pressure_app/features/settings/enter_timeformat_dialoge.dart';
@@ -28,7 +27,6 @@ import 'package:blood_pressure_app/model/storage/db/settings_loader.dart';
 import 'package:blood_pressure_app/model/storage/export_columns_store.dart';
 import 'package:blood_pressure_app/model/storage/storage.dart';
 import 'package:blood_pressure_app/model/weight_unit.dart';
-import 'package:blood_pressure_app/platform_integration/platform_client.dart';
 import 'package:file_picker/file_picker.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_gen/gen_l10n/app_localizations.dart';
@@ -354,7 +352,11 @@ class SettingsPage extends StatelessWidget {
                       return;
                     }
                     final archiveData = Uint8List.fromList(compressedArchive);
-                    await PlatformClient.shareData(archiveData, 'application/zip', 'bloodPressureSettings.zip');
+                    await FilePicker.platform.saveFile(
+                      type: FileType.any, // application/zip
+                      fileName: 'bloodPressureSettings.zip',
+                      bytes: archiveData,
+                    );
                   },
                 ),
                 ListTile(