owenclave / app/src/main/java/io/nekohasekai/sagernet/ui/BackupFragment.kt

github - dev - back to tree - upstream
/****************************************************************************** * Copyright (C) 2021 by nekohasekai * * * * This program is free software: you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ******************************************************************************/ package io.nekohasekai.sagernet.ui import android.content.Intent import android.net.Uri import android.os.Bundle import android.os.Parcel import android.os.Parcelable import android.provider.OpenableColumns import android.view.View import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AlertDialog import androidx.core.content.FileProvider import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.isVisible import androidx.core.view.updatePadding import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.gson.GsonBuilder import com.google.gson.JsonArray import com.google.gson.JsonObject import com.jakewharton.processphoenix.ProcessPhoenix import io.nekohasekai.sagernet.BuildConfig import io.nekohasekai.sagernet.R import io.nekohasekai.sagernet.SagerNet import io.nekohasekai.sagernet.database.* import io.nekohasekai.sagernet.database.preference.KeyValuePair import io.nekohasekai.sagernet.database.preference.PublicDatabase import io.nekohasekai.sagernet.databinding.LayoutBackupBinding import io.nekohasekai.sagernet.databinding.LayoutImportBinding import io.nekohasekai.sagernet.databinding.LayoutProgressBinding import io.nekohasekai.sagernet.ktx.* import java.io.File import kotlin.io.encoding.Base64 class BackupFragment : NamedFragment(R.layout.layout_backup) { var content = "" private val exportSettings = registerForActivityResult(ActivityResultContracts.CreateDocument()) { data -> if (data != null) { runOnDefaultDispatcher { try { requireActivity().contentResolver.openOutputStream( data )!!.bufferedWriter().use { it.write(content) } onMainDispatcher { snackbar(getString(R.string.action_export_msg)).show() } } catch (e: Exception) { Logs.w(e) onMainDispatcher { snackbar(e.readableMessage).show() } } } } } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val binding = LayoutBackupBinding.bind(view) ViewCompat.setOnApplyWindowInsetsListener(binding.root) { v, insets -> val bars = insets.getInsets( WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout() ) v.updatePadding( left = bars.left, right = bars.right, bottom = bars.bottom + dp2px(64), ) insets } binding.actionExport.setOnClickListener { runOnDefaultDispatcher { content = doBackup( binding.backupConfigurations.isChecked, binding.backupRules.isChecked, binding.backupSettings.isChecked ) onMainDispatcher { startFilesForResult( exportSettings, "owenclave_backup_${System.currentTimeMillis()}.json" ) } } } binding.actionShare.setOnClickListener { runOnDefaultDispatcher { content = doBackup( binding.backupConfigurations.isChecked, binding.backupRules.isChecked, binding.backupSettings.isChecked ) app.cacheDir.mkdirs() val cacheFile = File( app.cacheDir, "owenclave_backup_${System.currentTimeMillis()}.json" ) cacheFile.writeText(content) onMainDispatcher { startActivity( Intent.createChooser( Intent(Intent.ACTION_SEND).setType("application/json") .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) .putExtra( Intent.EXTRA_STREAM, FileProvider.getUriForFile( app, BuildConfig.APPLICATION_ID + ".cache", cacheFile ) ), app.getString(androidx.appcompat.R.string.abc_shareactionprovider_share_with) ) ) } } } binding.actionImportFile.setOnClickListener { startFilesForResult(importFile, "*/*") } } fun Parcelable.toBase64Str(): String { val parcel = Parcel.obtain() writeToParcel(parcel, 0) try { return Base64.encode(parcel.marshall()) } finally { parcel.recycle() } } fun doBackup(profile: Boolean, rule: Boolean, setting: Boolean): String { val out = JsonObject() out.addProperty("version", 1) if (profile) { out.add("profiles", JsonArray().apply { SagerDatabase.proxyDao.getAll().forEach { add(it.toBase64Str()) } }) out.add("groups", JsonArray().apply { SagerDatabase.groupDao.allGroups().forEach { add(it.toBase64Str()) } }) } if (rule) { out.add("rules", JsonArray().apply { SagerDatabase.rulesDao.allRules().forEach { add(it.toBase64Str()) } }) out.add("assets", JsonArray().apply { SagerDatabase.assetDao.getAll().forEach { add(it.toBase64Str()) } }) } if (setting) { out.add("settings", JsonArray().apply { PublicDatabase.kvPairDao.all().forEach { add(it.toBase64Str()) } }) } return GsonBuilder().setPrettyPrinting().create().toJson(out) } val importFile = registerForActivityResult(ActivityResultContracts.GetContent()) { file -> if (file != null) { runOnDefaultDispatcher { startImport(file) } } } suspend fun startImport(file: Uri) { val fileName = requireContext().contentResolver.query(file, null, null, null, null) ?.use { cursor -> cursor.moveToFirst() cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME).let(cursor::getString) } ?.takeIf { it.isNotBlank() } ?: file.pathSegments.last() .substringAfterLast('/') .substringAfter(':') if (!fileName.endsWith(".json")) { onMainDispatcher { snackbar(getString(R.string.backup_not_file, fileName)).show() } return } suspend fun invalid() = onMainDispatcher { onMainDispatcher { snackbar(getString(R.string.invalid_backup_file)).show() } } val content = try { parseJson((requireContext().contentResolver.openInputStream(file) ?: return).use { it.bufferedReader().readText() }).asJsonObject } catch (e: Exception) { Logs.w(e) invalid() return } val version = content.getInt("version") if (version == null || version != 1) { invalid() return } onMainDispatcher { val import = LayoutImportBinding.inflate(layoutInflater) if (!content.contains("profiles")) { import.backupConfigurations.isVisible = false } if (!content.contains("rules")) { import.backupRules.isVisible = false } if (!content.contains("settings")) { import.backupSettings.isVisible = false } MaterialAlertDialogBuilder(requireContext()).setTitle(R.string.backup_import) .setView(import.root) .setPositiveButton(R.string.backup_import) { _, _ -> SagerNet.stopService() val binding = LayoutProgressBinding.inflate(layoutInflater) binding.content.text = getString(R.string.backup_importing) val dialog = AlertDialog.Builder(requireContext()) .setView(binding.root) .setCancelable(false) .show() runOnDefaultDispatcher { runCatching { finishImport( content, import.backupConfigurations.isChecked, import.backupRules.isChecked, import.backupSettings.isChecked ) ProcessPhoenix.triggerRebirth( requireContext(), Intent(requireContext(), MainActivity::class.java) ) }.onFailure { Logs.w(it) onMainDispatcher { snackbar(it.readableMessage).show() } } onMainDispatcher { dialog.dismiss() } } } .setNegativeButton(android.R.string.cancel, null) .show() } } fun finishImport( content: JsonObject, profile: Boolean, rule: Boolean, setting: Boolean ) { if (profile && content.contains("profiles")) { val profiles = mutableListOf() content.getStringArray("profiles")?.forEach { val data = Base64.decode(it) val parcel = Parcel.obtain() parcel.unmarshall(data, 0, data.size) parcel.setDataPosition(0) profiles.add(ProxyEntity.CREATOR.createFromParcel(parcel)) parcel.recycle() } SagerDatabase.proxyDao.reset() SagerDatabase.proxyDao.insert(profiles) val groups = mutableListOf() content.getStringArray("groups")?.forEach { val data = Base64.decode(it) val parcel = Parcel.obtain() parcel.unmarshall(data, 0, data.size) parcel.setDataPosition(0) groups.add(ProxyGroup.CREATOR.createFromParcel(parcel)) parcel.recycle() } SagerDatabase.groupDao.reset() SagerDatabase.groupDao.insert(groups) } if (rule && content.contains("rules")) { val rules = mutableListOf() content.getStringArray("rules")?.forEach { val data = Base64.decode(it) val parcel = Parcel.obtain() parcel.unmarshall(data, 0, data.size) parcel.setDataPosition(0) rules.add(ParcelizeBridge.createRule(parcel)) parcel.recycle() } SagerDatabase.rulesDao.reset() SagerDatabase.rulesDao.insert(rules) val assets = mutableListOf() content.getStringArray("assets")?.forEach { val data = Base64.decode(it) val parcel = Parcel.obtain() parcel.unmarshall(data, 0, data.size) parcel.setDataPosition(0) assets.add(ParcelizeBridge.createAsset(parcel)) parcel.recycle() } SagerDatabase.assetDao.reset() SagerDatabase.assetDao.insert(assets) } if (setting && content.contains("settings")) { val settings = mutableListOf() content.getStringArray("settings")?.forEach { val data = Base64.decode(it) val parcel = Parcel.obtain() parcel.unmarshall(data, 0, data.size) parcel.setDataPosition(0) settings.add(KeyValuePair.CREATOR.createFromParcel(parcel)) parcel.recycle() } PublicDatabase.kvPairDao.reset() PublicDatabase.kvPairDao.insert(settings) } } }