SubwayTooter-Android-App/app/src/main/java/jp/juggler/subwaytooter/ActAppSetting.kt

1235 lines
35 KiB
Kotlin
Raw Normal View History

package jp.juggler.subwaytooter
import android.content.Intent
2020-01-31 08:46:30 +01:00
import android.content.SharedPreferences
import android.content.pm.ResolveInfo
2020-01-31 08:46:30 +01:00
import android.graphics.Color
import android.graphics.Typeface
import android.net.Uri
import android.os.Bundle
2020-01-31 08:46:30 +01:00
import android.os.Handler
import android.text.Editable
import android.text.TextWatcher
import android.util.JsonWriter
import android.view.View
import android.view.ViewGroup
2020-01-31 08:46:30 +01:00
import android.view.Window
import android.widget.*
import androidx.annotation.ColorInt
2019-08-25 10:28:16 +02:00
import androidx.appcompat.app.AlertDialog
2020-09-08 02:50:08 +02:00
import androidx.appcompat.widget.SwitchCompat
2020-01-31 08:46:30 +01:00
import androidx.core.content.ContextCompat
2019-08-25 10:28:16 +02:00
import androidx.core.content.FileProvider
2020-01-31 08:46:30 +01:00
import com.jrummyapps.android.colorpicker.ColorPickerDialog
import com.jrummyapps.android.colorpicker.ColorPickerDialogListener
import jp.juggler.subwaytooter.dialog.DlgAppPicker
import jp.juggler.subwaytooter.notification.PollingWorker
2020-01-31 08:46:30 +01:00
import jp.juggler.subwaytooter.table.AcctColor
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.util.AsyncActivity
import jp.juggler.subwaytooter.util.CustomShare
import jp.juggler.subwaytooter.util.CustomShareTarget
import jp.juggler.subwaytooter.util.cn
2018-12-01 00:02:18 +01:00
import jp.juggler.util.*
2020-01-31 08:46:30 +01:00
import org.apache.commons.io.IOUtils
import java.io.File
import java.io.FileOutputStream
2020-01-31 08:46:30 +01:00
import java.io.InputStream
import java.io.OutputStreamWriter
2020-01-31 08:46:30 +01:00
import java.text.NumberFormat
import java.util.*
import java.util.concurrent.TimeUnit
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
2020-01-31 08:46:30 +01:00
import kotlin.collections.ArrayList
import kotlin.math.abs
class ActAppSetting : AsyncActivity(), ColorPickerDialogListener, View.OnClickListener {
companion object {
internal val log = LogCategory("ActAppSetting")
fun open(activity : ActMain, request_code : Int) {
2018-05-18 22:48:29 +02:00
activity.startActivityForResult(
Intent(activity, ActAppSetting::class.java),
request_code
)
}
2020-01-31 08:46:30 +01:00
private const val COLOR_DIALOG_ID = 1
private const val STATE_CHOOSE_INTENT_TARGET = "customShareTarget"
// 他の設定子画面と重複しない値にすること
const val REQUEST_CODE_OTHER = 0
const val REQUEST_CODE_APP_DATA_IMPORT = 1
const val REQUEST_CODE_TIMELINE_FONT = 2
const val REQUEST_CODE_TIMELINE_FONT_BOLD = 3
2020-01-31 08:46:30 +01:00
val reLinefeed = Regex("[\\x0d\\x0a]+")
}
2020-01-31 08:46:30 +01:00
private var customShareTarget : CustomShareTarget? = null
lateinit var pref : SharedPreferences
lateinit var handler : Handler
private lateinit var lvList : ListView
2020-01-31 08:46:30 +01:00
private lateinit var adapter : MyAdapter
private lateinit var etSearch : EditText
override fun onCreate(savedInstanceState : Bundle?) {
super.onCreate(savedInstanceState)
2020-01-31 08:46:30 +01:00
requestWindowFeature(Window.FEATURE_NO_TITLE)
App1.setActivityTheme(this, noActionBar = true)
this.handler = App1.getAppState(this).handler
2020-09-29 19:44:56 +02:00
this.pref = pref()
2020-01-31 08:46:30 +01:00
// val intent = this.intent
// val layoutId = intent.getIntExtra(EXTRA_LAYOUT_ID, 0)
// val titleId = intent.getIntExtra(EXTRA_TITLE_ID, 0)
// this.title = getString(titleId)
if(savedInstanceState != null) {
try {
val sv = savedInstanceState.getString(STATE_CHOOSE_INTENT_TARGET)
customShareTarget = CustomShareTarget.values().firstOrNull { it.name == sv }
} catch(ex : Throwable) {
log.e(ex, "can't restore customShareTarget.")
}
}
initUi()
removeDefaultPref()
load(null, null)
}
private fun initUi() {
setContentView(R.layout.act_app_setting)
2019-08-31 16:33:13 +02:00
App1.initEdgeToEdge(this)
Styler.fixHorizontalPadding0(findViewById(R.id.llContent))
lvList = findViewById(R.id.lvList)
2020-01-31 08:46:30 +01:00
adapter = MyAdapter()
lvList.adapter = adapter
2020-01-31 08:46:30 +01:00
etSearch = findViewById<EditText>(R.id.etSearch).apply {
addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(p0 : Editable?) {
pendingQuery = p0?.toString()
this@ActAppSetting.handler.removeCallbacks(procQuery)
this@ActAppSetting.handler.postDelayed(procQuery, 166L)
}
override fun beforeTextChanged(
p0 : CharSequence?,
p1 : Int,
p2 : Int,
p3 : Int
) {
}
override fun onTextChanged(
p0 : CharSequence?,
p1 : Int,
p2 : Int,
p3 : Int
) {
}
})
}
2020-01-31 08:46:30 +01:00
findViewById<View>(R.id.btnSearchReset).apply {
setOnClickListener(this@ActAppSetting)
}
}
private fun removeDefaultPref() {
val e = pref.edit()
var changed = false
appSettingRoot.scan {
if(it.pref?.removeDefault(pref, e) == true) changed = true
}
if(changed) e.apply()
2020-01-31 08:46:30 +01:00
}
override fun onSaveInstanceState(outState : Bundle) {
super.onSaveInstanceState(outState)
val sv = customShareTarget?.name
if(sv != null) outState.putString(STATE_CHOOSE_INTENT_TARGET, sv)
}
override fun onStop() {
super.onStop()
2020-01-31 08:46:30 +01:00
// Pull通知チェック間隔を変更したかもしれないのでジョブを再設定する
PollingWorker.onAppSettingStop(this)
}
override fun onActivityResult(requestCode : Int, resultCode : Int, data : Intent?) {
if(resultCode == RESULT_OK && data != null) {
when(requestCode) {
REQUEST_CODE_APP_DATA_IMPORT -> {
data.handleGetContentResult(contentResolver).firstOrNull()?.uri?.let {
importAppData2(false, it)
}
}
REQUEST_CODE_TIMELINE_FONT -> {
handleFontResult(AppSettingItem.TIMELINE_FONT, data, "TimelineFont")
}
REQUEST_CODE_TIMELINE_FONT_BOLD -> {
handleFontResult(AppSettingItem.TIMELINE_FONT_BOLD, data, "TimelineFontBold")
}
}
}
2020-01-31 08:46:30 +01:00
super.onActivityResult(requestCode, resultCode, data)
}
2020-01-31 08:46:30 +01:00
override fun onBackPressed() {
when {
lastQuery != null -> load(lastSection, null)
lastSection != null -> load(null, null)
else -> super.onBackPressed()
}
}
override fun onClick(v : View) {
when(v.id) {
R.id.btnSearchReset -> {
handler.removeCallbacks(procQuery)
etSearch.setText("")
etSearch.hideKeyboard()
load(lastSection, null)
}
}
}
///////////////////////////////////////////////////////////////
2020-01-31 08:46:30 +01:00
private var pendingQuery : String? = null
2020-01-31 08:46:30 +01:00
private val procQuery : Runnable = Runnable {
if(pendingQuery != null) load(null, pendingQuery)
}
///////////////////////////////////////////////////////////////
private val divider = Any()
private val list = ArrayList<Any>()
private var lastSection : AppSettingItem? = null
2020-01-31 08:46:30 +01:00
private var lastQuery : String? = null
private fun load(section : AppSettingItem?, query : String?) {
2020-01-31 08:46:30 +01:00
list.clear()
2020-01-31 08:46:30 +01:00
var lastPath : String? = null
fun addParentPath(item : AppSettingItem) {
list.add(divider)
2020-01-31 08:46:30 +01:00
val pathList = ArrayList<String>()
var parent = item.parent
while(parent != null) {
if(parent.caption != 0) pathList.add(0, getString(parent.caption))
parent = parent.parent
}
val path = pathList.joinToString("/")
if(path != lastPath) {
lastPath = path
list.add(path)
list.add(divider)
}
}
if(query?.isNotEmpty() == true) {
lastQuery = query
fun scanGroup(level : Int, item : AppSettingItem) {
if(item.caption == 0) return
if(item.type != SettingType.Section) {
var match = getString(item.caption).contains(query, ignoreCase = true)
if(item.type == SettingType.Group) {
2020-01-31 08:46:30 +01:00
for(child in item.items) {
if(child.caption == 0) continue
if(getString(item.caption).contains(query, ignoreCase = true)) {
match = true
break
}
}
if(match) {
// put entire group
addParentPath(item)
list.add(item)
for(child in item.items) {
list.add(child)
}
}
return
}
if(match) {
2020-01-31 08:46:30 +01:00
addParentPath(item)
list.add(item)
}
}
for(child in item.items) {
scanGroup(level + 1, child)
2020-01-31 08:46:30 +01:00
}
}
scanGroup(0, appSettingRoot)
if(list.isNotEmpty()) list.add(divider)
} else if(section == null) {
// show root page
val root = appSettingRoot
lastQuery = null
lastSection = null
for(child in root.items) {
list.add(divider)
list.add(child)
}
list.add(divider)
} else {
// show section page
lastSection = section
lastQuery = null
fun scanGroup(level : Int, parent : AppSettingItem?) {
2020-01-31 08:46:30 +01:00
parent ?: return
for(item in parent.items) {
list.add(divider)
list.add(item)
if(item.items.isNotEmpty()) {
2020-01-31 08:46:30 +01:00
if(item.type == SettingType.Group) {
for(child in item.items) {
list.add(child)
}
} else {
scanGroup(level + 1, item)
}
}
}
}
scanGroup(0, section.cast())
if(list.isNotEmpty()) list.add(divider)
}
adapter.notifyDataSetChanged()
lvList.setSelectionFromTop(0, 0)
}
inner class MyAdapter : BaseAdapter() {
override fun getCount() : Int = list.size
override fun getItemId(position : Int) : Long = 0
override fun getItem(position : Int) : Any = list[position]
override fun getViewTypeCount() : Int = SettingType.values().maxByOrNull { it.id } !!.id + 1
2020-01-31 08:46:30 +01:00
override fun getItemViewType(position : Int) : Int =
when(val item = list[position]) {
is AppSettingItem -> item.type.id
is String -> SettingType.Path.id
divider -> SettingType.Divider.id
else -> error("can't generate view for type ${item}")
}
// true if the item at the specified position is not a separator.
// (A separator is a non-selectable, non-clickable item).
override fun areAllItemsEnabled() : Boolean = false
override fun isEnabled(position : Int) : Boolean = list[position] is AppSettingItem
2020-01-31 08:46:30 +01:00
override fun getView(position : Int, convertView : View?, parent : ViewGroup?) : View =
when(val item = list[position]) {
is AppSettingItem ->
getViewSettingItem(item, convertView, parent)
is String -> getViewPath(item, convertView)
divider -> getViewDivider(convertView)
else -> error("can't generate view for type ${item}")
}
}
private fun dip(dp : Float) : Int =
(resources.displayMetrics.density * dp + 0.5f).toInt()
private fun dip(dp : Int) : Int = dip(dp.toFloat())
private fun getViewDivider(convertView : View?) : View =
convertView ?: FrameLayout(this@ActAppSetting).apply {
layoutParams = AbsListView.LayoutParams(
AbsListView.LayoutParams.MATCH_PARENT,
AbsListView.LayoutParams.WRAP_CONTENT
)
addView(View(this@ActAppSetting).apply {
layoutParams = FrameLayout.LayoutParams(
AbsListView.LayoutParams.MATCH_PARENT,
dip(1)
).apply {
val margin_lr = 0
val margin_tb = dip(6)
setMargins(margin_lr, margin_tb, margin_lr, margin_tb)
}
setBackgroundColor(context.attrColor(R.attr.colorSettingDivider))
2020-01-31 08:46:30 +01:00
})
}
private fun getViewPath(path : String, convertView : View?) : View {
val tv : TextView = convertView.cast() ?: TextView(this@ActAppSetting).apply {
layoutParams = AbsListView.LayoutParams(
AbsListView.LayoutParams.MATCH_PARENT,
AbsListView.LayoutParams.WRAP_CONTENT
)
val pad_lr = 0
val pad_tb = dip(3)
setTypeface(typeface, Typeface.BOLD)
setPaddingRelative(pad_lr, pad_tb, pad_lr, pad_tb)
}
tv.text = path
return tv
}
private fun getViewSettingItem(
item : AppSettingItem,
convertView : View?,
parent : ViewGroup?
) : View {
val view : View
val holder : ViewHolderSettingItem
if(convertView != null) {
view = convertView
holder = convertView.tag.cast() !!
} else {
view = layoutInflater.inflate(R.layout.lv_setting_item, parent, false)
holder = ViewHolderSettingItem(view)
view.tag = holder
}
holder.bind(item)
return view
}
private var colorTarget : AppSettingItem? = null
override fun onDialogDismissed(dialogId : Int) {
}
override fun onColorSelected(dialogId : Int, @ColorInt colorSelected : Int) {
val colorTarget = this.colorTarget ?: return
val ip : IntPref = colorTarget.pref.cast() ?: error("$colorTarget has no in pref")
val c = when(colorTarget.type) {
SettingType.ColorAlpha -> colorSelected.notZero() ?: 0x01000000
else -> colorSelected or Color.BLACK
}
pref.edit().put(ip, c).apply()
findItemViewHolder(colorTarget)?.showColor()
colorTarget.changed(this)
}
inner class ViewHolderSettingItem(viewRoot : View) :
TextWatcher,
AdapterView.OnItemSelectedListener,
CompoundButton.OnCheckedChangeListener {
private val tvCaption : TextView = viewRoot.findViewById(R.id.tvCaption)
private val btnAction : Button = viewRoot.findViewById(R.id.btnAction)
private val checkBox : CheckBox = viewRoot.findViewById<CheckBox>(R.id.checkBox)
.also { it.setOnCheckedChangeListener(this) }
2020-09-08 02:50:08 +02:00
private val swSwitch : SwitchCompat = viewRoot.findViewById<SwitchCompat>(R.id.swSwitch)
2020-01-31 08:46:30 +01:00
.also { it.setOnCheckedChangeListener(this) }
val llExtra : LinearLayout = viewRoot.findViewById(R.id.llExtra)
val textView1 : TextView = viewRoot.findViewById(R.id.textView1)
private val llButtonBar : LinearLayout = viewRoot.findViewById(R.id.llButtonBar)
private val vColor : View = viewRoot.findViewById(R.id.vColor)
private val btnEdit : Button = viewRoot.findViewById(R.id.btnEdit)
private val btnReset : Button = viewRoot.findViewById(R.id.btnReset)
private val spSpinner : Spinner = viewRoot.findViewById<Spinner>(R.id.spSpinner)
.also { it.onItemSelectedListener = this }
private val etEditText : EditText = viewRoot.findViewById<EditText>(R.id.etEditText)
.also { it.addTextChangedListener(this) }
private val tvDesc : TextView = viewRoot.findViewById(R.id.tvDesc)
private val tvError : TextView = viewRoot.findViewById(R.id.tvError)
val activity : ActAppSetting
get() = this@ActAppSetting
var item : AppSettingItem? = null
private var bindingBusy = false
fun bind(item : AppSettingItem) {
bindingBusy = true
try {
this.item = item
tvCaption.vg(false)
btnAction.vg(false)
checkBox.vg(false)
swSwitch.vg(false)
llExtra.vg(false)
textView1.vg(false)
llButtonBar.vg(false)
vColor.vg(false)
spSpinner.vg(false)
etEditText.vg(false)
tvDesc.vg(false)
tvError.vg(false)
val name = if(item.caption == 0) "" else getString(item.caption)
if(item.desc != 0) {
tvDesc.vg(true)
tvDesc.text = getString(item.desc)
if(item.descClickSet) {
tvDesc.background = ContextCompat.getDrawable(
activity,
R.drawable.btn_bg_transparent_round6dp
)
tvDesc.setOnClickListener { item.descClick.invoke(activity) }
} else {
tvDesc.background = null
tvDesc.setOnClickListener(null)
tvDesc.isClickable = false
}
}
when(item.type) {
SettingType.Section -> {
btnAction.vg(true)
btnAction.text = name
btnAction.isEnabled = item.enabled
btnAction.setOnClickListener {
load(item.cast() !!, null)
}
}
SettingType.Action -> {
btnAction.vg(true)
btnAction.text = name
btnAction.isEnabled = item.enabled
btnAction.setOnClickListener {
item.action(activity)
}
}
SettingType.CheckBox -> {
val bp : BooleanPref =
item.pref.cast() ?: error("$name has no boolean pref")
2020-01-31 08:46:30 +01:00
checkBox.vg(false) // skip animation
checkBox.text = name
checkBox.isEnabled = item.enabled
checkBox.isChecked = bp(pref)
checkBox.vg(true)
}
SettingType.Switch -> {
val bp : BooleanPref =
item.pref.cast() ?: error("$name has no boolean pref")
2020-01-31 08:46:30 +01:00
showCaption(name)
swSwitch.vg(false) // skip animation
2020-09-29 19:44:56 +02:00
setSwitchColor(pref, swSwitch)
2020-01-31 08:46:30 +01:00
swSwitch.isEnabled = item.enabled
swSwitch.isChecked = bp(pref)
swSwitch.vg(true)
}
SettingType.Group -> {
showCaption(name)
}
SettingType.Sample -> {
llExtra.vg(true)
llExtra.removeAllViews()
layoutInflater.inflate(item.sampleLayoutId, llExtra, true)
item.sampleUpdate(activity, llExtra)
}
SettingType.ColorAlpha, SettingType.ColorOpaque -> {
val ip = item.pref.cast<IntPref>() ?: error("$name has no int pref")
showCaption(name)
llButtonBar.vg(true)
vColor.vg(true)
vColor.setBackgroundColor(ip(pref))
btnEdit.isEnabled = item.enabled
btnReset.isEnabled = item.enabled
btnEdit.setOnClickListener {
colorTarget = item
val color = ip(pref)
val builder = ColorPickerDialog.newBuilder()
.setDialogType(ColorPickerDialog.TYPE_CUSTOM)
.setAllowPresets(true)
.setShowAlphaSlider(item.type == SettingType.ColorAlpha)
.setDialogId(COLOR_DIALOG_ID)
if(color != 0) builder.setColor(color)
builder.show(activity)
}
btnReset.setOnClickListener {
pref.edit().remove(ip).apply()
showColor()
2020-01-31 08:46:30 +01:00
item.changed.invoke(activity)
}
}
SettingType.Spinner -> {
showCaption(name)
spSpinner.vg(true)
spSpinner.isEnabled = item.enabled
val pi = item.pref
if(pi is IntPref) {
// 整数型の設定のSpinnerは全て選択肢を単純に覚える
val argsInt = item.spinnerArgs
if(argsInt != null) {
initSpinner(spSpinner, argsInt.map { getString(it) })
} else {
initSpinner(spSpinner, item.spinnerArgsProc(activity))
}
spSpinner.setSelection(pi.invoke(pref))
} else {
item.spinnerInitializer.invoke(activity, spSpinner)
}
}
SettingType.EditText -> {
showCaption(name)
etEditText.vg(true)
?: error("EditText must have preference.")
etEditText.inputType = item.inputType
val text = when(val pi = item.pref) {
is FloatPref -> {
item.fromFloat.invoke(activity, pi(pref))
}
is StringPref -> {
pi(pref)
}
else -> error("EditText han incorrect pref $pi")
}
etEditText.setText(text)
etEditText.setSelection(0, text.length)
item.hint?.let { etEditText.hint = it }
updateErrorView()
}
SettingType.TextWithSelector -> {
showCaption(name)
llButtonBar.vg(true)
vColor.vg(false)
textView1.vg(true)
item.showTextView.invoke(activity, textView1)
btnEdit.setOnClickListener {
item.onClickEdit.invoke(activity)
}
btnReset.setOnClickListener {
item.onClickReset.invoke(activity)
}
}
else -> error("unknown type ${item.type}")
}
} finally {
bindingBusy = false
}
}
private fun showCaption(caption : String) {
if(caption.isNotEmpty()) {
tvCaption.vg(true)
tvCaption.text = caption
updateCaption()
}
}
fun updateCaption() {
val item = item ?: return
val key = item.pref?.key ?: return
2020-01-31 08:46:30 +01:00
val sample : TextView = tvCaption
var defaultExtra = defaultLineSpacingExtra[key]
if(defaultExtra == null) {
defaultExtra = sample.lineSpacingExtra
defaultLineSpacingExtra[key] = defaultExtra
}
var defaultMultiplier = defaultLineSpacingMultiplier[key]
if(defaultMultiplier == null) {
defaultMultiplier = sample.lineSpacingMultiplier
defaultLineSpacingMultiplier[key] = defaultMultiplier
}
2019-08-25 10:28:16 +02:00
2020-01-31 08:46:30 +01:00
val size = item.captionFontSize.invoke(activity)
if(size != null) sample.textSize = size
2019-08-25 10:28:16 +02:00
2020-01-31 08:46:30 +01:00
val spacing = item.captionSpacing.invoke(activity)
if(spacing == null || ! spacing.isFinite()) {
sample.setLineSpacing(defaultExtra, defaultMultiplier)
} else {
sample.setLineSpacing(0f, spacing)
}
2019-08-25 10:28:16 +02:00
2020-01-31 08:46:30 +01:00
}
private fun updateErrorView() {
val item = item ?: return
val sv = etEditText.text.toString()
val error = item.getError.invoke(activity, sv)
tvError.vg(error != null)?.text = error
}
fun showColor() {
val item = item ?: return
val ip = item.pref.cast<IntPref>() ?: return
val c = ip(pref)
vColor.setBackgroundColor(c)
}
override fun beforeTextChanged(p0 : CharSequence?, p1 : Int, p2 : Int, p3 : Int) {
}
override fun onTextChanged(p0 : CharSequence?, p1 : Int, p2 : Int, p3 : Int) {
}
override fun afterTextChanged(p0 : Editable?) {
if(bindingBusy) return
val item = item ?: return
2019-08-25 10:28:16 +02:00
2020-01-31 08:46:30 +01:00
val sv = item.filter.invoke(p0?.toString() ?: "")
2020-01-31 08:46:30 +01:00
when(val pi = item.pref) {
is StringPref -> {
pref.edit().put(pi, sv).apply()
}
is FloatPref -> {
val fv = item.toFloat.invoke(activity, sv)
if(fv.isFinite()) {
pref.edit().put(pi, fv).apply()
} else {
pref.edit().remove(pi.key).apply()
}
}
else -> {
error("not FloatPref or StringPref")
}
}
2020-01-31 08:46:30 +01:00
item.changed.invoke(activity)
updateErrorView()
}
2020-01-31 08:46:30 +01:00
override fun onNothingSelected(v : AdapterView<*>?) = Unit
2020-01-31 08:46:30 +01:00
override fun onItemSelected(
parent : AdapterView<*>?,
view : View?,
position : Int,
id : Long
) {
2020-01-31 08:46:30 +01:00
if(bindingBusy) return
val item = item ?: return
when(val pi = item.pref) {
is IntPref -> pref.edit().put(pi, spSpinner.selectedItemPosition).apply()
else -> item.spinnerOnSelected.invoke(activity, spSpinner, position)
}
item.changed.invoke(activity)
}
2020-01-31 08:46:30 +01:00
override fun onCheckedChanged(v : CompoundButton?, isChecked : Boolean) {
if(bindingBusy) return
val item = item ?: return
when(val pi = item.pref) {
is BooleanPref -> pref.edit().put(pi, isChecked).apply()
2020-01-31 08:46:30 +01:00
else -> error("CompoundButton has no booleanPref $pi")
}
2020-01-31 08:46:30 +01:00
item.changed.invoke(activity)
}
}
2020-01-31 08:46:30 +01:00
private fun initSpinner(spinner : Spinner, captions : List<String>) {
spinner.adapter = ArrayAdapter(
this,
android.R.layout.simple_spinner_item,
captions.toTypedArray()
).apply {
setDropDownViewResource(R.layout.lv_spinner_dropdown)
}
}
///////////////////////////////////////////////////////////////
2020-09-08 02:50:08 +02:00
@Suppress("BlockingMethodInNonBlockingContext")
2020-01-31 08:46:30 +01:00
fun exportAppData() {
runWithProgress(
"export app data",
{
val cache_dir = cacheDir
cache_dir.mkdir()
val file = File(
cache_dir,
"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(this@ActAppSetting, jw)
jw.flush()
} finally {
zipStream.closeEntry()
}
// カラム背景画像
val appState = App1.getAppState(this@ActAppSetting)
2020-12-21 03:13:03 +01:00
for(column in appState.columnList) {
AppDataExporter.saveBackgroundImage(
this@ActAppSetting,
zipStream,
column
)
}
}
file
},
{
val uri = FileProvider.getUriForFile(
this@ActAppSetting,
App1.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)
startActivityForResult(intent, REQUEST_CODE_OTHER)
}
)
}
2020-01-31 08:46:30 +01:00
// open data picker
fun importAppData1() {
try {
val intent = intentOpenDocument("*/*")
startActivityForResult(intent, REQUEST_CODE_APP_DATA_IMPORT)
} catch(ex : Throwable) {
2020-09-29 19:44:56 +02:00
showToast(ex, "importAppData(1) failed.")
}
}
2020-01-31 08:46:30 +01:00
// after data picked
private fun importAppData2(bConfirm : Boolean, uri : Uri) {
val type = contentResolver.getType(uri)
log.d("importAppData type=%s", type)
if(! bConfirm) {
AlertDialog.Builder(this)
.setMessage(getString(R.string.app_data_import_confirm))
.setNegativeButton(R.string.cancel, null)
2020-01-31 08:46:30 +01:00
.setPositiveButton(R.string.ok) { _, _ -> importAppData2(true, uri) }
.show()
return
}
val data = Intent()
data.data = uri
setResult(ActMain.RESULT_APP_DATA_IMPORT, data)
finish()
2020-01-31 08:46:30 +01:00
}
fun findItemViewHolder(item : AppSettingItem?) : ViewHolderSettingItem? {
if(item != null) {
for(i in 0 until lvList.childCount) {
val view = lvList.getChildAt(i)
val holder : ViewHolderSettingItem? = view?.tag?.cast()
if(holder?.item == item) return holder
}
}
return null
}
fun showSample(item : AppSettingItem?) {
item ?: error("showSample: missing item…")
findItemViewHolder(item)?.let {
item.sampleUpdate.invoke(this, it.llExtra)
}
}
2020-09-27 12:59:54 +02:00
fun setSwitchColor() =
2020-09-29 19:44:56 +02:00
setSwitchColor(pref, lvList)
2020-01-31 08:46:30 +01:00
//////////////////////////////////////////////////////
fun formatFontSize(fv : Float) : String =
when {
2020-02-01 19:28:16 +01:00
fv.isFinite() -> String.format(defaultLocale(this), "%.1f", fv)
2020-01-31 08:46:30 +01:00
else -> ""
}
fun parseFontSize(src : String) : Float {
try {
if(src.isNotEmpty()) {
2020-02-01 19:28:16 +01:00
val f = NumberFormat.getInstance(defaultLocale(this)).parse(src)?.toFloat()
2020-01-31 08:46:30 +01:00
return when {
f == null -> Float.NaN
f.isNaN() -> Float.NaN
f < 0f -> 0f
f > 999f -> 999f
else -> f
}
}
} catch(ex : Throwable) {
log.trace(ex)
}
2020-01-31 08:46:30 +01:00
return Float.NaN
}
2020-01-31 08:46:30 +01:00
private val defaultLineSpacingExtra = HashMap<String, Float>()
private val defaultLineSpacingMultiplier = HashMap<String, Float>()
private fun handleFontResult(item : AppSettingItem?, data : Intent, file_name : String) {
item ?: error("handleFontResult : setting item is null")
data.handleGetContentResult(contentResolver).firstOrNull()?.uri?.let {
val file = saveTimelineFont(it, file_name)
if(file != null) {
pref.edit().put(item.pref.cast() !!, file.absolutePath).apply()
showTimelineFont(item)
}
}
}
fun showTimelineFont(item : AppSettingItem?) {
item ?: return
val holder = findItemViewHolder(item) ?: return
item.showTextView.invoke(this, holder.textView1)
}
fun showTimelineFont(item : AppSettingItem, tv : TextView) {
val font_url = item.pref.cast<StringPref>() !!.invoke(this)
try {
if(font_url.isNotEmpty()) {
tv.typeface = Typeface.DEFAULT
val face = Typeface.createFromFile(font_url)
tv.typeface = face
tv.text = font_url
return
}
} catch(ex : Throwable) {
log.trace(ex)
}
// fallback
tv.text = getString(R.string.not_selected)
tv.typeface = Typeface.DEFAULT
}
private fun saveTimelineFont(uri : Uri?, file_name : String) : File? {
try {
if(uri == null) {
2020-09-29 19:44:56 +02:00
showToast(false, "missing uri.")
2020-01-31 08:46:30 +01:00
return null
}
contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
val dir = filesDir
dir.mkdir()
val tmp_file = File(dir, "$file_name.tmp")
val source : InputStream? = contentResolver.openInputStream(uri)
if(source == null) {
2020-09-29 19:44:56 +02:00
showToast(false, "openInputStream returns null. uri=%s", uri)
2020-01-31 08:46:30 +01:00
return null
} else {
source.use { inStream ->
FileOutputStream(tmp_file).use { outStream ->
IOUtils.copy(inStream, outStream)
}
}
}
val face = Typeface.createFromFile(tmp_file)
if(face == null) {
2020-09-29 19:44:56 +02:00
showToast(false, "Typeface.createFromFile() failed.")
2020-01-31 08:46:30 +01:00
return null
}
val file = File(dir, file_name)
if(! tmp_file.renameTo(file)) {
2020-09-29 19:44:56 +02:00
showToast(false, "File operation failed.")
2020-01-31 08:46:30 +01:00
return null
}
return file
} catch(ex : Throwable) {
log.trace(ex)
2020-09-29 19:44:56 +02:00
showToast(ex, "saveTimelineFont failed.")
2020-01-31 08:46:30 +01:00
return null
}
}
//////////////////////////////////////////////////////
inner class AccountAdapter internal constructor() : BaseAdapter() {
internal val list = java.util.ArrayList<SavedAccount>()
init {
for(a in SavedAccount.loadAccountList(this@ActAppSetting)) {
if(a.isPseudo) continue
list.add(a)
}
SavedAccount.sort(list)
}
override fun getCount() : Int {
return 1 + list.size
}
override fun getItem(position : Int) : Any? {
return if(position == 0) null else list[position - 1]
}
override fun getItemId(position : Int) : Long {
return 0
}
override fun getView(position : Int, viewOld : View?, parent : ViewGroup) : View {
val view = viewOld ?: layoutInflater.inflate(
android.R.layout.simple_spinner_item,
parent,
false
)
view.findViewById<TextView>(android.R.id.text1).text =
if(position == 0)
getString(R.string.ask_always)
else
2020-02-01 14:26:57 +01:00
AcctColor.getNickname(list[position - 1])
2020-01-31 08:46:30 +01:00
return view
}
override fun getDropDownView(position : Int, viewOld : View?, parent : ViewGroup) : View {
val view =
viewOld ?: layoutInflater.inflate(R.layout.lv_spinner_dropdown, parent, false)
view.findViewById<TextView>(android.R.id.text1).text =
if(position == 0)
getString(R.string.ask_always)
else
2020-02-01 14:26:57 +01:00
AcctColor.getNickname(list[position - 1])
2020-01-31 08:46:30 +01:00
return view
}
internal fun getIndexFromId(db_id : Long) : Int {
var i = 0
val ie = list.size
while(i < ie) {
if(list[i].db_id == db_id) return i + 1
++ i
}
return 0
}
internal fun getIdFromIndex(position : Int) : Long {
return if(position > 0) list[position - 1].db_id else - 1L
}
}
private class Item(
val id : String,
val caption : String,
val offset : Int
)
inner class TimeZoneAdapter internal constructor() : BaseAdapter() {
private val list = ArrayList<Item>()
init {
for(id in TimeZone.getAvailableIDs()) {
val tz = TimeZone.getTimeZone(id)
// GMT数字を指定するタイプのタイムゾーンは無視する。ただしGMT-12:00の項目だけは残す
// 3文字のIDは曖昧な場合があるので非推奨
// '/' を含まないIDは列挙しない
if(! when {
! tz.id.contains('/') -> false
tz.id == "Etc/GMT+12" -> true
tz.id.startsWith("Etc/") -> false
else -> true
}) continue
var offset = tz.rawOffset.toLong()
val caption = when(offset) {
0L -> String.format("(UTC\u00B100:00) %s %s", tz.id, tz.displayName)
else -> {
val format = if(offset > 0)
"(UTC+%02d:%02d) %s %s"
else
"(UTC-%02d:%02d) %s %s"
offset = abs(offset)
val hours = TimeUnit.MILLISECONDS.toHours(offset)
val minutes =
TimeUnit.MILLISECONDS.toMinutes(offset) - TimeUnit.HOURS.toMinutes(hours)
String.format(format, hours, minutes, tz.id, tz.displayName)
}
}
if(null == list.find { it.caption == caption }) {
list.add(Item(id, caption, tz.rawOffset))
}
}
2020-09-08 02:50:08 +02:00
list.sortWith { a, b ->
2020-01-31 08:46:30 +01:00
(a.offset - b.offset).notZero() ?: a.caption.compareTo(b.caption)
2020-09-08 02:50:08 +02:00
}
2020-01-31 08:46:30 +01:00
list.add(0, Item("", getString(R.string.device_timezone), 0))
}
override fun getCount() : Int {
return list.size
}
override fun getItem(position : Int) : Any {
2020-01-31 08:46:30 +01:00
return list[position]
}
override fun getItemId(position : Int) : Long {
return 0
}
override fun getView(position : Int, viewOld : View?, parent : ViewGroup) : View {
val view = viewOld ?: layoutInflater.inflate(
android.R.layout.simple_spinner_item,
parent,
false
)
val item = list[position]
view.findViewById<TextView>(android.R.id.text1).text = item.caption
return view
}
override fun getDropDownView(position : Int, viewOld : View?, parent : ViewGroup) : View {
val view =
viewOld ?: layoutInflater.inflate(R.layout.lv_spinner_dropdown, parent, false)
val item = list[position]
view.findViewById<TextView>(android.R.id.text1).text = item.caption
return view
}
internal fun getIndexFromId(tz_id : String) : Int {
val index = list.indexOfFirst { it.id == tz_id }
return if(index == - 1) 0 else index
}
internal fun getIdFromIndex(position : Int) : String {
return list[position].id
}
}
fun openCustomShareChooser(appSettingItem: AppSettingItem,target : CustomShareTarget) {
try {
val rv = DlgAppPicker(
this,
intent = Intent().apply {
action = Intent.ACTION_SEND
type = "text/plain"
putExtra(Intent.EXTRA_TEXT, getString(R.string.content_sample))
},
addCopyAction = true
) { setCustomShare(appSettingItem,target, it) }
.show()
2020-09-29 19:44:56 +02:00
if(! rv) showToast(true, "share target app is not installed.")
2020-01-31 08:46:30 +01:00
} catch(ex : Throwable) {
log.trace(ex)
2020-09-29 19:44:56 +02:00
showToast(ex, "openCustomShareChooser failed.")
2020-01-31 08:46:30 +01:00
}
}
fun setCustomShare(appSettingItem: AppSettingItem,target : CustomShareTarget, value : String) {
val sp : StringPref = appSettingItem.pref.cast() ?: error("$target: not StringPref")
2020-01-31 08:46:30 +01:00
pref.edit().put(sp, value).apply()
showCustomShareIcon(findItemViewHolder(appSettingItem)?.textView1, target)
2020-01-31 08:46:30 +01:00
}
fun showCustomShareIcon(tv : TextView?, target : CustomShareTarget) {
tv ?: return
val cn = CustomShare.getCustomShareComponentName(pref, target)
val (label, icon) = CustomShare.getInfo(this, cn)
2020-01-31 08:46:30 +01:00
tv.text = label ?: getString(R.string.not_selected)
tv.setCompoundDrawablesRelativeWithIntrinsicBounds(icon, null, null, null)
tv.compoundDrawablePadding = (resources.displayMetrics.density * 4f + 0.5f).toInt()
2020-01-31 08:46:30 +01:00
}
fun openWebBrowserChooser(appSettingItem: AppSettingItem, intent:Intent, filter: (ResolveInfo) -> Boolean ) {
try {
val rv = DlgAppPicker(
this,
intent=intent,
filter = filter,
addCopyAction = false
) { setWebBrowser( appSettingItem,it) }
.show()
if(! rv) showToast(true, "share target app is not installed.")
} catch(ex : Throwable) {
log.trace(ex)
showToast(ex, "openCustomShareChooser failed.")
}
}
fun setWebBrowser(appSettingItem: AppSettingItem, value : String) {
val sp : StringPref = appSettingItem.pref.cast() ?: error("${getString(appSettingItem.caption)}: not StringPref")
pref.edit().put(sp, value).apply()
showWebBrowser(findItemViewHolder(appSettingItem)?.textView1, value )
}
fun showWebBrowser(tv : TextView?,prefValue:String) {
tv ?: return
val cn =prefValue.cn()
val (label, icon) = CustomShare.getInfo(this, cn)
tv.text = label ?: getString(R.string.not_selected)
tv.setCompoundDrawablesRelativeWithIntrinsicBounds(icon, null, null, null)
tv.compoundDrawablePadding = (resources.displayMetrics.density * 4f + 0.5f).toInt()
}
2020-01-31 08:46:30 +01:00
}