diff --git a/android/app/src/main/kotlin/xyz/project/violet/MainActivity.kt b/android/app/src/main/kotlin/xyz/project/violet/MainActivity.kt index a9348adba..372a58ab8 100644 --- a/android/app/src/main/kotlin/xyz/project/violet/MainActivity.kt +++ b/android/app/src/main/kotlin/xyz/project/violet/MainActivity.kt @@ -1,5 +1,7 @@ package xyz.project.violet +import android.app.Activity +import android.content.Intent import android.os.Build import android.os.Bundle import android.os.Environment @@ -13,6 +15,8 @@ import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugins.GeneratedPluginRegistrant +import java.io.FileInputStream +import java.io.FileOutputStream class MainActivity : FlutterFragmentActivity() { private val VOLUME_CHANNEL = "xyz.project.violet/volume" @@ -82,6 +86,7 @@ class MainActivity : FlutterFragmentActivity() { MethodChannel(flutterEngine.dartExecutor.binaryMessenger, MISC_CHANNEL).setMethodCallHandler { call, result -> when (call.method) { "finishMainActivity" -> finishMainActivity(call, result) + "exportFile" -> exportFile(call, result) else -> result.notImplemented() } } @@ -100,4 +105,68 @@ class MainActivity : FlutterFragmentActivity() { finish() result.success(null) } + + private class ExportFileRequest( + val filePath: String, + val call: MethodCall, + val result: MethodChannel.Result, + ) + + private var nextExportFileRequestCode = 1000001; + private val exportFileRequestMap = hashMapOf() + + private fun exportFile(call: MethodCall, result: MethodChannel.Result) { + val filePath = call.argument("filePath") + + if (filePath == null) { + result.error("noArgument", "filePath", null) + return + } + + try { + val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + type = "application/vnd.sqlite3" + putExtra(Intent.EXTRA_TITLE, "violet-bookmarks.db") + } + + exportFileRequestMap[nextExportFileRequestCode] = + ExportFileRequest(filePath, call, result) + startActivityForResult(intent, nextExportFileRequestCode++); + } catch (e: Throwable) { + result.error("exception", e.toString(), e); + } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + + val request = exportFileRequestMap[requestCode] ?: return + + try { + if (resultCode != Activity.RESULT_OK) { + request.result.error("intentResultFail", "resultCode=$resultCode", null) + return + } + + try { + val uri = data!!.data!! + val targetFileDescriptor = contentResolver.openFileDescriptor(uri, "w") + + val input = FileInputStream(request.filePath) + val output = FileOutputStream(targetFileDescriptor!!.fileDescriptor) + + input.copyTo(output) + + input.close() + output.close() + + request.result.success(null) + } catch (e: Throwable) { + request.result.error("exception", e.toString(), e); + } + } finally { + exportFileRequestMap.remove(requestCode) + } + } } diff --git a/lib/pages/settings/settings_page.dart b/lib/pages/settings/settings_page.dart index 4472b5c04..482425f1c 100644 --- a/lib/pages/settings/settings_page.dart +++ b/lib/pages/settings/settings_page.dart @@ -60,6 +60,7 @@ import 'package:violet/pages/settings/tag_rebuild_page.dart'; import 'package:violet/pages/settings/tag_selector.dart'; import 'package:violet/pages/settings/version_page.dart'; import 'package:violet/pages/splash/splash_page.dart'; +import 'package:violet/platform/misc.dart'; import 'package:violet/server/violet.dart'; import 'package:violet/settings/settings.dart'; import 'package:violet/style/palette.dart'; @@ -1994,36 +1995,43 @@ class _SettingsPageState extends State title: Text(Translations.of(context).trans('exportingbookmark')), trailing: const Icon(Icons.keyboard_arrow_right), onTap: () async { - if (!await Permission.storage.isGranted) { - if (await Permission.storage.request() == - PermissionStatus.denied) { - flutterToast.showToast( - child: ToastWrapper( - isCheck: false, - msg: Translations.of(context).trans('noauth'), - ), - gravity: ToastGravity.BOTTOM, - toastDuration: const Duration(seconds: 4), - ); + final dir = Platform.isIOS + ? await getApplicationSupportDirectory() + : (await getApplicationDocumentsDirectory()); + final bookmarkDatabaseFile = File('${dir.path}/user.db'); - return; - } - } + if (Platform.isAndroid) { + await PlatformMiscMethods.instance.exportFile( + bookmarkDatabaseFile.path, + ); + } else { + if (!await Permission.storage.isGranted) { + if (await Permission.storage.request() == + PermissionStatus.denied) { + flutterToast.showToast( + child: ToastWrapper( + isCheck: false, + msg: Translations.of(context).trans('noauth'), + ), + gravity: ToastGravity.BOTTOM, + toastDuration: const Duration(seconds: 4), + ); - final selectedPath = await FilePicker.platform.getDirectoryPath(); + return; + } + } - if (selectedPath == null) { - return; - } + final selectedPath = + await FilePicker.platform.getDirectoryPath(); - final db = Platform.isIOS - ? await getApplicationSupportDirectory() - : (await getApplicationDocumentsDirectory()); - final dbfile = File('${db.path}/user.db'); + if (selectedPath == null) { + return; + } - final extpath = '$selectedPath/bookmark.db'; + final extpath = '$selectedPath/bookmark.db'; - await dbfile.copy(extpath); + await bookmarkDatabaseFile.copy(extpath); + } flutterToast.showToast( child: ToastWrapper( diff --git a/lib/platform/misc.dart b/lib/platform/misc.dart index 886c33a7c..369a0f7c4 100644 --- a/lib/platform/misc.dart +++ b/lib/platform/misc.dart @@ -16,4 +16,17 @@ class PlatformMiscMethods { await _methodChannel.invokeMethod('finishMainActivity'); } + + Future exportFile(String filePath) async { + if (!Platform.isAndroid) { + throw UnsupportedError('Android only'); + } + + await _methodChannel.invokeMethod( + 'exportFile', + { + 'filePath': filePath, + }, + ); + } }