add privacy policy, update dependencies

This commit is contained in:
tateisu 2018-09-27 23:21:37 +09:00
parent bc24aa7e15
commit d109e5c324
36 changed files with 460 additions and 230 deletions

View File

@ -3,23 +3,24 @@ package jp.juggler.apng.sample
import android.Manifest
import android.content.pm.PackageManager
import android.os.Build
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.os.SystemClock
import android.support.v4.app.ActivityCompat
import android.support.v4.content.ContextCompat
import android.support.v7.app.AppCompatActivity
import android.util.Log
import android.view.View
import android.view.ViewGroup
import android.widget.*
import android.widget.AdapterView
import android.widget.BaseAdapter
import android.widget.ListView
import android.widget.TextView
import jp.juggler.apng.ApngFrames
import kotlinx.coroutines.experimental.*
import kotlinx.coroutines.experimental.android.UI
import java.lang.ref.WeakReference
import android.support.v4.content.ContextCompat
import kotlinx.coroutines.experimental.android.Main
import kotlin.coroutines.experimental.CoroutineContext
class ActList : AppCompatActivity() {
class ActList : AppCompatActivity(), CoroutineScope {
companion object {
const val TAG = "ActList"
@ -33,7 +34,15 @@ class ActList : AppCompatActivity() {
private lateinit var listAdapter : MyAdapter
private var timeAnimationStart : Long = 0L
private lateinit var activityJob: Job
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + activityJob
override fun onCreate(savedInstanceState : Bundle?) {
activityJob = Job()
super.onCreate(savedInstanceState)
setContentView(R.layout.act_list)
this.listView = findViewById(R.id.listView)
@ -56,35 +65,13 @@ class ActList : AppCompatActivity() {
)
}
}
launch(UI) {
if(isDestroyed) return@launch
val list = async(CommonPool) {
// RawリソースのIDと名前の一覧
R.raw::class.java.fields
.mapNotNull { it.get(null) as? Int }
.map { id ->
ListItem(
id,
resources.getResourceName(id)
.replaceFirst(""".+/""".toRegex(), "")
)
}
.toMutableList()
.apply { sortBy { it.caption } }
}.await()
if(isDestroyed) return@launch
listAdapter.list.addAll(list)
listAdapter.notifyDataSetChanged()
}
load()
}
override fun onDestroy() {
super.onDestroy()
activityJob.cancel()
}
override fun onRequestPermissionsResult(
requestCode : Int,
@ -108,6 +95,27 @@ class ActList : AppCompatActivity() {
}
}
private fun load() = launch{
val list = async(Dispatchers.IO) {
// RawリソースのIDと名前の一覧
R.raw::class.java.fields
.mapNotNull { it.get(null) as? Int }
.map { id ->
ListItem(
id,
resources.getResourceName(id)
.replaceFirst(""".+/""".toRegex(), "")
)
}
.toMutableList()
.apply { sortBy { it.caption } }
}.await()
listAdapter.list.addAll(list)
listAdapter.notifyDataSetChanged()
}
inner class MyAdapter : BaseAdapter(), AdapterView.OnItemClickListener {
val list = ArrayList<ListItem>()
@ -155,12 +163,11 @@ class ActList : AppCompatActivity() {
}
class MyViewHolder(
inner class MyViewHolder(
viewRoot : View,
_activity : ActList
) {
private val activity = ref(_activity)
private val tvCaption : TextView = viewRoot.findViewById(R.id.tvCaption)
private val apngView : ApngView = viewRoot.findViewById(R.id.apngView)
@ -179,41 +186,33 @@ class ActList : AppCompatActivity() {
lastId = resId
apngView.apngFrames?.dispose()
apngView.apngFrames = null
launch(UI) {
launch{
var apngFrames :ApngFrames? = null
try {
if(activity()?.isDestroyed != false) return@launch
lastJob?.cancelAndJoin()
val job = async(CommonPool) {
activity()?.resources?.openRawResource(resId)?.use { inStream ->
val job = async(Dispatchers.IO) {
resources?.openRawResource(resId)?.use { inStream ->
ApngFrames.parseApng(inStream, 128)
}
}
lastJob = job
val apngFrames = job.await()
if(activity()?.isDestroyed == false
&& lastId == resId
&& apngFrames != null
) {
apngFrames = job.await()
if(apngFrames != null && lastId == resId ){
apngView.apngFrames = apngFrames
} else {
apngFrames?.dispose()
apngFrames = null
}
} catch(ex : Throwable) {
ex.printStackTrace()
Log.e(TAG, "load error: ${ex.javaClass.simpleName} ${ex.message}")
}finally{
apngFrames?.dispose()
}
}
}
}
}
}
class WeakRef<T : Any>(t : T) : WeakReference<T>(t) {
operator fun invoke() : T? = get()
}
fun <T : Any> ref(t : T) = WeakRef(t)

View File

@ -10,14 +10,13 @@ import android.util.Log
import android.view.View
import android.widget.TextView
import jp.juggler.apng.ApngFrames
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.async
import kotlinx.coroutines.experimental.launch
import kotlinx.coroutines.experimental.*
import kotlinx.coroutines.experimental.android.Main
import java.io.File
import java.io.FileOutputStream
import kotlin.coroutines.experimental.CoroutineContext
class ActViewer : AppCompatActivity() {
class ActViewer : AppCompatActivity() , CoroutineScope {
companion object {
const val TAG="ActViewer"
@ -35,7 +34,13 @@ class ActViewer : AppCompatActivity() {
private lateinit var apngView : ApngView
private lateinit var tvError : TextView
private lateinit var activityJob: Job
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + activityJob
override fun onCreate(savedInstanceState : Bundle?) {
activityJob = Job()
super.onCreate(savedInstanceState)
val intent = this.intent
@ -56,11 +61,11 @@ class ActViewer : AppCompatActivity() {
return@setOnLongClickListener true
}
launch(UI) {
launch{
var apngFrames : ApngFrames? = null
try {
if(isDestroyed) return@launch
val apngFrames = async(CommonPool) {
apngFrames = async(Dispatchers.IO) {
resources.openRawResource(resId).use {
ApngFrames.parseApng(
it,
@ -70,16 +75,11 @@ class ActViewer : AppCompatActivity() {
}
}.await()
apngView.visibility = View.VISIBLE
tvError.visibility = View.GONE
apngView.apngFrames = apngFrames
apngFrames = null
if(isDestroyed) {
apngFrames.dispose()
} else {
apngView.visibility = View.VISIBLE
tvError.visibility = View.GONE
apngView.apngFrames = apngFrames
}
} catch(ex : Throwable) {
ex.printStackTrace()
Log.e(ActList.TAG, "load error: ${ex.javaClass.simpleName} ${ex.message}")
@ -90,6 +90,8 @@ class ActViewer : AppCompatActivity() {
tvError.visibility = View.VISIBLE
tvError.text = message
}
}finally{
apngFrames?.dispose()
}
}
}
@ -97,11 +99,13 @@ class ActViewer : AppCompatActivity() {
override fun onDestroy() {
super.onDestroy()
apngView.apngFrames?.dispose()
activityJob.cancel()
}
private fun save(apngFrames:ApngFrames){
val title = this.title
launch(CommonPool){
launch(Dispatchers.IO){
val dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
dir.mkdirs()
if(! dir.exists() ) {

View File

@ -11,8 +11,8 @@ android {
targetSdkVersion target_sdk_version
minSdkVersion min_sdk_version
versionCode 288
versionName "2.8.8"
versionCode 289
versionName "2.8.9"
applicationId "jp.juggler.subwaytooter"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
@ -80,7 +80,7 @@ dependencies {
// https://firebase.google.com/support/release-notes/android
implementation "com.google.firebase:firebase-core:16.0.3"
implementation "com.google.firebase:firebase-messaging:17.3.1"
implementation "com.google.firebase:firebase-messaging:17.3.2"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
@ -89,9 +89,8 @@ dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlinx_coroutines_version"
// Anko Layouts
implementation "org.jetbrains.anko:anko:$anko_version"
implementation "org.jetbrains.anko:anko-sdk25:$anko_version"
// sdk15, sdk19, sdk21, sdk23 are also available
implementation "org.jetbrains.anko:anko-sdk25:$anko_version"
implementation "org.jetbrains.anko:anko-appcompat-v7:$anko_version"
// Coroutine listeners for Anko Layouts

View File

@ -1424,7 +1424,7 @@ class ActAccountSetting
@Throws(IOException::class)
override fun open() : InputStream {
return contentResolver.openInputStream(uri)
return contentResolver.openInputStream(uri) ?: error("openInputStream returns null")
}
override fun deleteTempFile() {

View File

@ -3,6 +3,7 @@ package jp.juggler.subwaytooter
import android.annotation.SuppressLint
import android.app.Activity
import android.app.Dialog
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.content.pm.PackageManager
@ -42,10 +43,6 @@ import jp.juggler.subwaytooter.api.entity.*
import org.apache.commons.io.IOUtils
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.InputStreamReader
import java.lang.ref.WeakReference
import java.util.ArrayList
import java.util.HashSet
@ -65,6 +62,7 @@ import jp.juggler.subwaytooter.span.MyClickableSpan
import jp.juggler.subwaytooter.span.MyClickableSpanClickCallback
import jp.juggler.subwaytooter.util.*
import jp.juggler.subwaytooter.view.ListDivider
import java.io.*
import java.util.zip.ZipInputStream
import kotlin.math.min
@ -433,6 +431,8 @@ class ActMain : AppCompatActivity()
if(savedInstanceState != null) {
sent_intent2?.let { handleSentIntent(it) }
}
checkPrivacyPolicy()
}
override fun onDestroy() {
@ -850,9 +850,7 @@ class ActMain : AppCompatActivity()
showFooterColor()
if(resultCode == RESULT_APP_DATA_IMPORT) {
if(data != null) {
importAppData(data.data)
}
importAppData(data?.data)
}
} else if(requestCode == REQUEST_CODE_TEXT) {
@ -1330,7 +1328,9 @@ class ActMain : AppCompatActivity()
env.tablet_pager.adapter = env.tablet_pager_adapter
env.tablet_pager.layoutManager = env.tablet_layout_manager
env.tablet_pager.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView : RecyclerView?, newState : Int) {
override fun onScrollStateChanged(recyclerView : RecyclerView, newState : Int) {
super.onScrollStateChanged(recyclerView, newState)
val vs = env.tablet_layout_manager.findFirstVisibleItemPosition()
@ -1345,7 +1345,7 @@ class ActMain : AppCompatActivity()
}
}
override fun onScrolled(recyclerView : RecyclerView?, dx : Int, dy : Int) {
override fun onScrolled(recyclerView : RecyclerView, dx : Int, dy : Int) {
super.onScrolled(recyclerView, dx, dy)
updateColumnStripSelection(- 1, - 1f)
}
@ -1465,7 +1465,7 @@ class ActMain : AppCompatActivity()
var slide_ratio = 0f
if(vr.first <= vr.last) {
val child = env.tablet_layout_manager.findViewByPosition(vr.first)
slide_ratio = Math.abs(child.left / nColumnWidth.toFloat())
slide_ratio = Math.abs( (child?.left ?: 0) / nColumnWidth.toFloat())
}
llColumnStrip.setVisibleRange(vr.first, vr.last, slide_ratio)
@ -2339,8 +2339,12 @@ class ActMain : AppCompatActivity()
}, { env ->
for(i in 0 until env.tablet_layout_manager.childCount) {
val v = env.tablet_layout_manager.getChildAt(i)
val columnViewHolder =
(env.tablet_pager.getChildViewHolder(v) as? TabletColumnViewHolder)?.columnViewHolder
val columnViewHolder =when(v){
null-> null
else->(env.tablet_pager.getChildViewHolder(v) as? TabletColumnViewHolder)?.columnViewHolder
}
if(columnViewHolder?.isColumnSettingShown == true) {
columnViewHolder.closeColumnSetting()
return@closeColumnSetting true
@ -2496,7 +2500,8 @@ class ActMain : AppCompatActivity()
//////////////////////////////////////////////////////////////////////////////////////////////
private fun importAppData(uri : Uri) {
private fun importAppData(uri : Uri?) {
uri ?: return
// remove all columns
run {
@ -2764,4 +2769,45 @@ class ActMain : AppCompatActivity()
}
}
private var dlgPrivacyPolicy : WeakReference<Dialog>?=null
private fun checkPrivacyPolicy() {
// 既に表示中かもしれない
if( dlgPrivacyPolicy?.get()?.isShowing == true) return
val res_id = when(getString(R.string.language_code)) {
"ja" -> R.raw.privacy_policy_ja
"fr" -> R.raw.privacy_policy_fr
else -> R.raw.privacy_policy_en
}
// プライバシーポリシーデータの読み込み
val bytes = loadRawResource(res_id)
if( bytes.isEmpty() ) return
// 同意ずみなら表示しない
val digest = bytes.digestSHA256().encodeBase64Url()
if( digest == Pref.spAgreedPrivacyPolicyDigest(pref) ) return
val dialog = AlertDialog.Builder(this)
.setTitle(R.string.privacy_policy)
.setMessage( bytes.decodeUTF8())
.setNegativeButton(R.string.cancel){_,_ ->
finish()
}
.setOnCancelListener{_->
finish()
}
.setPositiveButton(R.string.agree){_,_ ->
pref.edit().put(Pref.spAgreedPrivacyPolicyDigest,digest).apply()
}
.create()
dlgPrivacyPolicy = WeakReference(dialog)
dialog.show()
}
}

View File

@ -3,13 +3,9 @@ package jp.juggler.subwaytooter
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.widget.TextView
import org.apache.commons.io.IOUtils
import java.io.ByteArrayOutputStream
import jp.juggler.subwaytooter.util.LogCategory
import jp.juggler.subwaytooter.util.decodeUTF8
import jp.juggler.subwaytooter.util.loadRawResource
class ActOSSLicense : AppCompatActivity() {
@ -23,13 +19,8 @@ class ActOSSLicense : AppCompatActivity() {
setContentView(R.layout.act_oss_license)
try {
resources.openRawResource(R.raw.oss_license)?.use { inData ->
ByteArrayOutputStream().use { bao ->
IOUtils.copy(inData, bao)
val tv = findViewById<TextView>(R.id.tvText)
tv.text = bao.toByteArray().decodeUTF8()
}
}
val tv = findViewById<TextView>(R.id.tvText)
tv.text = loadRawResource(R.raw.oss_license).decodeUTF8()
} catch(ex : Throwable) {
log.trace(ex)
}

View File

@ -23,6 +23,7 @@ import android.support.v4.content.ContextCompat
import android.support.v7.app.AlertDialog
import android.support.v7.app.AppCompatActivity
import android.text.Editable
import android.text.Spannable
import android.text.TextWatcher
import android.text.method.LinkMovementMethod
import android.view.View
@ -641,19 +642,26 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba
// 再編集の場合はdefault_textは反映されない
val decodeOptions = DecodeOptions(this)
etContent.text = decodeOptions.decodeHTML(base_status.content)
etContent.setSelection(etContent.text.length)
etContentWarning.setText(decodeOptions.decodeEmoji(base_status.spoiler_text))
etContentWarning.setSelection(etContentWarning.text.length)
cbContentWarning.isChecked = etContentWarning.text.isNotEmpty()
var text :Spannable
text = decodeOptions.decodeHTML(base_status.content)
etContent.text = text
etContent.setSelection(text.length )
text =decodeOptions.decodeEmoji(base_status.spoiler_text)
etContentWarning.setText(text)
etContentWarning.setSelection(text.length)
cbContentWarning.isChecked = text.isNotEmpty()
cbNSFW.isChecked = base_status.sensitive == true
val src_enquete = base_status.enquete
val src_items = src_enquete?.items
if(src_items != null && src_enquete.type == NicoEnquete.TYPE_ENQUETE) {
cbEnquete.isChecked = true
etContent.text = decodeOptions.decodeHTML(src_enquete.question)
etContent.setSelection(etContent.text.length)
text = decodeOptions.decodeHTML(src_enquete.question)
etContent.text = text
etContent.setSelection(text.length)
var src_index = 0
for(et in list_etChoice) {
@ -763,22 +771,37 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba
if(svEmoji.isEmpty()) return
val editable = etContent.text
if(editable.isNotEmpty()
&& ! CharacterGroup.isWhitespace(editable[editable.length - 1].toInt())
) {
editable.append(' ')
}
if(selectBefore) {
val start = editable.length
editable.append(' ')
editable.append(svEmoji)
etContent.text = editable
etContent.setSelection(start)
} else {
editable.append(svEmoji)
etContent.text = editable
etContent.setSelection(editable.length)
if( editable == null ) {
val sb = StringBuilder ()
if(selectBefore) {
val start = 0
sb.append(' ')
sb.append(svEmoji)
etContent.setText(sb)
etContent.setSelection(start)
} else {
sb.append(svEmoji)
etContent.setText(sb)
etContent.setSelection(sb.length)
}
}else{
if(editable.isNotEmpty()
&& ! CharacterGroup.isWhitespace(editable[editable.length - 1].toInt())
) {
editable.append(' ')
}
if(selectBefore) {
val start = editable.length
editable.append(' ')
editable.append(svEmoji)
etContent.text = editable
etContent.setSelection(start)
} else {
editable.append(svEmoji)
etContent.text = editable
etContent.setSelection(editable.length)
}
}
}
@ -1494,7 +1517,7 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba
@Throws(IOException::class)
override fun open() : InputStream {
return contentResolver.openInputStream(uri)
return contentResolver.openInputStream(uri) ?: error("openInputStream returns null")
}
override fun deleteTempFile() {
@ -2348,7 +2371,7 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba
else -> R.raw.recommended_plugin_en
}
this.loadRawResource(res_id)?.let { data ->
this.loadRawResource(res_id).let { data ->
val text = data.decodeUTF8()
val viewRoot = layoutInflater.inflate(R.layout.dlg_plugin_missing, null, false)

View File

@ -4900,7 +4900,7 @@ class Column(
private fun loadSearchDesc(raw_en : Int, raw_ja : Int) : String {
val res_id = if("ja" == context.getString(R.string.language_code)) raw_ja else raw_en
return context.loadRawResource(res_id)?.decodeUTF8() ?: "?"
return context.loadRawResource(res_id).decodeUTF8()
}
private var cacheHeaderDesc : String? = null

View File

@ -194,7 +194,7 @@ class ColumnViewHolder(
listView = viewRoot.findViewById(R.id.listView)
if(Pref.bpShareViewPool(activity.pref)) {
listView.recycledViewPool = activity.viewPool
listView.setRecycledViewPool( activity.viewPool)
}
listView.itemAnimator = null
//

View File

@ -89,7 +89,7 @@ class StringPref(
) : BasePref<String>(key) {
override operator fun invoke(pref : SharedPreferences) : String {
return pref.getString(key, defVal)
return pref.getString(key,defVal) ?: defVal
}
override fun put(editor : SharedPreferences.Editor, v : String) {
@ -414,6 +414,7 @@ object Pref {
val spUserAgent = StringPref("UserAgent", "")
val spMediaReadTimeout = StringPref("spMediaReadTimeout", "60")
val spAgreedPrivacyPolicyDigest= StringPref("spAgreedPrivacyPolicyDigest", "")
// long
val lpTabletTootDefaultAccount = LongPref("tablet_toot_default_account", - 1L)

View File

@ -619,7 +619,7 @@ object Action_Toot {
val dialog = ActionsDialog()
val host_original = Uri.parse(url).authority
val host_original = Uri.parse(url).authority ?: ""
// 選択肢:ブラウザで表示する
dialog.addAction(

View File

@ -358,8 +358,11 @@ open class TootAccount(parser : TootParser, src : JSONObject) {
if(url != null) {
try {
// たぶんどんなURLでもauthorityの部分にホスト名が来るだろう(慢心)
val uri = Uri.parse(url)
return uri.authority.toLowerCase()
val host = Uri.parse(url).authority
if( host?.isNotEmpty() == true){
return host.toLowerCase()
}
log.e("findHostFromUrl: can't parse host from URL $url")
} catch(ex : Throwable) {
log.e(ex, "findHostFromUrl: can't parse host from URL $url")
}

View File

@ -38,12 +38,14 @@ object LoginForm {
) -> Unit
) {
val view = activity.layoutInflater.inflate(R.layout.dlg_account_add, null, false)
val etInstance :AutoCompleteTextView = view.findViewById(R.id.etInstance)
val btnOk :View = view.findViewById(R.id.btnOk)
val cbPseudoAccount :CheckBox = view.findViewById(R.id.cbPseudoAccount)
val cbInputAccessToken :CheckBox = view.findViewById(R.id.cbInputAccessToken)
val etInstance : AutoCompleteTextView = view.findViewById(R.id.etInstance)
val btnOk : View = view.findViewById(R.id.btnOk)
val cbPseudoAccount : CheckBox = view.findViewById(R.id.cbPseudoAccount)
val cbInputAccessToken : CheckBox = view.findViewById(R.id.cbInputAccessToken)
cbPseudoAccount.setOnCheckedChangeListener { _, _ -> cbInputAccessToken.isEnabled = ! cbPseudoAccount.isChecked }
cbPseudoAccount.setOnCheckedChangeListener { _, _ ->
cbInputAccessToken.isEnabled = ! cbPseudoAccount.isChecked
}
if(instanceArg != null && instanceArg.isNotEmpty()) {
etInstance.setText(instanceArg)
@ -61,36 +63,34 @@ object LoginForm {
}
val dialog = Dialog(activity)
dialog.setContentView(view)
btnOk.setOnClickListener(View.OnClickListener {
btnOk.setOnClickListener { _ ->
val instance = etInstance.text.toString().trim { it <= ' ' }
if( instance.isEmpty() ) {
showToast(activity, true, R.string.instance_not_specified)
return@OnClickListener
} else if(instance.contains("/") || instance.contains("@")) {
showToast(activity, true, R.string.instance_not_need_slash)
return@OnClickListener
when {
instance.isEmpty() -> showToast(activity, true, R.string.instance_not_specified)
instance.contains("/") || instance.contains("@") -> showToast(
activity,
true,
R.string.instance_not_need_slash
)
else -> onClickOk(
dialog,
instance,
cbPseudoAccount.isChecked,
cbInputAccessToken.isChecked
)
}
onClickOk(dialog, instance, cbPseudoAccount.isChecked, cbInputAccessToken.isChecked)
})
}
view.findViewById<View>(R.id.btnCancel).setOnClickListener { dialog.cancel() }
val instance_list = ArrayList<String>()
try {
val `is` = activity.resources.openRawResource(R.raw.server_list)
try {
val br = BufferedReader(InputStreamReader(`is`, "UTF-8"))
activity.resources.openRawResource(R.raw.server_list).use { inStream ->
val br = BufferedReader(InputStreamReader(inStream, "UTF-8"))
while(true) {
val s : String = br.readLine()?.trim { it <= ' ' }?.toLowerCase() ?: break
if(s.isNotEmpty()) instance_list.add(s)
}
} finally {
try {
`is`.close()
} catch(ignored : Throwable) {
}
}
} catch(ex : Throwable) {
log.trace(ex)
@ -107,7 +107,7 @@ object LoginForm {
override fun performFiltering(constraint : CharSequence?) : Filter.FilterResults {
val result = Filter.FilterResults()
if( constraint?.isNotEmpty() ==true ) {
if(constraint?.isNotEmpty() == true) {
val key = constraint.toString().toLowerCase()
// suggestions リストは毎回生成する必要がある。publishResultsと同時にアクセスされる場合がある
val suggestions = StringArray()
@ -123,7 +123,10 @@ object LoginForm {
return result
}
override fun publishResults(constraint : CharSequence?, results : Filter.FilterResults?) {
override fun publishResults(
constraint : CharSequence?,
results : Filter.FilterResults?
) {
clear()
val values = results?.values
if(values is StringArray) {
@ -145,7 +148,8 @@ object LoginForm {
dialog.window?.setLayout(
WindowManager.LayoutParams.MATCH_PARENT,
WindowManager.LayoutParams.WRAP_CONTENT)
WindowManager.LayoutParams.WRAP_CONTENT
)
dialog.show()
}

View File

@ -133,7 +133,7 @@ class AcctColor {
if(cached != null) return cached
try {
val where_arg = load_where_arg.get()
val where_arg = load_where_arg.get() ?: arrayOfNulls<String?>(1)
where_arg[0] = acct
App1.database.query(table, null, load_where, where_arg, null, null, null)
.use { cursor ->

View File

@ -111,7 +111,7 @@ object AcctSet :TableCompanion{
fun searchPrefix(prefix : String, limit : Int) : ArrayList<CharSequence> {
try {
val where_arg = prefix_search_where_arg.get()
val where_arg = prefix_search_where_arg.get() ?: arrayOfNulls<String?>(1)
where_arg[0] = makePattern(prefix)
App1.database.query(table, null, prefix_search_where, where_arg, null, null, COL_ACCT + " asc limit " + limit)
.use { cursor ->

View File

@ -46,7 +46,7 @@ object SubscriptionServerKey : TableCompanion {
fun find(clientIdentifier : String) : String? {
try {
val whereArgs = findWhereArgs.get()
val whereArgs = findWhereArgs.get() ?: arrayOfNulls<String?>(1)
whereArgs[0] = clientIdentifier
App1.database.query(
table,

View File

@ -1,6 +1,5 @@
package jp.juggler.subwaytooter.table
import android.database.Cursor
import android.database.sqlite.SQLiteDatabase
// SQLite にBooleanをそのまま保存することはできないのでInt型との変換が必要になる

View File

@ -124,7 +124,7 @@ object TagSet :TableCompanion{
val dst = ArrayList<CharSequence>()
try {
val where_arg = prefix_search_where_arg.get()
val where_arg = prefix_search_where_arg.get()?: arrayOfNulls<String?>(1)
where_arg[0] = makePattern(prefix)
App1.database.query(
table,
@ -133,7 +133,7 @@ object TagSet :TableCompanion{
where_arg,
null,
null,
COL_TAG + " asc limit " + limit
"$COL_TAG asc limit $limit"
).use { cursor ->
dst.ensureCapacity(cursor.count)
val idx_acct = cursor.getColumnIndex(COL_TAG)

View File

@ -214,22 +214,21 @@ class UserRelation {
private fun load(db_id : Long, who_id : Long) : UserRelation? {
try {
val where_arg = load_where_arg.get()
val where_arg = load_where_arg.get() ?: arrayOfNulls<String?>(2)
where_arg[0] = db_id.toString()
where_arg[1] = who_id.toString()
App1.database.query(table, null, load_where, where_arg, null, null, null)
.use { cursor ->
if(cursor.moveToNext()) {
val dst = UserRelation()
dst.following = 0 != cursor.getInt(cursor.getColumnIndex(COL_FOLLOWING))
dst.followed_by = 0 !=
cursor.getInt(cursor.getColumnIndex(COL_FOLLOWED_BY))
dst.blocking = 0 != cursor.getInt(cursor.getColumnIndex(COL_BLOCKING))
dst.muting = 0 != cursor.getInt(cursor.getColumnIndex(COL_MUTING))
dst.requested = 0 != cursor.getInt(cursor.getColumnIndex(COL_REQUESTED))
dst.following = cursor.getInt(cursor.getColumnIndex(COL_FOLLOWING)).i2b()
dst.followed_by =cursor.getInt(cursor.getColumnIndex(COL_FOLLOWED_BY)).i2b()
dst.blocking = cursor.getInt(cursor.getColumnIndex(COL_BLOCKING)).i2b()
dst.muting = cursor.getInt(cursor.getColumnIndex(COL_MUTING)).i2b()
dst.requested = cursor.getInt(cursor.getColumnIndex(COL_REQUESTED)).i2b()
dst.following_reblogs =
cursor.getInt(cursor.getColumnIndex(COL_FOLLOWING_REBLOGS))
dst.endorsed = 0 != cursor.getInt(cursor.getColumnIndex(COL_ENDORSED))
dst.endorsed = cursor.getInt(cursor.getColumnIndex(COL_ENDORSED)).i2b()
return dst
}
}

View File

@ -74,7 +74,7 @@ object UserRelationMisskey : TableCompanion {
cv.put(COL_MUTING, src.muting.b2i())
cv.put(COL_REQUESTED, src.requested.b2i())
cv.put(COL_FOLLOWING_REBLOGS, src.following_reblogs)
cv.put(COL_ENDORSED,src.endorsed.b2i())
cv.put(COL_ENDORSED, src.endorsed.b2i())
App1.database.replace(table, null, cv)
val key = String.format("%s:%s", db_id, whoId)
@ -112,7 +112,7 @@ object UserRelationMisskey : TableCompanion {
cv.put(COL_MUTING, src.muting.b2i())
cv.put(COL_REQUESTED, src.requested.b2i())
cv.put(COL_FOLLOWING_REBLOGS, src.following_reblogs)
cv.put(COL_ENDORSED,src.endorsed.b2i())
cv.put(COL_ENDORSED, src.endorsed.b2i())
db.replace(table, null, cv)
}
bOK = true
@ -144,22 +144,22 @@ object UserRelationMisskey : TableCompanion {
fun load(db_id : Long, who_id : String) : UserRelation? {
try {
val where_arg = load_where_arg.get()
val where_arg = load_where_arg.get() ?: arrayOfNulls<String?>(2)
where_arg[0] = db_id.toString()
where_arg[1] = who_id
App1.database.query(table, null, load_where, where_arg, null, null, null)
.use { cursor ->
if(cursor.moveToNext()) {
val dst = UserRelation()
dst.following = 0 != cursor.getInt(cursor.getColumnIndex(COL_FOLLOWING))
dst.followed_by = 0 !=
cursor.getInt(cursor.getColumnIndex(COL_FOLLOWED_BY))
dst.blocking = 0 != cursor.getInt(cursor.getColumnIndex(COL_BLOCKING))
dst.muting = 0 != cursor.getInt(cursor.getColumnIndex(COL_MUTING))
dst.requested = 0 != cursor.getInt(cursor.getColumnIndex(COL_REQUESTED))
dst.following = cursor.getInt(cursor.getColumnIndex(COL_FOLLOWING)).i2b()
dst.followed_by =
cursor.getInt(cursor.getColumnIndex(COL_FOLLOWED_BY)).i2b()
dst.blocking = cursor.getInt(cursor.getColumnIndex(COL_BLOCKING)).i2b()
dst.muting = cursor.getInt(cursor.getColumnIndex(COL_MUTING)).i2b()
dst.requested = cursor.getInt(cursor.getColumnIndex(COL_REQUESTED)).i2b()
dst.following_reblogs =
cursor.getInt(cursor.getColumnIndex(COL_FOLLOWING_REBLOGS))
dst.endorsed = 0 != cursor.getInt(cursor.getColumnIndex(COL_ENDORSED))
dst.endorsed = cursor.getInt(cursor.getColumnIndex(COL_ENDORSED)).i2b()
return dst
}
}

View File

@ -60,7 +60,7 @@ class BucketList<E> constructor(
// allocalted を指定しない場合は BucketPosを生成します
private fun findPos(
total_index : Int,
result : BucketPos = pos_internal.get()
result : BucketPos = pos_internal.get()!!
) : BucketPos {
if(total_index < 0 || total_index >= size) {

View File

@ -262,7 +262,7 @@ object HTMLDecoder {
log.d("parseChild: %s|%s %s [%s]", indent, child.tag, open_type, child.text)
if(OPEN_TYPE_OPEN == open_type) {
child.addChild(t, indent + "--")
child.addChild(t, "$indent--")
}
}
if(DEBUG_HTML_PARSER) log.d("parseChild: %s)%s", indent, tag)
@ -435,10 +435,10 @@ object HTMLDecoder {
sb.append("://")
}
sb.append(uri.authority)
val a = uri.encodedPath
val a = uri.encodedPath ?: ""
val q = uri.encodedQuery
val f = uri.encodedFragment
val remain = a + (if(q == null) "" else "?" + q) + if(f == null) "" else "#" + f
val remain = a + (if(q==null) "" else "?$q") + if(f==null) "" else "#$f"
if(remain.length > 10) {
sb.append(remain.substring(0, 10))
sb.append("")

View File

@ -122,17 +122,19 @@ internal class PopupAutoCompleteAcct(
}
v.setOnClickListener {
val src = et.text
val src_length = src.length
val start = Math.min(src_length, sel_start)
val end = Math.min(src_length, sel_end)
val start : Int
val editable = et.text ?: ""
val sb = SpannableStringBuilder()
.append(src.subSequence(0, start))
val src_length = editable.length
start = Math.min(src_length, sel_start)
val end = Math.min(src_length, sel_end)
sb.append(editable.subSequence(0, start))
val remain = editable.subSequence(end, src_length)
if(acct[0] == ' ') {
// 絵文字ショートコード
if(! EmojiDecoder.canStartShortCode(src, start)) sb.append(' ')
if(! EmojiDecoder.canStartShortCode(sb, start)) sb.append(' ')
sb.append(acct.subSequence(2, acct.length))
} else {
// @user@host, #hashtag
@ -141,7 +143,7 @@ internal class PopupAutoCompleteAcct(
}
val newSelection = sb.length
if(end < src_length) sb.append(src.subSequence(end, src_length))
sb.append(remain)
et.text = sb
et.setSelection(newSelection)

View File

@ -310,7 +310,7 @@ class PostHelper(
if( visibility_checked == TootVisibility.DirectSpecified ){
val userIds = JSONArray()
val reMention = Pattern.compile("(?:\\A|\\s)@([a-zA-Z0-9_]{1,20})(?:@([\\w\\.\\:-]+))?(?:\\z|\\s)")
val reMention = Pattern.compile("(?:\\A|\\s)@([a-zA-Z0-9_]{1,20})(?:@([\\w.:-]+))?(?:\\z|\\s)")
val m = reMention.matcher(content)
while(m.find()){
val username = m.group(1)
@ -830,7 +830,7 @@ class PostHelper(
EmojiPicker(activity, instance, isMisskey) { name, instance, bInstanceHasCustomEmoji ->
val et = this.et ?: return@EmojiPicker
val src = et.text
val src = et.text ?: ""
val src_length = src.length
val end = Math.min(src_length, et.selectionEnd)
val start = src.lastIndexOf(':', end - 1)
@ -841,7 +841,7 @@ class PostHelper(
.appendEmoji(name, instance, bInstanceHasCustomEmoji)
val newSelection = sb.length
if(end < src_length) sb.append(src.subSequence(end, src_length))
if(end < src_length) sb.append(src.subSequence(end, src_length) )
et.text = sb
et.setSelection(newSelection)
@ -858,17 +858,17 @@ class PostHelper(
EmojiPicker(activity, instance, isMisskey) { name, instance, bInstanceHasCustomEmoji ->
val et = this.et ?: return@EmojiPicker
val src = et.text
val src = et.text ?: ""
val src_length = src.length
val start = Math.min(src_length, et.selectionStart)
val end = Math.min(src_length, et.selectionEnd)
val sb = SpannableStringBuilder()
.append(src.subSequence(0, start))
.append(src.subSequence(0, start) )
.appendEmoji(name, instance, bInstanceHasCustomEmoji)
val newSelection = sb.length
if(end < src_length) sb.append(src.subSequence(end, src_length))
if(end < src_length) sb.append(src.subSequence(end, src_length) )
et.text = sb
et.setSelection(newSelection)

View File

@ -27,7 +27,8 @@ class ScrollPosition {
}
fun restore(holder : ColumnViewHolder) {
if(adapterIndex in 0 until holder.listView.adapter.itemCount) {
val adapter = holder.listView.adapter ?: return
if(adapterIndex in 0 until adapter.itemCount) {
holder.listLayoutManager.scrollToPositionWithOffset(adapterIndex, offset)
}
}

View File

@ -104,7 +104,7 @@ object StorageUtils{
// tree
return paths[1]
}
throw IllegalArgumentException("Invalid URI: " + documentUri)
throw IllegalArgumentException("Invalid URI: $documentUri")
}
fun getFile(context : Context, path : String) : File? {
@ -138,7 +138,7 @@ object StorageUtils{
}
}
// MediaStore Uri
context.contentResolver.query(uri, null, null, null, null).use { cursor ->
context.contentResolver.query(uri, null, null, null, null)?.use { cursor ->
if(cursor.moveToFirst()) {
val col_count = cursor.columnCount
for(i in 0 until col_count) {

View File

@ -856,19 +856,15 @@ fun View.showKeyboard() {
////////////////////////////////////////////////////////////////////
// context
fun Context.loadRawResource(res_id : Int) : ByteArray? {
try {
this.resources.openRawResource(res_id).use { inStream ->
val bao = ByteArrayOutputStream()
IOUtils.copy(inStream, bao)
return bao.toByteArray()
}
} catch(ex : Throwable) {
Utils.log.trace(ex)
fun Context.loadRawResource( resId:Int):ByteArray{
resources.openRawResource(resId).use{ inStream->
val bao = ByteArrayOutputStream( inStream.available() )
IOUtils.copy(inStream,bao)
return bao.toByteArray()
}
return null
}
////////////////////////////////////////////////////////////////////
// file

View File

@ -411,7 +411,7 @@ class MyNetworkImageView : AppCompatImageView {
invalidate()
}
override fun onVisibilityChanged(changedView : View?, visibility : Int) {
override fun onVisibilityChanged(changedView : View, visibility : Int) {
super.onVisibilityChanged(changedView, visibility)
loadImageIfNecessary()
}

View File

@ -51,6 +51,8 @@ class TabletModeRecyclerView : RecyclerView {
if(index >= 0) {
if(mForbidStartDragging) return false
val layoutManager = this.layoutManager ?: return false
val x = (e.getX(index) + 0.5f).toInt()
val y = (e.getY(index) + 0.5f).toInt()
val canScrollHorizontally = layoutManager.canScrollHorizontally()

View File

@ -0,0 +1,53 @@
## Subway Tooter Privacy Policy
### User data handled by Subway Tooter
#### Access token of SNS services
In accordance with access delegation of SNS services (Mastodon, Misskey, etc.), Subway Tooter saves the access token to application data.
Also due to old Mastodon restrictions, In accordance with user's explicit permission, Subway Tooter send an access token to the application server or notification listener server for push notification.
Also due to current Mastodon restrictions, Subway Tooter send an unrecoverable digest of access token to the application server for push subscription.
#### User information registered in the SNS service
In accordance with access delegation of SNS services (Mastodon, Misskey, etc.), Subway Tooter saves the information and settings of login account to application data. This includes not only the public profile, also may include information that the login account only can read or update.
Except in the case of sending to the relevant SNS service for editing account information with accordance with user's explicit intent, Subway Tooter does not send this information to the outside of app.
When deleting account information from Subway Tooter, also the user information will be deleted from Subway Tooter's application data.
#### Activities on the SNS service
Subway Tooter has the function to read / update the activities from login account on SNS service such as posts, favorites, boosts, and lists.
Subway Tooter will not send these information to outside the application EXCEPT in the following cases.
- With user's explicit intent, sending activity update/delete request to relevant SNS service.
- With user's explicit intent, sharing URLs or datas to external web browser or other applications.
- With user's explicit intent, downloading data to the folder that is accessible by other applications.
#### Information coming from the input plugin
Subway Tooter has a function to get information from input plugin (external application).
With user's explicit intent, Those information may send to relevant SNS service.
#### User tracking
Subway Tooter uses Firebase Cloud Messaging for push notifications.
Following information will be send to external server.
- Firebase Cloud Messaging sends message recipient information to Google server.
- Subway Tooter sends push subscription management information to the app server.
There is no other kind of user tracking.
#### User location
Subway Tooter has no function to get user location.
The user location may be entered from input plugin, but Subway Tooter handles it as like as normal text.
#### Advertisement
Subway Tooter has no function to show Advertisement.
There are cases SNS services may show ad in its API response, but Subway Tooter does not participate in those.

View File

@ -0,0 +1,54 @@
## Subway Tooter Privacy Policy
### User data handled by Subway Tooter
#### Access token of SNS services
In accordance with access delegation of SNS services (Mastodon, Misskey, etc.), Subway Tooter saves the access token to application data.
Also due to old Mastodon restrictions, In accordance with user's explicit permission, Subway Tooter send an access token to the application server or notification listener server for push notification.
Also due to current Mastodon restrictions, Subway Tooter send an unrecoverable digest of access token to the application server for push subscription.
#### User information registered in the SNS service
In accordance with access delegation of SNS services (Mastodon, Misskey, etc.), Subway Tooter saves the information and settings of login account to application data. This includes not only the public profile, also may include information that the login account only can read or update.
Except in the case of sending to the relevant SNS service for editing account information with accordance with user's explicit intent, Subway Tooter does not send this information to the outside of app.
When deleting account information from Subway Tooter, also the user information will be deleted from Subway Tooter's application data.
#### Activities on the SNS service
Subway Tooter has the function to read / update the activities from login account on SNS service such as posts, favorites, boosts, and lists.
Subway Tooter will not send these information to outside the application EXCEPT in the following cases.
- With user's explicit intent, sending activity update/delete request to relevant SNS service.
- With user's explicit intent, sharing URLs or datas to external web browser or other applications.
- With user's explicit intent, downloading data to the folder that is accessible by other applications.
#### Information coming from the input plugin
Subway Tooter has a function to get information from input plugin (external application).
With user's explicit intent, Those information may send to relevant SNS service.
#### User tracking
Subway Tooter uses Firebase Cloud Messaging for push notifications.
Following information will be send to external server.
- Firebase Cloud Messaging sends message recipient information to Google server.
- Subway Tooter sends push subscription management information to the app server.
There is no other kind of user tracking.
#### User location
Subway Tooter has no function to get user location.
The user location may be entered from input plugin, but Subway Tooter handles it as like as normal text.
#### Advertisement
Subway Tooter has no function to show Advertisement.
There are cases SNS services may show ad in its API response, but Subway Tooter does not participate in those.

View File

@ -0,0 +1,49 @@
## Subway Tooter プライバシー ポリシー
### Subway Tooter が取り扱うユーザデータ
#### SNSサービスのアクセストークン
Subway Tooter はMastodonやMisskey等のSNSサービスのアカウントの権限の認可を受けてそのアクセストークンをアプリ内部に保存します。
また古いMastodonの制限により、プッシュ通知の実現のためにユーザが許可した場合のみアクセストークンをアプリサーバや通知サーバに送ることがあります。
また現行のMastodonの制限により、プッシュ通知の実現のためにアクセストークンを復元不可能なダイジェスト化したものをアプリサーバに保存します。このダイジェストからアクセストークンを復元することはまず不可能です。
#### SNSサービスに登録したユーザ情報
Subway Tooter は登録されたログインアカウントの情報をアプリ内部に保存します。これは公開プロフィールの範囲だけではなく、ログインアカウントと紐付けられたアクセストークンを持つ本人だけが取得/変更可能な情報を含みます。
アカウント情報の編集のために当該SNSサービス自体に送る場合を除き、Subway Tooter はこの情報を外部に送信することはありません。
Subway Tooterからアカウント情報を削除した際に、この情報はSubway Tooterのアプリ内部からも削除されます。
#### SNSサービスで行われた活動
Subway Tooter はログインアカウントから行われた投稿、お気に入り、ブースト、リストなどの活動情報を取得/更新する機能を持ちます。
以下の場合を除き、Subway Tooter がこれらの情報をアプリ外部に送信することはありません。
- 活動情報を更新するために当該SNSサービスに更新/削除リクエストを送る場合
- ユーザの明示的な操作により、端末内のWebブラウザや他アプリにURLやデータを共有する場合
- ユーザの明示的な操作により、端末内の他アプリがアクセス可能な場所にデータを保存する場合
#### 入力プラグイン経由で得られた情報
Subway Tooter は投稿フォームから入力プラグイン(外部アプリ)経由で情報を得る機能があります。ユーザの明示的な操作により得られたそれらの情報は、投稿内容の一部としてSNSサービスに送信されます。
#### ユーザトラッキング
Subway Tooter はPush通知の受信にFirebase Cloud Messagingを使うため、以下の情報をサーバに送信します。
- (Firebase Cloud Messaging による) メッセージ送信先情報のGoogleサーバへの送信
- (Subway Tooterによる) push購読の管理に必要な情報のSubway Tooter アプリサーバへの送信
#### 位置情報
Subway Tooter には位置情報を取り扱う機能はありません。
入力プラグイン経由で間接的に位置情報が入力される場合がありえますが、Subway Tooterはその情報を通常のテキストと同程度に取り扱います。
#### 広告
Subway Tooter には広告を表示する機能はありません。
SNSサービスから取得した情報に何らかの広告が含まれる場合はありえますが、Subway Tooterはそれらの広告には関与しません。

View File

@ -760,5 +760,7 @@
<string name="link_color">リンクの色 (アプリ再起動が必要)</string>
<string name="missing_closeable_column">閉じれるカラムが表示範囲内にありません</string>
<string name="dont_use_custom_tabs">リンクを開く際に (ChromeやFirefoxの) Custom Tabsを使わない</string>
<string name="privacy_policy">プライバシーポリシー</string>
<string name="agree">同意</string>
</resources>

View File

@ -778,5 +778,7 @@
<string name="link_color">Link color (app restart required)</string>
<string name="missing_closeable_column">missing closeable column in visible range.</string>
<string name="dont_use_custom_tabs">Don\'t use (Chrome/Firefox) Custom Tabs when open links</string>
<string name="privacy_policy">Privacy Policy</string>
<string name="agree">Agree</string>
</resources>

View File

@ -1,20 +1,21 @@
buildscript {
ext.kotlin_version = '1.2.70'
ext.kotlinx_coroutines_version = '0.25.3'
ext.anko_version='0.10.5'
ext.asl_version='27.1.1'
ext.target_sdk_version = 27
ext.min_sdk_version = 21
ext.target_sdk_version = 28
ext.asl_version='28.0.0'
ext.kotlin_version = '1.2.71'
ext.kotlinx_coroutines_version = '0.27.0'
ext.anko_version='0.10.5'
repositories {
google()
jcenter()
mavenCentral()
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.1.4'
classpath 'com.android.tools.build:gradle:3.2.0'
classpath 'com.google.gms:google-services:4.1.0'
// https://android-developers.googleblog.com/2018/05/announcing-new-sdk-versioning.html
// com.google.gms:google-services:3.3.0 使3.3.0Gradle sync
@ -30,10 +31,10 @@ buildscript {
allprojects {
repositories {
google()
jcenter()
maven { url 'https://maven.google.com' }
maven { url 'https://jitpack.io' }
google()
}
}

View File

@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip