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

548 lines
16 KiB
Kotlin

package jp.juggler.subwaytooter
import android.annotation.SuppressLint
import android.content.Intent
import android.net.Uri
import android.os.AsyncTask
import android.os.Bundle
import android.os.PersistableBundle
import android.os.Process
import android.text.Editable
import android.text.TextWatcher
import android.view.View
import android.view.ViewGroup
import android.widget.*
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.FileProvider
import jp.juggler.subwaytooter.api.entity.TootStatus
import jp.juggler.subwaytooter.dialog.ActionsDialog
import jp.juggler.subwaytooter.dialog.ProgressDialogEx
import jp.juggler.util.*
import org.apache.commons.io.IOUtils
import org.jetbrains.anko.textColor
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.FileOutputStream
import java.util.*
import kotlin.collections.ArrayList
class ActLanguageFilter : AppCompatActivity(), View.OnClickListener {
private class MyItem(
val code : String,
var allow : Boolean
)
companion object {
internal val log = LogCategory("ActLanguageFilter")
internal const val EXTRA_COLUMN_INDEX = "column_index"
private const val STATE_LANGUAGE_LIST = "language_list"
fun open(activity : ActMain, idx : Int, request_code : Int) {
val intent = Intent(activity, ActLanguageFilter::class.java)
intent.putExtra(EXTRA_COLUMN_INDEX, idx)
activity.startActivityForResult(intent, request_code)
}
// 他の設定子画面と重複しない値にすること
private const val REQUEST_CODE_OTHER = 0
private const val REQUEST_CODE_IMPORT = 1
private val languageComparator = Comparator<MyItem> { a, b ->
when {
a.code == TootStatus.LANGUAGE_CODE_DEFAULT -> - 1
b.code == TootStatus.LANGUAGE_CODE_DEFAULT -> 1
a.code == TootStatus.LANGUAGE_CODE_UNKNOWN -> - 1
b.code == TootStatus.LANGUAGE_CODE_UNKNOWN -> 1
else -> a.code.compareTo(b.code)
}
}
private fun equalsLanguageList(a : JsonObject?, b : JsonObject?) : Boolean {
fun JsonObject.encodeToString() : String {
val clone = this.toString().decodeJsonObject()
if(! clone.contains(TootStatus.LANGUAGE_CODE_DEFAULT)) {
clone[TootStatus.LANGUAGE_CODE_DEFAULT] = true
}
return clone.keys.sorted().joinToString(",") { "$it=${this[it]}" }
}
val a_sign = (a ?: JsonObject()).encodeToString()
val b_sign = (b ?: JsonObject()).encodeToString()
return a_sign == b_sign
}
}
private val languageNameMap by lazy {
HashMap<String, String>().apply {
// from https://github.com/google/cld3/blob/master/src/task_context_params.cc#L43
val languageNamesCld3 = arrayOf(
"eo", "co", "eu", "ta", "de", "mt", "ps", "te", "su", "uz", "zh-Latn", "ne",
"nl", "sw", "sq", "hmn", "ja", "no", "mn", "so", "ko", "kk", "sl", "ig",
"mr", "th", "zu", "ml", "hr", "bs", "lo", "sd", "cy", "hy", "uk", "pt",
"lv", "iw", "cs", "vi", "jv", "be", "km", "mk", "tr", "fy", "am", "zh",
"da", "sv", "fi", "ht", "af", "la", "id", "fil", "sm", "ca", "el", "ka",
"sr", "it", "sk", "ru", "ru-Latn", "bg", "ny", "fa", "haw", "gl", "et",
"ms", "gd", "bg-Latn", "ha", "is", "ur", "mi", "hi", "bn", "hi-Latn", "fr",
"yi", "hu", "xh", "my", "tg", "ro", "ar", "lb", "el-Latn", "st", "ceb",
"kn", "az", "si", "ky", "mg", "en", "gu", "es", "pl", "ja-Latn", "ga", "lt",
"sn", "yo", "pa", "ku"
)
for(src1 in languageNamesCld3) {
val src2 = src1.replace("-Latn", "")
val isLatn = src2 != src1
val locale = Locale(src2)
log.w("languageNameMap $src1 ${locale.language} ${locale.country} ${locale.displayName}")
put(
src1, if(isLatn) {
"${locale.displayName}(Latn)"
} else {
locale.displayName
}
)
}
put(TootStatus.LANGUAGE_CODE_DEFAULT, getString(R.string.language_code_default))
put(TootStatus.LANGUAGE_CODE_UNKNOWN, getString(R.string.language_code_unknown))
}
}
private fun getDesc(item : MyItem) : String {
val code = item.code
return languageNameMap[code] ?: getString(R.string.custom)
}
private var column_index : Int = 0
internal lateinit var column : Column
internal lateinit var app_state : AppState
internal var density : Float = 0f
private lateinit var listView : ListView
private lateinit var adapter : MyAdapter
private val languageList = ArrayList<MyItem>()
private var loading_busy : Boolean = false
override fun onCreate(savedInstanceState : Bundle?) {
super.onCreate(savedInstanceState)
App1.setActivityTheme(this)
initUI()
app_state = App1.getAppState(this)
density = app_state.density
column_index = intent.getIntExtra(EXTRA_COLUMN_INDEX, 0)
column = app_state.column_list[column_index]
if(savedInstanceState != null) {
try {
val sv = savedInstanceState.getString(STATE_LANGUAGE_LIST, null)
if(sv != null) {
val list = sv.decodeJsonObject()
load(list)
return
}
} catch(ex : Throwable) {
log.trace(ex)
}
}
load(column.language_filter)
}
override fun onSaveInstanceState(outState : Bundle, outPersistentState : PersistableBundle) {
super.onSaveInstanceState(outState, outPersistentState)
outState.putString(STATE_LANGUAGE_LIST, encodeLanguageList().toString())
}
override fun onBackPressed() {
if(! equalsLanguageList(column.language_filter, encodeLanguageList())) {
AlertDialog.Builder(this)
.setMessage(R.string.language_filter_quit_waring)
.setPositiveButton(R.string.ok) { _, _ -> finish() }
.setNegativeButton(R.string.cancel, null)
.show()
return
}
super.onBackPressed()
}
private fun initUI() {
setContentView(R.layout.act_language_filter)
App1.initEdgeToEdge(this)
Styler.fixHorizontalPadding(findViewById(R.id.llContent))
for(id in intArrayOf(
R.id.btnAdd,
R.id.btnSave,
R.id.btnMore
)) {
findViewById<View>(id)?.setOnClickListener(this)
}
listView = findViewById(R.id.listView)
adapter = MyAdapter()
listView.adapter = adapter
listView.onItemClickListener = adapter
}
// UIのデータをJsonObjectにエンコード
private fun encodeLanguageList() = jsonObject {
for(item in languageList) {
put(item.code, item.allow)
}
}
private fun load(src : JsonObject?) {
loading_busy = true
try {
languageList.clear()
if(src != null) {
for(key in src.keys) {
languageList.add(MyItem(key, src.boolean(key) ?: true))
}
}
if(null == languageList.find { it.code == TootStatus.LANGUAGE_CODE_DEFAULT }) {
languageList.add(MyItem(TootStatus.LANGUAGE_CODE_DEFAULT, true))
}
languageList.sortWith(languageComparator)
adapter.notifyDataSetChanged()
} finally {
loading_busy = false
}
}
private fun save() {
column.language_filter = encodeLanguageList()
}
private inner class MyAdapter : BaseAdapter(), AdapterView.OnItemClickListener {
override fun getCount() : Int = languageList.size
override fun getItemId(idx : Int) : Long = 0L
override fun getItem(idx : Int) : Any = languageList[idx]
override fun getView(idx : Int, viewArg : View?, parent : ViewGroup?) : View {
val tv = (viewArg ?: layoutInflater.inflate(
R.layout.lv_language_filter,
parent,
false
)) as TextView
val item = languageList[idx]
tv.text = String.format(
"%s %s : %s",
item.code,
getDesc(item),
getString(if(item.allow) R.string.language_show else R.string.language_hide)
)
tv.textColor = getAttributeColor(
this@ActLanguageFilter, when(item.allow) {
true -> R.attr.colorContentText
false -> R.attr.colorRegexFilterError
}
)
return tv
}
override fun onItemClick(parent : AdapterView<*>?, viewArg : View?, idx : Int, id : Long) {
if(idx in languageList.indices) edit(languageList[idx])
}
}
override fun onClick(v : View) {
when(v.id) {
R.id.btnSave -> {
save()
val data = Intent()
data.putExtra(EXTRA_COLUMN_INDEX, column_index)
setResult(RESULT_OK, data)
finish()
}
R.id.btnAdd -> edit(null)
R.id.btnMore -> {
ActionsDialog()
.addAction(getString(R.string.clear_all)) {
languageList.clear()
languageList.add(MyItem(TootStatus.LANGUAGE_CODE_DEFAULT, true))
adapter.notifyDataSetChanged()
}
.addAction(getString(R.string.export)) { export() }
.addAction(getString(R.string.import_)) { import() }
.show(this)
}
}
}
private fun edit(myItem : MyItem?) =
DlgLanguageFilter.open(this, myItem, object : DlgLanguageFilter.Callback {
override fun onOK(code : String, allow : Boolean) {
val it = languageList.iterator()
while(it.hasNext()) {
val item = it.next()
if(item.code == code) {
item.allow = allow
adapter.notifyDataSetChanged()
return
}
}
languageList.add(MyItem(code, allow))
languageList.sortWith(languageComparator)
adapter.notifyDataSetChanged()
return
}
override fun onDelete(code : String) {
val it = languageList.iterator()
while(it.hasNext()) {
val item = it.next()
if(item.code == code) it.remove()
}
adapter.notifyDataSetChanged()
}
})
private object DlgLanguageFilter {
interface Callback {
fun onOK(code : String, allow : Boolean)
fun onDelete(code : String)
}
@SuppressLint("InflateParams")
fun open(activity : ActLanguageFilter, item : MyItem?, callback : Callback) {
val view = activity.layoutInflater.inflate(R.layout.dlg_language_filter, null, false)
val etLanguage : EditText = view.findViewById(R.id.etLanguage)
val btnPresets : ImageButton = view.findViewById(R.id.btnPresets)
val tvLanguage : TextView = view.findViewById(R.id.tvLanguage)
val rbShow : RadioButton = view.findViewById(R.id.rbShow)
val rbHide : RadioButton = view.findViewById(R.id.rbHide)
when(item?.allow ?: true) {
true -> rbShow.isChecked = true
else -> rbHide.isChecked = true
}
fun updateDesc() {
val code = etLanguage.text.toString().trim()
val desc = activity.languageNameMap[code] ?: activity.getString(R.string.custom)
tvLanguage.text = desc
}
val languageList =
activity.languageNameMap.map { MyItem(it.key, true) }.sortedWith(languageComparator)
btnPresets.setOnClickListener {
val ad = ActionsDialog()
for(a in languageList) {
ad.addAction("${a.code} ${activity.getDesc(a)}") {
etLanguage.setText(a.code)
updateDesc()
}
}
ad.show(activity, activity.getString(R.string.presets))
}
etLanguage.setText(item?.code ?: "")
updateDesc()
etLanguage.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(p0 : Editable?) {
updateDesc()
}
override fun beforeTextChanged(p0 : CharSequence?, p1 : Int, p2 : Int, p3 : Int) {
}
override fun onTextChanged(p0 : CharSequence?, p1 : Int, p2 : Int, p3 : Int) {
}
})
if(item != null) {
etLanguage.isEnabled = false
btnPresets.isEnabled = false
btnPresets.setEnabledColor(
activity,
R.drawable.ic_edit,
getAttributeColor(activity, R.attr.colorVectorDrawable),
false
)
}
val builder = AlertDialog.Builder(activity)
.setView(view)
.setCancelable(true)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.ok) { _, _ ->
callback.onOK(etLanguage.text.toString().trim(), rbShow.isChecked)
}
if(item != null && item.code != TootStatus.LANGUAGE_CODE_DEFAULT) {
builder.setNeutralButton(R.string.delete) { _, _ ->
callback.onDelete(etLanguage.text.toString().trim())
}
}
builder.show()
}
}
private fun export() {
val progress = ProgressDialogEx(this)
val data = JsonObject().apply {
for(item in languageList) {
put(item.code, item.allow)
}
}
.toString()
.encodeUTF8()
val task = @SuppressLint("StaticFieldLeak")
object : AsyncTask<Void, String, File?>() {
override fun doInBackground(vararg params : Void) : File? {
try {
val cache_dir = cacheDir
cache_dir.mkdir()
val file = File(
cache_dir,
"SubwayTooter-language-filter.${Process.myPid()}.${Process.myTid()}.json"
)
FileOutputStream(file).use { it.write(data) }
return file
} catch(ex : Throwable) {
log.trace(ex)
showToast(
this@ActLanguageFilter,
ex,
"can't save filter data to temporary file."
)
}
return null
}
override fun onCancelled(result : File?) {
onPostExecute(result)
}
override fun onPostExecute(result : File?) {
progress.dismissSafe()
if(isCancelled || result == null) {
// cancelled.
return
}
try {
val uri = FileProvider.getUriForFile(
this@ActLanguageFilter,
App1.FILE_PROVIDER_AUTHORITY,
result
)
val intent = Intent(Intent.ACTION_SEND)
intent.type = contentResolver.getType(uri)
intent.putExtra(Intent.EXTRA_SUBJECT, "SubwayTooter language filter 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)
} catch(ex : Throwable) {
log.trace(ex)
showToast(this@ActLanguageFilter, ex, "export failed.")
}
}
}
progress.isIndeterminateEx = true
progress.setCancelable(true)
progress.setOnCancelListener { task.cancel(true) }
progress.show()
task.executeOnExecutor(App1.task_executor)
}
private fun import() {
try {
val intent = intentOpenDocument("*/*")
startActivityForResult(intent, REQUEST_CODE_IMPORT)
} catch(ex : Throwable) {
showToast(this, ex, "import failed.")
}
}
override fun onActivityResult(requestCode : Int, resultCode : Int, data : Intent?) {
if(resultCode == RESULT_OK && data != null && requestCode == REQUEST_CODE_IMPORT) {
data.handleGetContentResult(contentResolver).firstOrNull()?.uri?.let {
import2(it)
}
}
super.onActivityResult(requestCode, resultCode, data)
}
private fun import2(uri : Uri) {
val type = contentResolver.getType(uri)
log.d("import2 type=%s", type)
val progress = ProgressDialogEx(this)
val task = @SuppressLint("StaticFieldLeak")
object : AsyncTask<Void, String, JsonObject?>() {
override fun doInBackground(vararg params : Void) : JsonObject? {
try {
val source = contentResolver.openInputStream(uri)
if(source == null) {
showToast(this@ActLanguageFilter, true, "openInputStream failed.")
return null
}
return source.use { inStream ->
val bao = ByteArrayOutputStream()
IOUtils.copy(inStream, bao)
bao.toByteArray().decodeUTF8().decodeJsonObject()
}
} catch(ex : Throwable) {
log.trace(ex)
showToast(this@ActLanguageFilter, ex, "can't load filter data.")
return null
}
}
override fun onCancelled(result : JsonObject?) {
onPostExecute(result)
}
override fun onPostExecute(result : JsonObject?) {
progress.dismissSafe()
// cancelled.
if(isCancelled || result == null) return
load(result)
}
}
progress.isIndeterminateEx = true
progress.setCancelable(true)
progress.setOnCancelListener { task.cancel(true) }
progress.show()
task.executeOnExecutor(App1.task_executor)
}
}