アプリデータのエクスポート時にローカルフォルダに保存できるようにする

This commit is contained in:
tateisu 2023-05-04 01:20:11 +09:00
parent 54a7a4df36
commit f33026baf3
5 changed files with 139 additions and 55 deletions

View File

@ -22,6 +22,7 @@ import android.view.inputmethod.EditorInfo
import android.widget.*
import android.widget.TextView.OnEditorActionListener
import androidx.annotation.ColorInt
import androidx.annotation.WorkerThread
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
@ -57,9 +58,12 @@ import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.coroutine.launchProgress
import jp.juggler.util.data.*
import jp.juggler.util.log.LogCategory
import jp.juggler.util.log.dialogOrToast
import jp.juggler.util.log.showToast
import jp.juggler.util.log.withCaption
import jp.juggler.util.ui.*
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.InputStream
import java.io.OutputStreamWriter
@ -92,6 +96,7 @@ class ActAppSetting : AppCompatActivity(), ColorPickerDialogListener, View.OnCli
val reLinefeed = Regex("[\\x0d\\x0a]+")
}
// states
private var customShareTarget: CustomShareTarget? = null
lateinit var handler: Handler
@ -156,6 +161,7 @@ class ActAppSetting : AppCompatActivity(), ColorPickerDialogListener, View.OnCli
arImportAppData.register(this)
arTimelineFont.register(this)
arTimelineFontBold.register(this)
arSaveAppData.register(this)
App1.setActivityTheme(this)
@ -168,8 +174,9 @@ class ActAppSetting : AppCompatActivity(), ColorPickerDialogListener, View.OnCli
if (savedInstanceState != null) {
try {
val sv = savedInstanceState.getString(STATE_CHOOSE_INTENT_TARGET)
customShareTarget = CustomShareTarget.values().firstOrNull { it.name == sv }
savedInstanceState.getString(STATE_CHOOSE_INTENT_TARGET)?.let { target ->
customShareTarget = CustomShareTarget.values().firstOrNull { it.name == target }
}
} catch (ex: Throwable) {
log.e(ex, "can't restore customShareTarget.")
}
@ -231,8 +238,9 @@ class ActAppSetting : AppCompatActivity(), ColorPickerDialogListener, View.OnCli
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
val sv = customShareTarget?.name
if (sv != null) outState.putString(STATE_CHOOSE_INTENT_TARGET, sv)
customShareTarget?.name?.let {
outState.putString(STATE_CHOOSE_INTENT_TARGET, it)
}
}
override fun dispatchKeyEvent(event: KeyEvent) = try {
@ -307,6 +315,7 @@ class ActAppSetting : AppCompatActivity(), ColorPickerDialogListener, View.OnCli
}
return
}
else -> {
val caption = getString(item.caption)
val match = caption.contains(query, ignoreCase = true)
@ -671,8 +680,10 @@ class ActAppSetting : AppCompatActivity(), ColorPickerDialogListener, View.OnCli
val text = when (val pi = item.pref) {
is FloatPref ->
item.fromFloat.invoke(actAppSetting, pi.value)
is StringPref ->
pi.value
else -> error("EditText has incorrect pref $pi")
}
@ -829,54 +840,97 @@ class ActAppSetting : AppCompatActivity(), ColorPickerDialogListener, View.OnCli
///////////////////////////////////////////////////////////////
@Suppress("BlockingMethodInNonBlockingContext")
fun exportAppData() {
fun sendAppData() {
val activity = this
launchProgress(
"export app data",
doInBackground = {
val cacheDir = activity.cacheDir
cacheDir.mkdir()
val file = File(
cacheDir,
"SubwayTooter.${android.os.Process.myPid()}.${android.os.Process.myTid()}.zip"
)
// ZipOutputStreamオブジェクトの作成
ZipOutputStream(FileOutputStream(file)).use { zipStream ->
// アプリデータjson
zipStream.putNextEntry(ZipEntry("AppData.json"))
try {
val jw = JsonWriter(OutputStreamWriter(zipStream, "UTF-8"))
AppDataExporter.encodeAppData(activity, jw)
jw.flush()
} finally {
zipStream.closeEntry()
}
// カラム背景画像
val appState = App1.getAppState(activity)
for (column in appState.columnList) {
AppDataExporter.saveBackgroundImage(activity, zipStream, column)
}
}
file
},
doInBackground = { encodeAppData() },
afterProc = {
val uri = FileProvider.getUriForFile(activity, FILE_PROVIDER_AUTHORITY, it)
val intent = Intent(Intent.ACTION_SEND)
intent.type = contentResolver.getType(uri)
intent.putExtra(Intent.EXTRA_SUBJECT, "SubwayTooter app data")
intent.putExtra(Intent.EXTRA_STREAM, uri)
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION)
arNoop.launch(intent)
try {
val uri =
FileProvider.getUriForFile(activity, FILE_PROVIDER_AUTHORITY, it)
Intent(Intent.ACTION_SEND).apply {
type = contentResolver.getType(uri)
putExtra(Intent.EXTRA_SUBJECT, "SubwayTooter app data")
putExtra(Intent.EXTRA_STREAM, uri)
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION)
}.launch(arNoop)
} catch (ex: Throwable) {
log.e(ex, "exportAppData failed.")
dialogOrToast(ex.withCaption(getString(R.string.missing_app_can_receive_action_send)))
}
}
)
}
fun saveAppData() {
try {
Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "application/zip"
putExtra(Intent.EXTRA_TITLE, "SubwayTooter app data.zip")
}.launch(arSaveAppData)
} catch (ex: Throwable) {
log.e(ex, "can't find app that can handle ACTION_CREATE_DOCUMENT.")
dialogOrToast(ex.withCaption("can't find app that can handle ACTION_CREATE_DOCUMENT."))
}
}
private val arSaveAppData = ActivityResultHandler(log) { r ->
launchAndShowError {
if (r.resultCode != RESULT_OK) return@launchAndShowError
val outUri = r.data?.data ?: error("missing result.data.data")
launchProgress(
"save app data",
doInBackground = {
val tempFile = encodeAppData()
try {
FileInputStream(tempFile).use { inStream ->
(contentResolver.openOutputStream(outUri)
?: error("contentResolver.openOutputStream returns null : $outUri"))
.use { inStream.copyTo(it) }
}
} finally {
tempFile.delete()
}
}
)
}
}
/**
* アプリデータを一時ファイルに保存する
* -
*/
@WorkerThread
private fun encodeAppData(): File {
val activity = this
val cacheDir = externalCacheDir ?: cacheDir ?: error("missing cache directory")
cacheDir.mkdirs()
val name = "SubwayTooter.${android.os.Process.myPid()}.${android.os.Process.myTid()}.zip"
val file = File(cacheDir, name)
ZipOutputStream(FileOutputStream(file)).use { zipStream ->
zipStream.putNextEntry(ZipEntry("AppData.json"))
try {
val jw = JsonWriter(OutputStreamWriter(zipStream, "UTF-8"))
AppDataExporter.encodeAppData(activity, jw)
jw.flush()
} finally {
zipStream.closeEntry()
}
// カラム背景画像
val appState = App1.getAppState(activity)
for (column in appState.columnList) {
AppDataExporter.saveBackgroundImage(activity, zipStream, column)
}
}
return file
}
// open data picker
fun importAppData1() {
try {

View File

@ -9,14 +9,29 @@ import android.widget.Spinner
import android.widget.TextView
import androidx.annotation.StringRes
import androidx.appcompat.widget.AppCompatImageView
import jp.juggler.subwaytooter.*
import jp.juggler.subwaytooter.ActAppSetting
import jp.juggler.subwaytooter.ActDrawableList
import jp.juggler.subwaytooter.ActExitReasons
import jp.juggler.subwaytooter.ActGlideTest
import jp.juggler.subwaytooter.App1
import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.actmain.selectPushDistributor
import jp.juggler.subwaytooter.dialog.runInProgress
import jp.juggler.subwaytooter.drawable.MediaBackgroundDrawable
import jp.juggler.subwaytooter.itemviewholder.AdditionalButtonsPosition
import jp.juggler.subwaytooter.notification.showAlertNotification
import jp.juggler.subwaytooter.pref.*
import jp.juggler.subwaytooter.pref.impl.*
import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.subwaytooter.pref.PrefF
import jp.juggler.subwaytooter.pref.PrefI
import jp.juggler.subwaytooter.pref.PrefL
import jp.juggler.subwaytooter.pref.PrefS
import jp.juggler.subwaytooter.pref.impl.BasePref
import jp.juggler.subwaytooter.pref.impl.BooleanPref
import jp.juggler.subwaytooter.pref.impl.FloatPref
import jp.juggler.subwaytooter.pref.impl.IntPref
import jp.juggler.subwaytooter.pref.impl.LongPref
import jp.juggler.subwaytooter.pref.impl.StringPref
import jp.juggler.subwaytooter.setStatusBarColor
import jp.juggler.subwaytooter.table.daoSavedAccount
import jp.juggler.subwaytooter.table.sortedByNickname
import jp.juggler.subwaytooter.util.CustomShareTarget
@ -1140,12 +1155,18 @@ val appSettingRoot = AppSettingItem(null, SettingType.Section, R.string.app_sett
}
}
action(R.string.app_data_export) {
action = { exportAppData() }
}
action(R.string.app_data_import) {
action = { importAppData1() }
desc = R.string.app_data_import_desc
section(R.string.app_data_export_import) {
group(R.string.app_data_export) {
action(R.string.save_to_local_folder) {
action = { saveAppData() }
}
action(R.string.send_to_other_app) {
action = { sendAppData() }
}
}
action(R.string.app_data_import) {
action = { importAppData1() }
desc = R.string.app_data_import_desc
}
}
}

View File

@ -1274,5 +1274,9 @@
<string name="media_count">%1$d個の添付データ (タップで全て表示)</string>
<string name="applied_when_post">投稿送信時に反映されます</string>
<string name="exceed_reaction_per_account">リアクション個数の制限(%1$d)</string>
<string name="copy_reaction_name">Copy reaction name</string>
<string name="copy_reaction_name">リアクション名をコピー</string>
<string name="send_to_other_app">外部アプリに送信</string>
<string name="save_to_local_folder">ローカルフォルダに保存</string>
<string name="app_data_export_import">アプリデータのエクスポート/インポート</string>
</resources>

View File

@ -1283,4 +1283,7 @@
<string name="applied_when_post">applied when post.</string>
<string name="exceed_reaction_per_account">Reaction limit is %1$d.</string>
<string name="copy_reaction_name">Copy reaction name</string>
<string name="send_to_other_app">Send to other app</string>
<string name="save_to_local_folder">Save to local folder</string>
<string name="app_data_export_import">Export/Import app data</string>
</resources>

View File

@ -368,6 +368,8 @@ class ActivityResultHandler(
}
}
fun Intent.launch(ar: ActivityResultHandler) = ar.launch(this)
val AppCompatActivity.isLiveActivity: Boolean
get() = !(isFinishing || isDestroyed)