mirror of
https://github.com/tateisu/SubwayTooter
synced 2025-02-01 11:26:48 +01:00
アプリデータのエクスポート時にローカルフォルダに保存できるようにする
This commit is contained in:
parent
54a7a4df36
commit
f33026baf3
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -368,6 +368,8 @@ class ActivityResultHandler(
|
||||
}
|
||||
}
|
||||
|
||||
fun Intent.launch(ar: ActivityResultHandler) = ar.launch(this)
|
||||
|
||||
val AppCompatActivity.isLiveActivity: Boolean
|
||||
get() = !(isFinishing || isDestroyed)
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user