diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActAppSetting.kt b/app/src/main/java/jp/juggler/subwaytooter/ActAppSetting.kt index 97d2d536..2e924471 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActAppSetting.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ActAppSetting.kt @@ -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 { diff --git a/app/src/main/java/jp/juggler/subwaytooter/appsetting/AppSettingItem.kt b/app/src/main/java/jp/juggler/subwaytooter/appsetting/AppSettingItem.kt index e9a902e6..48633f54 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/appsetting/AppSettingItem.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/appsetting/AppSettingItem.kt @@ -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 + } } } diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 811c9fd9..7a81290d 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -1274,5 +1274,9 @@ %1$d個の添付データ (タップで全て表示) 投稿送信時に反映されます リアクション個数の制限(%1$d) - Copy reaction name + リアクション名をコピー + 外部アプリに送信 + ローカルフォルダに保存 + アプリデータのエクスポート/インポート + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5e92f2cc..ab7172c2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1283,4 +1283,7 @@ applied when post. Reaction limit is %1$d. Copy reaction name + Send to other app + Save to local folder + Export/Import app data diff --git a/base/src/main/java/jp/juggler/util/ui/UiUtils.kt b/base/src/main/java/jp/juggler/util/ui/UiUtils.kt index 09964e6a..75615ad8 100644 --- a/base/src/main/java/jp/juggler/util/ui/UiUtils.kt +++ b/base/src/main/java/jp/juggler/util/ui/UiUtils.kt @@ -368,6 +368,8 @@ class ActivityResultHandler( } } +fun Intent.launch(ar: ActivityResultHandler) = ar.launch(this) + val AppCompatActivity.isLiveActivity: Boolean get() = !(isFinishing || isDestroyed)