This commit is contained in:
tateisu 2021-06-13 20:48:48 +09:00
parent c08a5dfd8e
commit 5f7f486471
37 changed files with 3140 additions and 3186 deletions

View File

@ -840,7 +840,7 @@ class ActAppSetting : AppCompatActivity(), ColorPickerDialogListener, View.OnCli
private fun importAppData2(bConfirm: Boolean, uri: Uri) {
val type = contentResolver.getType(uri)
log.d("importAppData type=%s", type)
log.d("importAppData type=$type")
if (!bConfirm) {
AlertDialog.Builder(this)

View File

@ -7,6 +7,7 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.FileProvider
import jp.juggler.util.LogCategory
import jp.juggler.util.digestSHA256Hex
import okhttp3.internal.toHexString
import org.apache.commons.io.IOUtils
import java.io.File
import java.io.FileOutputStream
@ -32,7 +33,7 @@ class ActCallback : AppCompatActivity() {
}
override fun onCreate(savedInstanceState : Bundle?) {
log.d("onCreate flags=%x", intent.flags)
log.d("onCreate flags=0x${intent.flags.toHexString()}")
super.onCreate(savedInstanceState)
var intent : Intent? = intent

View File

@ -141,7 +141,7 @@ class ActMediaViewer : AppCompatActivity(), View.OnClickListener {
}
override fun onRepeatModeChanged(repeatMode: Int) {
log.d("exoPlayer onRepeatModeChanged %d", repeatMode)
log.d("exoPlayer onRepeatModeChanged $repeatMode", )
}
override fun onPlayerError(error: ExoPlaybackException) {

View File

@ -135,14 +135,14 @@ object AppDataExporter {
Cursor.FIELD_TYPE_FLOAT -> {
val d = cursor.getDouble(i)
if(d.isNaN() || d.isInfinite()) {
log.w("column %s is nan or infinite value.", names[i])
log.w("column ${names[i]} is nan or infinite value.")
} else {
writer.name(names[i])
writer.value(d)
}
}
Cursor.FIELD_TYPE_BLOB -> log.w("column %s is blob.", names[i])
Cursor.FIELD_TYPE_BLOB -> log.w("column ${names[i]} is blob." )
}
}

View File

@ -20,8 +20,6 @@ import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.util.NetworkStateTracker
import jp.juggler.subwaytooter.util.PostAttachment
import jp.juggler.util.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.apache.commons.io.IOUtils
import java.io.ByteArrayOutputStream
import java.io.File
@ -37,14 +35,14 @@ enum class DedupMode {
}
class DedupItem(
val text: String,
var time: Long = SystemClock.elapsedRealtime()
val text: String,
var time: Long = SystemClock.elapsedRealtime()
)
class AppState(
internal val context: Context,
internal val handler: Handler,
internal val pref: SharedPreferences
internal val context: Context,
internal val handler: Handler,
internal val pref: SharedPreferences
) {
companion object {
@ -129,9 +127,9 @@ class AppState(
private val _columnList = ArrayList<Column>()
// make shallow copy
val columnList: List<Column>
get() = synchronized(_columnList) { ArrayList(_columnList) }
// make shallow copy
val columnList: List<Column>
get() = synchronized(_columnList) { ArrayList(_columnList) }
val columnCount: Int
get() = synchronized(_columnList) { _columnList.size }
@ -140,7 +138,7 @@ class AppState(
synchronized(_columnList) { _columnList.elementAtOrNull(i) }
fun columnIndex(column: Column?) =
synchronized(_columnList) { _columnList.indexOf(column).takeIf{ it != -1 } }
synchronized(_columnList) { _columnList.indexOf(column).takeIf { it != -1 } }
fun editColumnList(save: Boolean = true, block: (ArrayList<Column>) -> Unit) {
synchronized(_columnList) {
@ -230,10 +228,10 @@ class AppState(
restartTTS()
} else {
log.d(
"proc_flushSpeechQueue: tts is speaking. queue_count=%d, expire_remain=%.3f",
queue_count,
expire_remain / 1000f
)
"proc_flushSpeechQueue: tts is speaking. queue_count=${queue_count}, expire_remain=${
String.format("%.3f",expire_remain / 1000f)
}"
)
handler.postDelayed(this, expire_remain)
return
}
@ -241,7 +239,7 @@ class AppState(
}
val sv = tts_queue.removeFirst()
log.d("proc_flushSpeechQueue: speak %s", sv)
log.d("proc_flushSpeechQueue: speak ${sv}")
val voice_count = voice_list.size
if (voice_count > 0) {
@ -251,11 +249,11 @@ class AppState(
tts_speak_start = now
tts.speak(
sv,
TextToSpeech.QUEUE_ADD,
null, // Bundle params
(++utteranceIdSeed).toString() // String utteranceId
)
sv,
TextToSpeech.QUEUE_ADD,
null, // Bundle params
(++utteranceIdSeed).toString() // String utteranceId
)
} catch (ex: Throwable) {
log.trace(ex)
log.e(ex, "proc_flushSpeechQueue catch exception.")
@ -277,7 +275,7 @@ class AppState(
columnList.mapIndexedNotNull { index, column ->
try {
val dst = JsonObject()
ColumnEncoder.encode(column,dst, index)
ColumnEncoder.encode(column, dst, index)
dst
} catch (ex: JsonException) {
log.trace(ex)
@ -381,7 +379,7 @@ class AppState(
context.showToast(false, R.string.text_to_speech_initializing)
log.d("initializing TextToSpeech…")
launchIO{
launchIO {
var tmp_tts: TextToSpeech? = null
@ -391,11 +389,11 @@ class AppState(
val tts = tmp_tts
if (tts == null || TextToSpeech.SUCCESS != status) {
context.showToast(
false,
R.string.text_to_speech_initialize_failed,
status
)
log.d("speech initialize failed. status=%s", status)
false,
R.string.text_to_speech_initialize_failed,
status
)
log.d("speech initialize failed. status=${status}" )
return@OnInitListener
}
@ -423,15 +421,8 @@ class AppState(
} else {
val lang = defaultLocale(context).toLanguageTag()
for (v in voice_set) {
log.d(
"Voice %s %s %s",
v.name,
v.locale.toLanguageTag(),
lang
)
if (lang != v.locale.toLanguageTag()) {
continue
}
log.d( "Voice ${ v.name} ${ v.locale.toLanguageTag()} ${lang}" )
if (lang != v.locale.toLanguageTag()) continue
voice_list.add(v)
}
}
@ -443,9 +434,9 @@ class AppState(
handler.post(proc_flushSpeechQueue)
context.registerReceiver(
tts_receiver,
IntentFilter(TextToSpeech.ACTION_TTS_QUEUE_PROCESSING_COMPLETED)
)
tts_receiver,
IntentFilter(TextToSpeech.ACTION_TTS_QUEUE_PROCESSING_COMPLETED)
)
// tts.setOnUtteranceProgressListener( new UtteranceProgressListener() {
// @Override public void onStart( String utteranceId ){
@ -603,7 +594,8 @@ class AppState(
}
if (item.sound_type == HighlightWord.SOUND_TYPE_CUSTOM && item.sound_uri.mayUri()
.tryRingtone()) return
.tryRingtone()
) return
RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION).tryRingtone()
}

View File

@ -329,7 +329,7 @@ fun Column.isFiltered(item: TootNotification): Boolean {
TootNotification.TYPE_FOLLOW_REQUEST_ACCEPTED_MISSKEY -> {
val who = item.account
if (who != null && favMuteSet?.contains(access_info.getFullAcct(who)) == true) {
Column.log.d("%s is in favMuteSet.", access_info.getFullAcct(who))
Column.log.d("${access_info.getFullAcct(who)} is in favMuteSet.")
return true
}
}
@ -404,21 +404,21 @@ fun Column.checkFiltersForListData(trees: FilterTrees?) {
}
fun reloadFilter(context: Context, access_info: SavedAccount) {
launchMain{
launchMain {
var resultList: ArrayList<TootFilter>? = null
context.runApiTask(
access_info,
progressStyle = ApiTask.PROGRESS_NONE
){client->
client.request(ApiPath.PATH_FILTERS)?.also{ result->
result.jsonArray?.let{
) { client ->
client.request(ApiPath.PATH_FILTERS)?.also { result ->
result.jsonArray?.let {
resultList = TootFilter.parseList(it)
}
}
}
resultList?.let{
resultList?.let {
Column.log.d("update filters for ${access_info.acct.pretty}")
for (column in App1.getAppState(context).columnList) {
if (column.access_info == access_info) {

View File

@ -258,6 +258,7 @@ class ColumnTask_Loading(
column.saveRange(bBottom = true, bTop = true, result = result, list = src)
} else {
column.saveRangeTop(result, src)
true
}
return when {
@ -416,6 +417,7 @@ class ColumnTask_Loading(
column.saveRange(bBottom = true, bTop = true, result = result, list = src)
} else {
column.saveRangeTop(result, src)
true
}
return when {
@ -998,7 +1000,7 @@ class ColumnTask_Loading(
target_status.conversation_main = true
// 祖先
val list_asc = java.util.ArrayList<TootStatus>()
val list_asc = ArrayList<TootStatus>()
while (true) {
if (client.isApiCancelled) return null
queryParams["offset"] = list_asc.size
@ -1012,7 +1014,7 @@ class ColumnTask_Loading(
}
// 直接の子リプライ。(子孫をたどることまではしない)
val list_desc = java.util.ArrayList<TootStatus>()
val list_desc = ArrayList<TootStatus>()
val idSet = HashSet<EntityId>()
var untilId: EntityId? = null
@ -1048,7 +1050,7 @@ class ColumnTask_Loading(
}
// 一つのリストにまとめる
this.list_tmp = java.util.ArrayList<TimelineItem>(
this.list_tmp = ArrayList<TimelineItem>(
list_asc.size + list_desc.size + 2
).apply {
addAll(list_asc.sortedBy { it.time_created_at })
@ -1084,7 +1086,7 @@ class ColumnTask_Loading(
target_status.conversation_main = true
if (conversation_context != null) {
this.list_tmp = java.util.ArrayList(
this.list_tmp = ArrayList(
1
+ (conversation_context.ancestors?.size ?: 0)
+ (conversation_context.descendants?.size ?: 0)

View File

@ -103,8 +103,6 @@ class ColumnTask_Refresh(
sp = holder.scrollPosition
}
if (bBottom) {
val changeList = listOf(
AdapterChange(

View File

@ -237,21 +237,17 @@ class ColumnViewHolder(
val column = this@ColumnViewHolder.column
if (column == null) {
log.d("restoreScrollPosition [%d], column==null", page_idx)
log.d("restoreScrollPosition [${page_idx}], column==null")
return
}
if (column.is_dispose.get()) {
log.d("restoreScrollPosition [%d], column is disposed", page_idx)
log.d("restoreScrollPosition [${page_idx}], column is disposed")
return
}
if (column.hasMultipleViewHolder()) {
log.d(
"restoreScrollPosition [%d] %s , column has multiple view holder. retry later.",
page_idx,
column.getColumnName(true)
)
log.d("restoreScrollPosition [${page_idx}] ${column.getColumnName(true)}, column has multiple view holder. retry later.")
// タブレットモードでカラムを追加/削除した際に発生する。
// このタイミングでスクロール位置を復元してもうまくいかないので延期する
@ -277,30 +273,16 @@ class ColumnViewHolder(
// }
// }
log.d(
"restoreScrollPosition [$page_idx] %s , column has no saved scroll position.",
column.getColumnName(true)
)
log.d("restoreScrollPosition [$page_idx] ${column.getColumnName(true)} , column has no saved scroll position.")
return
}
column.scroll_save = null
if (listView.visibility != View.VISIBLE) {
log.d(
"restoreScrollPosition [$page_idx] %s , listView is not visible. saved position %s,%s is dropped.",
column.getColumnName(true),
sp.adapterIndex,
sp.offset
)
log.d("restoreScrollPosition [$page_idx] ${column.getColumnName(true)} , listView is not visible. saved position ${sp.adapterIndex},${sp.offset} is dropped.")
} else {
log.d(
"restoreScrollPosition [%d] %s , listView is visible. resume %s,%s",
page_idx,
column.getColumnName(true),
sp.adapterIndex,
sp.offset
)
log.d("restoreScrollPosition [${page_idx}] ${column.getColumnName(true)} , listView is visible. resume ${sp.adapterIndex},${sp.offset}")
sp.restore(this@ColumnViewHolder)
}
@ -418,7 +400,7 @@ class ColumnViewHolder(
llColumnHeader,
llRefreshError,
).forEach { it.setOnClickListener(this) }
).forEach { it.setOnClickListener(this) }
btnColumnClose.setOnLongClickListener(this)

View File

@ -53,7 +53,7 @@ fun ColumnViewHolder.loadBackgroundImage(iv: ImageView, url: String?) {
val screen_h = iv.resources.displayMetrics.heightPixels
// 非同期処理を開始
last_image_task = launchMain{
last_image_task = launchMain {
val bitmap = try {
withContext(Dispatchers.IO) {
try {
@ -93,7 +93,7 @@ fun ColumnViewHolder.onPageDestroy(page_idx: Int) {
// タブレットモードの場合、onPageCreateより前に呼ばれる
val column = this.column
if (column != null) {
ColumnViewHolder.log.d("onPageDestroy [%d] %s", page_idx, tvColumnName.text)
ColumnViewHolder.log.d("onPageDestroy [${page_idx}] ${tvColumnName.text}")
saveScrollPosition()
listView.adapter = null
column.removeColumnViewHolder(this)
@ -111,7 +111,7 @@ fun ColumnViewHolder.onPageCreate(column: Column, page_idx: Int, page_count: Int
this.column = column
this.page_idx = page_idx
ColumnViewHolder.log.d("onPageCreate [%d] %s", page_idx, column.getColumnName(true))
ColumnViewHolder.log.d("onPageCreate [${page_idx}] ${column.getColumnName(true)}")
val bSimpleList =
column.type != ColumnType.CONVERSATION && Pref.bpSimpleList(activity.pref)

View File

@ -209,22 +209,17 @@ fun ColumnViewHolder.scrollToTop2() {
fun ColumnViewHolder.saveScrollPosition(): Boolean {
val column = this.column
when {
column == null -> ColumnViewHolder.log.d("saveScrollPosition [%d] , column==null", page_idx)
column == null ->
ColumnViewHolder.log.d("saveScrollPosition [${page_idx}] , column==null")
column.is_dispose.get() -> ColumnViewHolder.log.d(
"saveScrollPosition [%d] , column is disposed",
page_idx
)
column.is_dispose.get() ->
ColumnViewHolder.log.d("saveScrollPosition [${page_idx}] , column is disposed")
listView.visibility != View.VISIBLE -> {
val scroll_save = ScrollPosition()
column.scroll_save = scroll_save
ColumnViewHolder.log.d(
"saveScrollPosition [%d] %s , listView is not visible, save %s,%s",
page_idx,
column.getColumnName(true),
scroll_save.adapterIndex,
scroll_save.offset
"saveScrollPosition [${page_idx}] ${column.getColumnName(true)} , listView is not visible, save ${scroll_save.adapterIndex},${scroll_save.offset}"
)
return true
}
@ -233,11 +228,7 @@ fun ColumnViewHolder.saveScrollPosition(): Boolean {
val scroll_save = ScrollPosition(this)
column.scroll_save = scroll_save
ColumnViewHolder.log.d(
"saveScrollPosition [%d] %s , listView is visible, save %s,%s",
page_idx,
column.getColumnName(true),
scroll_save.adapterIndex,
scroll_save.offset
"saveScrollPosition [${page_idx}] ${column.getColumnName(true)} , listView is visible, save ${scroll_save.adapterIndex},${scroll_save.offset}"
)
return true
}
@ -262,7 +253,7 @@ fun ColumnViewHolder.setScrollPosition(sp: ScrollPosition, deltaDp: Float = 0f)
listLayoutManager.scrollVerticallyBy(dy, recycler, state)
} catch (ex: Throwable) {
ColumnViewHolder.log.trace(ex)
ColumnViewHolder.log.e("can't access field in class %s", RecyclerView::class.java.simpleName)
ColumnViewHolder.log.e("can't access field in class ${RecyclerView::class.java.simpleName}")
}
}, 20L)
}

View File

@ -26,7 +26,7 @@ class EventReceiver : BroadcastReceiver() {
ACTION_NOTIFICATION_DELETE ->
PollingWorker.queueNotificationDeleted( context,intent.data)
else -> log.e("onReceive: unsupported action %s", action)
else -> log.e("onReceive: unsupported action ${action}")
}
}
}

View File

@ -11,44 +11,44 @@ import jp.juggler.subwaytooter.notification.PollingWorker
import jp.juggler.util.LogCategory
class MyFirebaseMessagingService : FirebaseMessagingService() {
companion object {
internal val log = LogCategory("MyFirebaseMessagingService")
}
override fun onMessageReceived(remoteMessage : RemoteMessage) {
super.onMessageReceived(remoteMessage)
var tag : String? = null
val data = remoteMessage.data
for((key, value) in data) {
log.d("onMessageReceived: %s=%s", key, value)
when(key){
"notification_tag" -> tag = value
"acct" -> tag= "acct<>$value"
}
}
val context = applicationContext
val intent = Intent(context, PollingForegrounder::class.java)
if(tag != null) intent.putExtra(PollingWorker.EXTRA_TAG, tag)
if(Build.VERSION.SDK_INT >= 26) {
context.startForegroundService(intent)
} else {
context.startService(intent)
}
}
override fun onNewToken(token : String) {
try {
log.d("onTokenRefresh: token=%s", token)
PrefDevice.prefDevice(this).edit().putString(PrefDevice.KEY_DEVICE_TOKEN, token).apply()
companion object {
internal val log = LogCategory("MyFirebaseMessagingService")
}
PollingWorker.queueFCMTokenUpdated(this)
} catch(ex : Throwable) {
log.trace(ex)
}
}
override fun onMessageReceived(remoteMessage: RemoteMessage) {
super.onMessageReceived(remoteMessage)
var tag: String? = null
val data = remoteMessage.data
for ((key, value) in data) {
log.d("onMessageReceived: ${key}=${value}")
when (key) {
"notification_tag" -> tag = value
"acct" -> tag = "acct<>$value"
}
}
val context = applicationContext
val intent = Intent(context, PollingForegrounder::class.java)
if (tag != null) intent.putExtra(PollingWorker.EXTRA_TAG, tag)
if (Build.VERSION.SDK_INT >= 26) {
context.startForegroundService(intent)
} else {
context.startService(intent)
}
}
override fun onNewToken(token: String) {
try {
log.d("onTokenRefresh: token=${token}")
PrefDevice.prefDevice(this).edit().putString(PrefDevice.KEY_DEVICE_TOKEN, token).apply()
PollingWorker.queueFCMTokenUpdated(this)
} catch (ex: Throwable) {
log.trace(ex)
}
}
}

View File

@ -5,33 +5,33 @@ import androidx.recyclerview.widget.RecyclerView
import jp.juggler.util.LogCategory
internal class TabletColumnViewHolder(
activity : ActMain,
parent: ViewGroup,
val columnViewHolder : ColumnViewHolder = ColumnViewHolder(activity,parent)
activity: ActMain,
parent: ViewGroup,
val columnViewHolder: ColumnViewHolder = ColumnViewHolder(activity, parent)
) : RecyclerView.ViewHolder(columnViewHolder.viewRoot) {
companion object {
val log = LogCategory("TabletColumnViewHolder")
}
private var pageIndex = - 1
fun bind(column : Column, pageIndex : Int, column_count : Int) {
log.d("bind. %d => %d ", this.pageIndex, pageIndex)
columnViewHolder.onPageDestroy(this.pageIndex)
this.pageIndex = pageIndex
columnViewHolder.onPageCreate(column, pageIndex, column_count)
if(! column.bFirstInitialized) {
column.startLoading()
}
}
fun onViewRecycled() {
log.d("onViewRecycled %d", pageIndex)
columnViewHolder.onPageDestroy(pageIndex)
}
companion object {
val log = LogCategory("TabletColumnViewHolder")
}
private var pageIndex = -1
fun bind(column: Column, pageIndex: Int, column_count: Int) {
log.d("bind. ${this.pageIndex} => ${pageIndex}")
columnViewHolder.onPageDestroy(this.pageIndex)
this.pageIndex = pageIndex
columnViewHolder.onPageCreate(column, pageIndex, column_count)
if (!column.bFirstInitialized) {
column.startLoading()
}
}
fun onViewRecycled() {
log.d("onViewRecycled ${pageIndex}")
columnViewHolder.onPageDestroy(pageIndex)
}
}

View File

@ -63,7 +63,7 @@ class UpdateRelationEnv(val column: Column) {
UserRelation.saveListMisskey(now, column.access_info.db_id, who_list, start, step)
start += step
}
Column.log.d("updateRelation: update %d relations.", end)
Column.log.d("updateRelation: update ${end} relations.")
}
// 2018/11/1 Misskeyにもリレーション取得APIができた
@ -108,7 +108,7 @@ class UpdateRelationEnv(val column: Column) {
UserRelation.saveList2(now, column.access_info.db_id, list)
}
}
Column.log.d("updateRelation: update %d relations.", n)
Column.log.d("updateRelation: update ${n} relations.")
}
@ -139,7 +139,7 @@ class UpdateRelationEnv(val column: Column) {
list
)
}
Column.log.d("updateRelation: update %d relations.", n)
Column.log.d("updateRelation: update ${n} relations.")
}
}
@ -158,7 +158,7 @@ class UpdateRelationEnv(val column: Column) {
AcctSet.saveList(now, acct_list, n, length)
n += length
}
Column.log.d("updateRelation: update %d acct.", n)
Column.log.d("updateRelation: update ${n} acct.")
}
@ -177,7 +177,7 @@ class UpdateRelationEnv(val column: Column) {
TagSet.saveList(now, tag_list, n, length)
n += length
}
Column.log.d("updateRelation: update %d tag.", n)
Column.log.d("updateRelation: update ${n} tag.")
}
}
}

View File

@ -231,7 +231,7 @@ private fun appServerUnregister(context: Context, account: SavedAccount) {
val response = call.await()
log.e("appServerUnregister: %s", response)
log.e("appServerUnregister: ${response}")
} catch (ex: Throwable) {
log.trace(ex, "appServerUnregister failed.")
}

View File

@ -132,7 +132,7 @@ private fun ActMain.conversationRemote(
val (result, status) = client.syncStatus(access_info, remote_status_url)
if (status != null) {
local_status_id = status.id
log.d("status id conversion %s => %s", remote_status_url, status.id)
log.d("status id conversion ${remote_status_url}=>${status.id}")
}
result
}

View File

@ -8,120 +8,123 @@ import jp.juggler.util.asciiPattern
import jp.juggler.util.groupEx
import java.util.*
class TootList(parser:TootParser,src : JsonObject): TimelineItem(), Comparable<TootList> {
class TootList(parser: TootParser, src: JsonObject) : TimelineItem(), Comparable<TootList> {
val id : EntityId
val id: EntityId
val title : String?
// タイトルの数字列部分は数字の大小でソートされるようにしたい
private val title_for_sort : ArrayList<Any>?
// 内部で使用する
var isRegistered : Boolean = false
var userIds :ArrayList<EntityId>? = null
init {
if( parser.serviceType == ServiceType.MISSKEY){
id = EntityId.mayDefault(src.string("id") )
title = src.string("name") ?: src.string("title") // v11,v10
this.title_for_sort = makeTitleForSort(this.title)
val user_list = ArrayList<EntityId>()
userIds = user_list
src.jsonArray("userIds")?.forEach {
val id = EntityId.mayNull( it as? String )
if(id != null ) user_list.add(id )
}
}else {
id = EntityId.mayDefault(src.string("id") )
title = src.string("title")
this.title_for_sort = makeTitleForSort(this.title)
}
}
override fun getOrderId() = id
companion object {
private var log = LogCategory("TootList")
private val reNumber = """(\d+)""".asciiPattern()
private fun makeTitleForSort(title : String?) : ArrayList<Any> {
val list = ArrayList<Any>()
if(title != null) {
val m = reNumber.matcher(title)
var last_end = 0
while(m.find()) {
val match_start = m.start()
val match_end = m.end()
if(match_start > last_end) {
list.add(title.substring(last_end, match_start))
}
try {
list.add(m.groupEx(1)!!.toLong())
} catch(ex : Throwable) {
list.clear()
list.add(title)
return list
}
last_end = match_end
}
val end = title.length
if(end > last_end) {
list.add(title.substring(last_end, end))
}
}
return list
}
private fun compareLong(a : Long, b : Long) : Int {
return a.compareTo(b)
}
private fun compareString(a : String, b : String) : Int {
return a.compareTo(b)
}
}
override fun compareTo(other : TootList) : Int {
val la = this.title_for_sort
val lb = other.title_for_sort
if(la == null) {
return if(lb == null) 0 else - 1
} else if(lb == null) {
return 1
}
val sa = la.size
val sb = lb.size
var i = 0
while(true) {
val oa = if(i >= sa) null else la[i]
val ob = if(i >= sb) null else lb[i]
if(oa == null && ob == null) return 0
val delta = when {
oa == null -> - 1
ob == null -> 1
oa is Long && ob is Long -> compareLong(oa, ob)
oa is String && ob is String -> compareString(oa, ob)
else -> (ob is Long).b2i() - (oa is Long).b2i()
}
log.d(
"%s %s %s"
, oa
, if(delta < 0) "<" else if(delta > 0) ">" else "="
, ob
)
if(delta != 0) return delta
++ i
}
}
val title: String?
// タイトルの数字列部分は数字の大小でソートされるようにしたい
private val title_for_sort: ArrayList<Any>?
// 内部で使用する
var isRegistered: Boolean = false
var userIds: ArrayList<EntityId>? = null
init {
if (parser.serviceType == ServiceType.MISSKEY) {
id = EntityId.mayDefault(src.string("id"))
title = src.string("name") ?: src.string("title") // v11,v10
this.title_for_sort = makeTitleForSort(this.title)
val user_list = ArrayList<EntityId>()
userIds = user_list
src.jsonArray("userIds")?.forEach {
val id = EntityId.mayNull(it as? String)
if (id != null) user_list.add(id)
}
} else {
id = EntityId.mayDefault(src.string("id"))
title = src.string("title")
this.title_for_sort = makeTitleForSort(this.title)
}
}
override fun getOrderId() = id
companion object {
private var log = LogCategory("TootList")
private val reNumber = """(\d+)""".asciiPattern()
private fun makeTitleForSort(title: String?): ArrayList<Any> {
val list = ArrayList<Any>()
if (title != null) {
val m = reNumber.matcher(title)
var last_end = 0
while (m.find()) {
val match_start = m.start()
val match_end = m.end()
if (match_start > last_end) {
list.add(title.substring(last_end, match_start))
}
try {
list.add(m.groupEx(1)!!.toLong())
} catch (ex: Throwable) {
list.clear()
list.add(title)
return list
}
last_end = match_end
}
val end = title.length
if (end > last_end) {
list.add(title.substring(last_end, end))
}
}
return list
}
private fun compareLong(a: Long, b: Long): Int {
return a.compareTo(b)
}
private fun compareString(a: String, b: String): Int {
return a.compareTo(b)
}
}
override fun compareTo(other: TootList): Int {
val la = this.title_for_sort
val lb = other.title_for_sort
if (la == null) {
return if (lb == null) 0 else -1
} else if (lb == null) {
return 1
}
val sa = la.size
val sb = lb.size
var i = 0
while (true) {
val oa = if (i >= sa) null else la[i]
val ob = if (i >= sb) null else lb[i]
if (oa == null && ob == null) return 0
val delta = when {
oa == null -> -1
ob == null -> 1
oa is Long && ob is Long -> compareLong(oa, ob)
oa is String && ob is String -> compareString(oa, ob)
else -> (ob is Long).b2i() - (oa is Long).b2i()
}
log.d(
"${oa} ${
when {
delta < 0 -> "<"
delta > 0 -> ">"
else -> "="
}
} ${ob}"
)
if (delta != 0) return delta
++i
}
}
}

View File

@ -4,88 +4,88 @@ import jp.juggler.subwaytooter.api.TootParser
import jp.juggler.util.*
object TootPayload {
val log = LogCategory("TootPayload")
private val reNumber = "([-]?\\d+)".asciiPattern()
// ストリーミングAPIのペイロード部分をTootStatus,TootNotification,整数IDのどれかに解釈する
fun parsePayload(
parser : TootParser,
event : String,
parent : JsonObject,
parent_text : String
) : Any? {
try {
val payload = parent["payload"] ?: return null
if(payload is JsonObject) {
return when(event) {
// ここを通るケースはまだ確認できていない
"update" -> parser.status(payload)
// ここを通るケースはまだ確認できていない
"notification" -> parser.notification(payload)
// ここを通るケースはまだ確認できていない
else -> {
log.e("unknown payload(1). message=%s", parent_text)
null
}
}
} else if(payload is JsonArray) {
log.e("unknown payload(1b). message=%s", parent_text)
return null
}
if(payload is Number) {
// 2017/8/24 18:37 mastodon.juggler.jpでここを通った
return payload.toLong()
}
if(payload is String) {
if(payload[0] == '{') {
val src = payload.decodeJsonObject()
return when(event) {
// 2017/8/24 18:37 mastodon.juggler.jpでここを通った
"update" -> parser.status(src)
// 2017/8/24 18:37 mastodon.juggler.jpでここを通った
"notification" -> parser.notification(src)
"conversation" -> parseItem(::TootConversationSummary, parser, src)
"announcement" -> parseItem(::TootAnnouncement, parser, src)
val log = LogCategory("TootPayload")
"emoji_reaction",
"announcement.reaction" -> parseItem(TootReaction::parseFedibird, src)
private val reNumber = "([-]?\\d+)".asciiPattern()
else -> {
log.e("unknown payload(2). message=%s", parent_text)
// ここを通るケースはまだ確認できていない
}
}
} else if(payload[0] == '[') {
log.e("unknown payload(2b). message=%s", parent_text)
return null
}
// 2017/8/24 18:37 mdx.ggtea.org でここを通った
val m = reNumber.matcher(payload)
if(m.find()) {
return m.groupEx(1) !!.toLong(10)
}
}
// ここを通るケースはまだ確認できていない
log.e("unknown payload(3). message=%s", parent_text)
} catch(ex : Throwable) {
log.trace(ex)
}
return null
}
// ストリーミングAPIのペイロード部分をTootStatus,TootNotification,整数IDのどれかに解釈する
fun parsePayload(
parser: TootParser,
event: String,
parent: JsonObject,
parent_text: String
): Any? {
try {
val payload = parent["payload"] ?: return null
if (payload is JsonObject) {
return when (event) {
// ここを通るケースはまだ確認できていない
"update" -> parser.status(payload)
// ここを通るケースはまだ確認できていない
"notification" -> parser.notification(payload)
// ここを通るケースはまだ確認できていない
else -> {
log.e("unknown payload(1). message=${parent_text}")
null
}
}
} else if (payload is JsonArray) {
log.e("unknown payload(1b). message=${parent_text}")
return null
}
if (payload is Number) {
// 2017/8/24 18:37 mastodon.juggler.jpでここを通った
return payload.toLong()
}
if (payload is String) {
if (payload[0] == '{') {
val src = payload.decodeJsonObject()
return when (event) {
// 2017/8/24 18:37 mastodon.juggler.jpでここを通った
"update" -> parser.status(src)
// 2017/8/24 18:37 mastodon.juggler.jpでここを通った
"notification" -> parser.notification(src)
"conversation" -> parseItem(::TootConversationSummary, parser, src)
"announcement" -> parseItem(::TootAnnouncement, parser, src)
"emoji_reaction",
"announcement.reaction" -> parseItem(TootReaction::parseFedibird, src)
else -> {
log.e("unknown payload(2). message=${parent_text}")
// ここを通るケースはまだ確認できていない
}
}
} else if (payload[0] == '[') {
log.e("unknown payload(2b). message=${parent_text}")
return null
}
// 2017/8/24 18:37 mdx.ggtea.org でここを通った
val m = reNumber.matcher(payload)
if (m.find()) {
return m.groupEx(1)!!.toLong(10)
}
}
// ここを通るケースはまだ確認できていない
log.e("unknown payload(3). message=${parent_text}")
} catch (ex: Throwable) {
log.trace(ex)
}
return null
}
}

View File

@ -951,24 +951,24 @@ class TootStatus(parser: TootParser, src: JsonObject) : TimelineItem() {
return list
}
fun updateReactionMastodon( newReactionSet: TootReactionSet ) {
fun updateReactionMastodon(newReactionSet: TootReactionSet) {
synchronized(this) {
this.reactionSet = newReactionSet
}
}
fun updateReactionMastodonByEvent( newReaction: TootReaction ) {
fun updateReactionMastodonByEvent(newReaction: TootReaction) {
synchronized(this) {
var reactionSet = this.reactionSet
if( newReaction.count <= 0 ){
reactionSet?.get(newReaction.name)?.let{ reactionSet?.remove(it) }
}else{
if (newReaction.count <= 0) {
reactionSet?.get(newReaction.name)?.let { reactionSet?.remove(it) }
} else {
if (reactionSet == null) {
reactionSet = TootReactionSet(isMisskey = false)
this.reactionSet = reactionSet
}
when(val old = reactionSet[newReaction.name]) {
when (val old = reactionSet[newReaction.name]) {
null -> reactionSet.add(newReaction)
// 同一オブジェクトならマージは不要
@ -1007,7 +1007,7 @@ class TootStatus(parser: TootParser, src: JsonObject) : TimelineItem() {
if (byMe) {
// 自分でリアクションしたらUIで更新した後にストリーミングイベントが届くことがある
// その場合はカウントを変更しない
if(reactionSet.any{ it.me && it.name == code}) return false
if (reactionSet.any { it.me && it.name == code }) return false
}
log.d("increaseReaction noteId=$id byMe=$byMe caller=$caller")
@ -1017,7 +1017,7 @@ class TootStatus(parser: TootParser, src: JsonObject) : TimelineItem() {
reactionSet[code]?.also { it.count = max(0, it.count + 1L) }
?: TootReaction(name = code, count = 1L).also { reactionSet.add(it) }
if(byMe) reaction.me = true
if (byMe) reaction.me = true
return true
}
@ -1037,7 +1037,7 @@ class TootStatus(parser: TootParser, src: JsonObject) : TimelineItem() {
if (byMe) {
// 自分でリアクションしたらUIで更新した後にストリーミングイベントが届くことがある
// その場合はカウントを変更しない
if(reactionSet.any{ !it.me && it.name == code}) return false
if (reactionSet.any { !it.me && it.name == code }) return false
}
log.d("decreaseReaction noteId=$id byMe=$byMe caller=$caller")
@ -1046,7 +1046,7 @@ class TootStatus(parser: TootParser, src: JsonObject) : TimelineItem() {
val reaction = reactionSet[code]
?.also { it.count = max(0L, it.count - 1L) }
if(byMe) reaction?.me = false
if (byMe) reaction?.me = false
return true
}
@ -1210,10 +1210,10 @@ class TootStatus(parser: TootParser, src: JsonObject) : TimelineItem() {
m = reDate.matcher(strTime)
if (m.find()) return parseTime("${strTime}T00:00:00.000Z")
log.w("invalid time format: %s", strTime)
log.w("invalid time format: ${strTime}")
} catch (ex: Throwable) { // ParseException, ArrayIndexOutOfBoundsException
log.trace(ex)
log.e(ex, "TootStatus.parseTime failed. src=%s", strTime)
log.e(ex, "TootStatus.parseTime failed. src=$strTime")
}
}
return 0L
@ -1224,7 +1224,7 @@ class TootStatus(parser: TootParser, src: JsonObject) : TimelineItem() {
try {
val m = reMSPTime.matcher(strTime)
if (!m.find()) {
log.d("invalid time format: %s", strTime)
log.d("invalid time format: $strTime")
} else {
val g = GregorianCalendar(tz_utc)
g.set(
@ -1240,7 +1240,7 @@ class TootStatus(parser: TootParser, src: JsonObject) : TimelineItem() {
}
} catch (ex: Throwable) { // ParseException, ArrayIndexOutOfBoundsException
log.trace(ex)
log.e(ex, "parseTimeMSP failed. src=%s", strTime)
log.e(ex, "parseTimeMSP failed. src=${strTime}" )
}
}

View File

@ -103,7 +103,7 @@ class PollingForegrounder : IntentService("PollingForegrounder") {
if (sv.isEmpty() || sv == last_status) return@handleFCMMessage
// 状況が変化したらログと通知領域に出力する
last_status = sv
log.d("onStatus %s", sv)
log.d("onStatus $sv")
startForeground(NOTIFICATION_ID_FOREGROUNDER, createNotification(context, sv))
}
}

View File

@ -357,10 +357,7 @@ class PollingWorker private constructor(contextArg: Context) {
// ジョブが完了した?
val now = SystemClock.elapsedRealtime()
if (!pw.hasJob(JobId.Push)) {
log.d(
"handleFCMMessage: JOB_FCM completed. time=%.2f",
(now - time_start) / 1000f
)
log.d("handleFCMMessage: JOB_FCM completed. time=${String.format("%.2f", (now - time_start) / 1000f)}")
break
}
@ -398,7 +395,7 @@ class PollingWorker private constructor(contextArg: Context) {
private val workerNotifier = Channel<Unit>(capacity = Channel.CONFLATED)
fun notifyWorker() =
workerNotifier.trySend(Unit)
workerNotifier.trySend(Unit)
init {
log.d("init")

View File

@ -77,7 +77,7 @@ object NotestockHelper {
parseList(parser, data)
.also {
if (it.isEmpty())
log.d("search result is empty. %s", result.bodyString)
log.d("search result is empty. ${result.bodyString}")
}
)

View File

@ -72,7 +72,7 @@ object TootsearchHelper {
parseList(parser, root)
.also {
if (it.isEmpty())
log.d("search result is empty. %s", result.bodyString)
log.d("search result is empty. ${result.bodyString}")
}
)
}

View File

@ -164,12 +164,7 @@ class AcctColor {
log.e(ex, "load failed.")
}
log.d(
"lruCache size=%s,hit=%s,miss=%s",
mMemoryCache.size(),
mMemoryCache.hitCount(),
mMemoryCache.missCount()
)
log.d("lruCache size=${mMemoryCache.size()},hit=${mMemoryCache.hitCount()},miss=${mMemoryCache.missCount()}")
val ac = AcctColor(key, acctPretty)
mMemoryCache.put(key, ac)
return ac

View File

@ -69,14 +69,7 @@ class NotificationTracking {
post_id.putTo(cv, COL_POST_ID)
cv.put(COL_POST_TIME, post_time)
val rows = App1.database.update(table, cv, WHERE_AID, arrayOf(account_db_id.toString(),notificationType))
log.d(
"updatePost account_db_id=%s, nt=%s, post=%s,%s update_rows=%s"
, account_db_id
, notificationType
, post_id
, post_time
, rows
)
log.d("updatePost account_db_id=${account_db_id}, nt=${notificationType}, post=${post_id},${post_time} update_rows=${rows}")
dirty=false
clearCache(account_db_id,notificationType)
} catch(ex : Throwable) {

View File

@ -11,166 +11,166 @@ import jp.juggler.util.digestSHA256Hex
import jp.juggler.util.decodeJsonObject
class PostDraft {
var id : Long = 0
var time_save : Long = 0
var json : JsonObject? = null
var hash : String? = null
class ColIdx(cursor : Cursor) {
internal val idx_id : Int
internal val idx_time_save : Int
internal val idx_json : Int
internal val idx_hash : Int
init {
idx_id = cursor.getColumnIndex(COL_ID)
idx_time_save = cursor.getColumnIndex(COL_TIME_SAVE)
idx_json = cursor.getColumnIndex(COL_JSON)
idx_hash = cursor.getColumnIndex(COL_HASH)
}
}
fun delete() {
try {
App1.database.delete(table, "$COL_ID=?", arrayOf(id.toString()))
} catch(ex : Throwable) {
log.e(ex, "delete failed.")
}
}
companion object : TableCompanion {
private val log = LogCategory("PostDraft")
private const val table = "post_draft"
private const val COL_ID = BaseColumns._ID
private const val COL_TIME_SAVE = "time_save"
private const val COL_JSON = "json"
private const val COL_HASH = "hash"
override fun onDBCreate(db : SQLiteDatabase) {
log.d("onDBCreate!")
db.execSQL(
"create table if not exists " + table
+ "(" + COL_ID + " INTEGER PRIMARY KEY"
+ "," + COL_TIME_SAVE + " integer not null"
+ "," + COL_JSON + " text not null"
+ "," + COL_HASH + " text not null"
+ ")"
)
db.execSQL(
"create unique index if not exists " + table + "_hash on " + table + "(" + COL_HASH + ")"
)
db.execSQL(
"create index if not exists " + table + "_time on " + table + "(" + COL_TIME_SAVE + ")"
)
}
override fun onDBUpgrade(db : SQLiteDatabase, oldVersion : Int, newVersion : Int) {
if(oldVersion < 12 && newVersion >= 12) {
onDBCreate(db)
}
}
private fun deleteOld(now : Long) {
try {
// 古いデータを掃除する
val expire = now - 86400000L * 30
App1.database.delete(table, "$COL_TIME_SAVE<?", arrayOf(expire.toString()))
} catch(ex : Throwable) {
log.e(ex, "deleteOld failed.")
}
}
fun save(now : Long, json : JsonObject) {
deleteOld(now)
try {
// make hash
val sb = StringBuilder()
json.keys.toMutableList().apply { sort() }.forEach { k ->
val v = json[k]?.toString() ?: "(null)"
sb.append("&")
sb.append(k)
sb.append("=")
sb.append(v)
}
val hash = sb.toString().digestSHA256Hex()
// save to db
App1.database.replace(table, null, ContentValues().apply {
put(COL_TIME_SAVE, now)
put(COL_JSON, json.toString())
put(COL_HASH, hash)
})
} catch(ex : Throwable) {
log.e(ex, "save failed.")
}
}
fun hasDraft() : Boolean {
try {
App1.database.query(table, arrayOf("count(*)"), null, null, null, null, null)
.use { cursor ->
if(cursor.moveToNext()) {
val count = cursor.getInt(0)
return count > 0
}
}
} catch(ex : Throwable) {
log.trace(ex)
log.e(ex, "hasDraft failed.")
}
return false
}
fun createCursor() : Cursor? {
try {
return App1.database.query(
table,
null,
null,
null,
null,
null,
"$COL_TIME_SAVE desc"
)
} catch(ex : Throwable) {
log.trace(ex)
log.e(ex, "createCursor failed.")
}
return null
}
fun loadFromCursor(cursor : Cursor, colIdxArg : ColIdx?, position : Int) : PostDraft? {
val colIdx = colIdxArg ?: ColIdx(cursor)
if(! cursor.moveToPosition(position)) {
log.d("loadFromCursor: move failed. position=%s", position)
return null
}
val dst = PostDraft()
dst.id = cursor.getLong(colIdx.idx_id)
dst.time_save = cursor.getLong(colIdx.idx_time_save)
try {
dst.json = cursor.getString(colIdx.idx_json).decodeJsonObject()
} catch(ex : Throwable) {
log.trace(ex)
dst.json = JsonObject()
}
dst.hash = cursor.getString(colIdx.idx_hash)
return dst
}
}
var id: Long = 0
var time_save: Long = 0
var json: JsonObject? = null
var hash: String? = null
class ColIdx(cursor: Cursor) {
internal val idx_id: Int
internal val idx_time_save: Int
internal val idx_json: Int
internal val idx_hash: Int
init {
idx_id = cursor.getColumnIndex(COL_ID)
idx_time_save = cursor.getColumnIndex(COL_TIME_SAVE)
idx_json = cursor.getColumnIndex(COL_JSON)
idx_hash = cursor.getColumnIndex(COL_HASH)
}
}
fun delete() {
try {
App1.database.delete(table, "$COL_ID=?", arrayOf(id.toString()))
} catch (ex: Throwable) {
log.e(ex, "delete failed.")
}
}
companion object : TableCompanion {
private val log = LogCategory("PostDraft")
private const val table = "post_draft"
private const val COL_ID = BaseColumns._ID
private const val COL_TIME_SAVE = "time_save"
private const val COL_JSON = "json"
private const val COL_HASH = "hash"
override fun onDBCreate(db: SQLiteDatabase) {
log.d("onDBCreate!")
db.execSQL(
"create table if not exists " + table
+ "(" + COL_ID + " INTEGER PRIMARY KEY"
+ "," + COL_TIME_SAVE + " integer not null"
+ "," + COL_JSON + " text not null"
+ "," + COL_HASH + " text not null"
+ ")"
)
db.execSQL(
"create unique index if not exists " + table + "_hash on " + table + "(" + COL_HASH + ")"
)
db.execSQL(
"create index if not exists " + table + "_time on " + table + "(" + COL_TIME_SAVE + ")"
)
}
override fun onDBUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
if (oldVersion < 12 && newVersion >= 12) {
onDBCreate(db)
}
}
private fun deleteOld(now: Long) {
try {
// 古いデータを掃除する
val expire = now - 86400000L * 30
App1.database.delete(table, "$COL_TIME_SAVE<?", arrayOf(expire.toString()))
} catch (ex: Throwable) {
log.e(ex, "deleteOld failed.")
}
}
fun save(now: Long, json: JsonObject) {
deleteOld(now)
try {
// make hash
val sb = StringBuilder()
json.keys.toMutableList().apply { sort() }.forEach { k ->
val v = json[k]?.toString() ?: "(null)"
sb.append("&")
sb.append(k)
sb.append("=")
sb.append(v)
}
val hash = sb.toString().digestSHA256Hex()
// save to db
App1.database.replace(table, null, ContentValues().apply {
put(COL_TIME_SAVE, now)
put(COL_JSON, json.toString())
put(COL_HASH, hash)
})
} catch (ex: Throwable) {
log.e(ex, "save failed.")
}
}
fun hasDraft(): Boolean {
try {
App1.database.query(table, arrayOf("count(*)"), null, null, null, null, null)
.use { cursor ->
if (cursor.moveToNext()) {
val count = cursor.getInt(0)
return count > 0
}
}
} catch (ex: Throwable) {
log.trace(ex)
log.e(ex, "hasDraft failed.")
}
return false
}
fun createCursor(): Cursor? {
try {
return App1.database.query(
table,
null,
null,
null,
null,
null,
"$COL_TIME_SAVE desc"
)
} catch (ex: Throwable) {
log.trace(ex)
log.e(ex, "createCursor failed.")
}
return null
}
fun loadFromCursor(cursor: Cursor, colIdxArg: ColIdx?, position: Int): PostDraft? {
val colIdx = colIdxArg ?: ColIdx(cursor)
if (!cursor.moveToPosition(position)) {
log.d("loadFromCursor: move failed. position=${position}")
return null
}
val dst = PostDraft()
dst.id = cursor.getLong(colIdx.idx_id)
dst.time_save = cursor.getLong(colIdx.idx_time_save)
try {
dst.json = cursor.getString(colIdx.idx_json).decodeJsonObject()
} catch (ex: Throwable) {
log.trace(ex)
dst.json = JsonObject()
}
dst.hash = cursor.getString(colIdx.idx_hash)
return dst
}
}
}

View File

@ -26,8 +26,8 @@ import java.util.concurrent.TimeUnit
import kotlin.math.ceil
class CustomEmojiCache(
val context: Context,
private val handler: Handler
val context: Context,
private val handler: Handler
) {
companion object {
@ -48,10 +48,10 @@ class CustomEmojiCache(
}
private class DbCache(
val id: Long,
val timeUsed: Long,
val data: ByteArray
) {
val id: Long,
val timeUsed: Long,
val data: ByteArray
) {
companion object : TableCompanion {
@ -65,48 +65,48 @@ class CustomEmojiCache(
override fun onDBCreate(db: SQLiteDatabase) {
db.execSQL(
"""create table if not exists $table
"""create table if not exists $table
($COL_ID INTEGER PRIMARY KEY
,$COL_TIME_SAVE integer not null
,$COL_TIME_USED integer not null
,$COL_URL text not null
,$COL_DATA blob not null
)""".trimIndent()
)
)
db.execSQL("create unique index if not exists ${table}_url on ${table}($COL_URL)")
db.execSQL("create index if not exists ${table}_old on ${table}($COL_TIME_USED)")
}
override fun onDBUpgrade(
db: SQLiteDatabase,
oldVersion: Int,
newVersion: Int
) {
db: SQLiteDatabase,
oldVersion: Int,
newVersion: Int
) {
}
fun load(db: SQLiteDatabase, url: String, now: Long) =
db.rawQuery(
"select $COL_ID,$COL_TIME_USED,$COL_DATA from $table where $COL_URL=?",
arrayOf(url)
)?.use { cursor ->
"select $COL_ID,$COL_TIME_USED,$COL_DATA from $table where $COL_URL=?",
arrayOf(url)
)?.use { cursor ->
if (cursor.count == 0)
null
else {
cursor.moveToNext()
DbCache(
id = cursor.getLong(cursor.getColumnIndex(COL_ID)),
timeUsed = cursor.getLong(cursor.getColumnIndex(COL_TIME_USED)),
data = cursor.getBlob(cursor.getColumnIndex(COL_DATA))
).apply {
id = cursor.getLong(cursor.getColumnIndex(COL_ID)),
timeUsed = cursor.getLong(cursor.getColumnIndex(COL_TIME_USED)),
data = cursor.getBlob(cursor.getColumnIndex(COL_DATA))
).apply {
if (now - timeUsed >= 5 * 3600000L) {
db.update(
table,
ContentValues().apply {
put(COL_TIME_USED, now)
},
"$COL_ID=?",
arrayOf(id.toString())
)
table,
ContentValues().apply {
put(COL_TIME_USED, now)
},
"$COL_ID=?",
arrayOf(id.toString())
)
}
}
}
@ -115,23 +115,23 @@ class CustomEmojiCache(
fun sweep(db: SQLiteDatabase, now: Long) {
val expire = now - TimeUnit.DAYS.toMillis(30)
db.delete(
table,
"$COL_TIME_USED < ?",
arrayOf(expire.toString())
)
table,
"$COL_TIME_USED < ?",
arrayOf(expire.toString())
)
}
fun update(db: SQLiteDatabase, url: String, data: ByteArray) {
val now = System.currentTimeMillis()
db.replace(table,
null,
ContentValues().apply {
put(COL_URL, url)
put(COL_DATA, data)
put(COL_TIME_USED, now)
put(COL_TIME_SAVE, now)
}
)
null,
ContentValues().apply {
put(COL_URL, url)
put(COL_DATA, data)
put(COL_TIME_USED, now)
put(COL_TIME_SAVE, now)
}
)
}
}
}
@ -166,10 +166,10 @@ class CustomEmojiCache(
}
private class Request(
val refTarget: WeakReference<Any>,
val url: String,
val onLoadComplete: () -> Unit
)
val refTarget: WeakReference<Any>,
val url: String,
val onLoadComplete: () -> Unit
)
// APNGデコード済のキャッシュデータ
private val cache = ConcurrentHashMap<String, CacheItem>()
@ -253,10 +253,10 @@ class CustomEmojiCache(
}
fun getFrames(
refDrawTarget: WeakReference<Any>?,
url: String,
onLoadComplete: () -> Unit
): ApngFrames? {
refDrawTarget: WeakReference<Any>?,
url: String,
onLoadComplete: () -> Unit
): ApngFrames? {
try {
if (refDrawTarget?.get() == null) {
log.e("draw: DrawTarget is null ")
@ -353,13 +353,7 @@ class CustomEmojiCache(
if (cache_used) continue
if (DEBUG)
log.d(
"start get image. queue_size=%d, cache_size=%d url=%s",
queue_size,
cache_size,
request.url
)
if (DEBUG) log.d("start get image. queue_size=${queue_size}, cache_size=${cache_size} url=${request.url}")
val now = System.currentTimeMillis()
@ -377,8 +371,7 @@ class CustomEmojiCache(
data = try {
App1.getHttpCached(request.url)
} catch (ex: Throwable) {
log.e("get failed. url=%s", request.url)
log.trace(ex)
log.trace(ex, "get failed. url=${request.url}")
null
}
te = elapsedTime
@ -473,7 +466,7 @@ class CustomEmojiCache(
// fall thru
} catch (ex: Throwable) {
if (DEBUG) log.trace(ex)
log.e(ex, "PNG decode failed. %s ", url)
log.e(ex, "PNG decode failed. $url ")
}
// 通常のビットマップでのロードを試みる
@ -483,12 +476,12 @@ class CustomEmojiCache(
if (DEBUG) log.d("bitmap decoded.")
return ApngFrames(b)
} else {
log.e("Bitmap decode returns null. %s", url)
log.e("Bitmap decode returns null. $url")
}
// fall thru
} catch (ex: Throwable) {
log.e(ex, "Bitmap decode failed. %s", url)
log.e(ex, "Bitmap decode failed. $url")
}
// SVGのロードを試みる
@ -501,7 +494,7 @@ class CustomEmojiCache(
// fall thru
} catch (ex: Throwable) {
log.e(ex, "SVG decode failed. %s", url)
log.e(ex, "SVG decode failed. $url")
}
return null
@ -510,9 +503,9 @@ class CustomEmojiCache(
private val options = BitmapFactory.Options()
private fun decodeBitmap(
data: ByteArray,
@Suppress("SameParameterValue") pixel_max: Int
): Bitmap? {
data: ByteArray,
@Suppress("SameParameterValue") pixel_max: Int
): Bitmap? {
options.inJustDecodeBounds = true
options.inScaled = false
options.outWidth = 0
@ -536,10 +529,10 @@ class CustomEmojiCache(
}
private fun decodeSVG(
url: String,
data: ByteArray,
@Suppress("SameParameterValue") pixelMax: Float
): Bitmap? {
url: String,
data: ByteArray,
@Suppress("SameParameterValue") pixelMax: Float
): Bitmap? {
try {
val svg = SVG.getFromInputStream(ByteArrayInputStream(data))
@ -572,13 +565,13 @@ class CustomEmojiCache(
val canvas = Canvas(b)
svg.renderToCanvas(
canvas,
if (aspect >= 1f) {
RectF(0f, h_ceil - dst_h, dst_w, dst_h) // 後半はw,hを指定する
} else {
RectF(w_ceil - dst_w, 0f, dst_w, dst_h) // 後半はw,hを指定する
}
)
canvas,
if (aspect >= 1f) {
RectF(0f, h_ceil - dst_h, dst_w, dst_h) // 後半はw,hを指定する
} else {
RectF(w_ceil - dst_w, 0f, dst_w, dst_h) // 後半はw,hを指定する
}
)
return b
} catch (ex: Throwable) {
log.e(ex, "decodeSVG failed. $url")

View File

@ -13,302 +13,302 @@ import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentLinkedQueue
class CustomEmojiLister(
val context : Context,
private val handler : Handler
val context: Context,
private val handler: Handler
) {
companion object {
private val log = LogCategory("CustomEmojiLister")
internal const val CACHE_MAX = 50
internal const val ERROR_EXPIRE = 60000L * 5
private val elapsedTime : Long
get() = SystemClock.elapsedRealtime()
}
internal class CacheItem(
val key : String,
var list : ArrayList<CustomEmoji>? = null,
var listWithAliases : ArrayList<CustomEmoji>? = null,
// ロードした時刻
var time_update : Long = elapsedTime,
// 参照された時刻
var time_used : Long = time_update
)
internal class Request(
val accessInfo : SavedAccount,
val reportWithAliases : Boolean = false,
val onListLoaded : (list : ArrayList<CustomEmoji>) -> Unit?
)
// 成功キャッシュ
internal val cache = ConcurrentHashMap<String, CacheItem>()
// エラーキャッシュ
internal val cache_error = ConcurrentHashMap<String, Long>()
private val cache_error_item = CacheItem("error")
// ロード要求
internal val queue = ConcurrentLinkedQueue<Request>()
private val worker : Worker
init {
this.worker = Worker()
}
// ネットワーク接続が変化したらエラーキャッシュをクリア
fun onNetworkChanged() {
cache_error.clear()
}
private fun getCached(now : Long, accessInfo : SavedAccount) : CacheItem? {
val host = accessInfo.apiHost.ascii
// 成功キャッシュ
val item = cache[host]
if(item != null && now - item.time_update <= ERROR_EXPIRE) {
item.time_used = now
return item
}
// エラーキャッシュ
val time_error = cache_error[host]
if(time_error != null && now < time_error + ERROR_EXPIRE) {
return cache_error_item
}
return null
}
fun getList(
accessInfo : SavedAccount,
onListLoaded : (list : ArrayList<CustomEmoji>) -> Unit
) : ArrayList<CustomEmoji>? {
try {
synchronized(cache) {
val item = getCached(elapsedTime, accessInfo)
if(item != null) return item.list
}
queue.add(Request(accessInfo, onListLoaded = onListLoaded))
worker.notifyEx()
} catch(ex : Throwable) {
log.trace(ex)
}
return null
}
fun getListWithAliases(
accessInfo : SavedAccount,
onListLoaded : (list : ArrayList<CustomEmoji>) -> Unit
) : ArrayList<CustomEmoji>? {
try {
synchronized(cache) {
val item = getCached(elapsedTime, accessInfo)
if(item != null) return item.listWithAliases
}
queue.add(
Request(
accessInfo,
reportWithAliases = true,
onListLoaded = onListLoaded
)
)
worker.notifyEx()
} catch(ex : Throwable) {
log.trace(ex)
}
return null
}
fun getMap(accessInfo : SavedAccount) : HashMap<String, CustomEmoji>? {
val list = getList(accessInfo) {
// 遅延ロード非対応
} ?: return null
//
val dst = HashMap<String, CustomEmoji>()
for(e in list) {
dst[e.shortcode] = e
}
return dst
}
private inner class Worker : WorkerBase() {
override fun cancel() {
// このスレッドはキャンセルされない。プロセスが生きている限り動き続ける。
}
override suspend fun run() {
while(true) {
try {
// リクエストを取得する
val request = queue.poll()
if(request == null) {
// なければ待機
waitEx(86400000L)
continue
}
val cached = synchronized(cache) {
val item = getCached(elapsedTime, request.accessInfo)
return@synchronized if(item != null) {
val list = item.list
val listWithAliases = item.listWithAliases
if(list != null && listWithAliases != null) {
fireCallback(request, list, listWithAliases)
}
true
} else {
// キャッシュにはなかった
sweep_cache()
false
}
}
if(cached) continue
val accessInfo = request.accessInfo
val cacheKey = accessInfo.apiHost.ascii
var list : ArrayList<CustomEmoji>? = null
var listWithAlias : ArrayList<CustomEmoji>? = null
try {
val data = if(accessInfo.isMisskey) {
App1.getHttpCachedString(
"https://${cacheKey}/api/meta",
accessInfo = accessInfo
) { builder ->
builder.post(JsonObject().toRequestBody())
}
} else {
App1.getHttpCachedString(
"https://${cacheKey}/api/v1/custom_emojis",
accessInfo = accessInfo
)
}
if(data != null) {
val a = decodeEmojiList(data, accessInfo)
list = a
listWithAlias = makeListWithAlias(a)
}
} catch(ex : Throwable) {
log.trace(ex)
}
synchronized(cache) {
val now = elapsedTime
if(list == null || listWithAlias == null) {
cache_error.put(cacheKey, now)
} else {
var item : CacheItem? = cache[cacheKey]
if(item == null) {
item = CacheItem(cacheKey, list, listWithAlias)
cache[cacheKey] = item
} else {
item.list = list
item.listWithAliases = listWithAlias
item.time_update = now
}
fireCallback(request, list, listWithAlias)
}
}
} catch(ex : Throwable) {
log.trace(ex)
waitEx(3000L)
}
}
}
private fun fireCallback(
request : Request,
list : ArrayList<CustomEmoji>,
listWithAliases : ArrayList<CustomEmoji>
) {
handler.post {
request.onListLoaded(
if(request.reportWithAliases) {
listWithAliases
} else {
list
}
)
}
}
// キャッシュの掃除
private fun sweep_cache() {
// 超過してる数
val over = cache.size - CACHE_MAX
if(over <= 0) return
// 古い要素を一時リストに集める
val now = elapsedTime
val list = ArrayList<CacheItem>(over)
for(item in cache.values) {
if(now - item.time_used > 1000L) list.add(item)
}
// 昇順ソート
list.sortBy { it.time_used }
// 古い物から順に捨てる
var removed = 0
for(item in list) {
cache.remove(item.key)
if(++ removed >= over) break
}
}
private fun decodeEmojiList(
data : String,
accessInfo : SavedAccount
) : ArrayList<CustomEmoji>? {
return try {
val list = if(accessInfo.isMisskey) {
parseList(
CustomEmoji.decodeMisskey,
accessInfo.apDomain,
data.decodeJsonObject().jsonArray("emojis")
)
} else {
parseList(
CustomEmoji.decode,
accessInfo.apDomain,
data.decodeJsonArray()
)
}
list.sortWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.shortcode })
list
} catch(ex : Throwable) {
log.e(ex, "decodeEmojiList failed. instance=%s", accessInfo.apiHost.ascii)
null
}
}
private fun makeListWithAlias(list : ArrayList<CustomEmoji>?) : ArrayList<CustomEmoji> {
val dst = ArrayList<CustomEmoji>()
if(list != null) {
dst.addAll(list)
for(item in list) {
val aliases = item.aliases ?: continue
for(alias in aliases) {
if(alias.equals(item.shortcode, ignoreCase = true)) continue
dst.add(item.makeAlias(alias))
}
}
dst.sortWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.alias ?: it.shortcode })
}
return dst
}
}
companion object {
private val log = LogCategory("CustomEmojiLister")
internal const val CACHE_MAX = 50
internal const val ERROR_EXPIRE = 60000L * 5
private val elapsedTime: Long
get() = SystemClock.elapsedRealtime()
}
internal class CacheItem(
val key: String,
var list: ArrayList<CustomEmoji>? = null,
var listWithAliases: ArrayList<CustomEmoji>? = null,
// ロードした時刻
var time_update: Long = elapsedTime,
// 参照された時刻
var time_used: Long = time_update
)
internal class Request(
val accessInfo: SavedAccount,
val reportWithAliases: Boolean = false,
val onListLoaded: (list: ArrayList<CustomEmoji>) -> Unit?
)
// 成功キャッシュ
internal val cache = ConcurrentHashMap<String, CacheItem>()
// エラーキャッシュ
internal val cache_error = ConcurrentHashMap<String, Long>()
private val cache_error_item = CacheItem("error")
// ロード要求
internal val queue = ConcurrentLinkedQueue<Request>()
private val worker: Worker
init {
this.worker = Worker()
}
// ネットワーク接続が変化したらエラーキャッシュをクリア
fun onNetworkChanged() {
cache_error.clear()
}
private fun getCached(now: Long, accessInfo: SavedAccount): CacheItem? {
val host = accessInfo.apiHost.ascii
// 成功キャッシュ
val item = cache[host]
if (item != null && now - item.time_update <= ERROR_EXPIRE) {
item.time_used = now
return item
}
// エラーキャッシュ
val time_error = cache_error[host]
if (time_error != null && now < time_error + ERROR_EXPIRE) {
return cache_error_item
}
return null
}
fun getList(
accessInfo: SavedAccount,
onListLoaded: (list: ArrayList<CustomEmoji>) -> Unit
): ArrayList<CustomEmoji>? {
try {
synchronized(cache) {
val item = getCached(elapsedTime, accessInfo)
if (item != null) return item.list
}
queue.add(Request(accessInfo, onListLoaded = onListLoaded))
worker.notifyEx()
} catch (ex: Throwable) {
log.trace(ex)
}
return null
}
fun getListWithAliases(
accessInfo: SavedAccount,
onListLoaded: (list: ArrayList<CustomEmoji>) -> Unit
): ArrayList<CustomEmoji>? {
try {
synchronized(cache) {
val item = getCached(elapsedTime, accessInfo)
if (item != null) return item.listWithAliases
}
queue.add(
Request(
accessInfo,
reportWithAliases = true,
onListLoaded = onListLoaded
)
)
worker.notifyEx()
} catch (ex: Throwable) {
log.trace(ex)
}
return null
}
fun getMap(accessInfo: SavedAccount): HashMap<String, CustomEmoji>? {
val list = getList(accessInfo) {
// 遅延ロード非対応
} ?: return null
//
val dst = HashMap<String, CustomEmoji>()
for (e in list) {
dst[e.shortcode] = e
}
return dst
}
private inner class Worker : WorkerBase() {
override fun cancel() {
// このスレッドはキャンセルされない。プロセスが生きている限り動き続ける。
}
override suspend fun run() {
while (true) {
try {
// リクエストを取得する
val request = queue.poll()
if (request == null) {
// なければ待機
waitEx(86400000L)
continue
}
val cached = synchronized(cache) {
val item = getCached(elapsedTime, request.accessInfo)
return@synchronized if (item != null) {
val list = item.list
val listWithAliases = item.listWithAliases
if (list != null && listWithAliases != null) {
fireCallback(request, list, listWithAliases)
}
true
} else {
// キャッシュにはなかった
sweep_cache()
false
}
}
if (cached) continue
val accessInfo = request.accessInfo
val cacheKey = accessInfo.apiHost.ascii
var list: ArrayList<CustomEmoji>? = null
var listWithAlias: ArrayList<CustomEmoji>? = null
try {
val data = if (accessInfo.isMisskey) {
App1.getHttpCachedString(
"https://${cacheKey}/api/meta",
accessInfo = accessInfo
) { builder ->
builder.post(JsonObject().toRequestBody())
}
} else {
App1.getHttpCachedString(
"https://${cacheKey}/api/v1/custom_emojis",
accessInfo = accessInfo
)
}
if (data != null) {
val a = decodeEmojiList(data, accessInfo)
list = a
listWithAlias = makeListWithAlias(a)
}
} catch (ex: Throwable) {
log.trace(ex)
}
synchronized(cache) {
val now = elapsedTime
if (list == null || listWithAlias == null) {
cache_error.put(cacheKey, now)
} else {
var item: CacheItem? = cache[cacheKey]
if (item == null) {
item = CacheItem(cacheKey, list, listWithAlias)
cache[cacheKey] = item
} else {
item.list = list
item.listWithAliases = listWithAlias
item.time_update = now
}
fireCallback(request, list, listWithAlias)
}
}
} catch (ex: Throwable) {
log.trace(ex)
waitEx(3000L)
}
}
}
private fun fireCallback(
request: Request,
list: ArrayList<CustomEmoji>,
listWithAliases: ArrayList<CustomEmoji>
) {
handler.post {
request.onListLoaded(
if (request.reportWithAliases) {
listWithAliases
} else {
list
}
)
}
}
// キャッシュの掃除
private fun sweep_cache() {
// 超過してる数
val over = cache.size - CACHE_MAX
if (over <= 0) return
// 古い要素を一時リストに集める
val now = elapsedTime
val list = ArrayList<CacheItem>(over)
for (item in cache.values) {
if (now - item.time_used > 1000L) list.add(item)
}
// 昇順ソート
list.sortBy { it.time_used }
// 古い物から順に捨てる
var removed = 0
for (item in list) {
cache.remove(item.key)
if (++removed >= over) break
}
}
private fun decodeEmojiList(
data: String,
accessInfo: SavedAccount
): ArrayList<CustomEmoji>? {
return try {
val list = if (accessInfo.isMisskey) {
parseList(
CustomEmoji.decodeMisskey,
accessInfo.apDomain,
data.decodeJsonObject().jsonArray("emojis")
)
} else {
parseList(
CustomEmoji.decode,
accessInfo.apDomain,
data.decodeJsonArray()
)
}
list.sortWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.shortcode })
list
} catch (ex: Throwable) {
log.e(ex, "decodeEmojiList failed. instance=${accessInfo.apiHost.ascii}")
null
}
}
private fun makeListWithAlias(list: ArrayList<CustomEmoji>?): ArrayList<CustomEmoji> {
val dst = ArrayList<CustomEmoji>()
if (list != null) {
dst.addAll(list)
for (item in list) {
val aliases = item.aliases ?: continue
for (alias in aliases) {
if (alias.equals(item.shortcode, ignoreCase = true)) continue
dst.add(item.makeAlias(alias))
}
}
dst.sortWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.alias ?: it.shortcode })
}
return dst
}
}
}

View File

@ -350,7 +350,7 @@ class PostHelper(
if (visibility == checkVisibility && !checkFun(instance)) {
val strVisibility = Styler.getVisibilityString(activity, account.isMisskey, checkVisibility)
return@runApiTask TootApiResult(
getString(R.string.server_has_no_support_of_visibility,strVisibility)
getString(R.string.server_has_no_support_of_visibility, strVisibility)
)
}
}
@ -783,7 +783,7 @@ class PostHelper(
val limit = 100
val s = src.substring(start, end)
val acct_list = AcctSet.searchPrefix(s, limit)
log.d("search for %s, result=%d", s, acct_list.size)
log.d("search for ${s}, result=${acct_list.size}")
if (acct_list.isEmpty()) {
closeAcctPopup()
} else {
@ -811,7 +811,7 @@ class PostHelper(
val limit = 100
val s = src.substring(last_sharp + 1, end)
val tag_list = TagSet.searchPrefix(s, limit)
log.d("search for %s, result=%d", s, tag_list.size)
log.d("search for ${s}, result=${tag_list.size}")
if (tag_list.isEmpty()) {
closeAcctPopup()
} else {
@ -863,7 +863,7 @@ class PostHelper(
val s =
src.substring(last_colon + 1, end).lowercase().replace('-', '_')
val matches = EmojiDecoder.searchShortCode(activity, s, remain)
log.d("checkEmoji: search for %s, result=%d", s, matches.size)
log.d("checkEmoji: search for ${s}, result=${matches.size}")
code_list.addAll(matches)
}

View File

@ -8,79 +8,78 @@ import java.io.FileNotFoundException
import java.util.*
class TaskList {
companion object {
private val log = LogCategory("TaskList")
private const val FILE_TASK_LIST = "JOB_TASK_LIST"
}
private lateinit var _list : LinkedList<JsonObject>
@Synchronized
private fun prepareList(context : Context) : LinkedList<JsonObject> {
if(! ::_list.isInitialized) {
_list = LinkedList()
try {
context.openFileInput(FILE_TASK_LIST).use { inputStream ->
val bao = ByteArrayOutputStream()
IOUtils.copy(inputStream, bao)
bao.toByteArray().decodeUTF8().decodeJsonArray().objectList().forEach {
_list.add(it)
}
}
} catch(ex : FileNotFoundException) {
log.e(ex, "prepareList: file not found.")
} catch(ex : Throwable) {
log.trace(ex, "TaskList: prepareArray failed.")
}
}
return _list
}
@Synchronized
private fun saveArray(context : Context) {
val list = prepareList(context)
try {
log.d("saveArray size=%s", list.size)
val data = JsonArray(list).toString().encodeUTF8()
context.openFileOutput(FILE_TASK_LIST, Context.MODE_PRIVATE)
.use { IOUtils.write(data, it) }
} catch(ex : Throwable) {
log.trace(ex)
log.e(ex, "TaskList: saveArray failed.size=%s", list.size)
}
}
@Synchronized
fun addLast(context : Context, removeOld : Boolean, taskData : JsonObject) {
val list = prepareList(context)
if(removeOld) {
val it = list.iterator()
while(it.hasNext()) {
val item = it.next()
if(taskData == item) it.remove()
}
}
list.addLast(taskData)
saveArray(context)
}
@Suppress("unused")
@Synchronized
fun hasNext(context : Context) : Boolean {
return prepareList(context).isNotEmpty()
}
@Synchronized
fun next(context : Context) : JsonObject? {
val list = prepareList(context)
val item = if(list.isEmpty()) null else list.removeFirst()
saveArray(context)
return item
}
companion object {
private val log = LogCategory("TaskList")
private const val FILE_TASK_LIST = "JOB_TASK_LIST"
}
private lateinit var _list: LinkedList<JsonObject>
@Synchronized
private fun prepareList(context: Context): LinkedList<JsonObject> {
if (!::_list.isInitialized) {
_list = LinkedList()
try {
context.openFileInput(FILE_TASK_LIST).use { inputStream ->
val bao = ByteArrayOutputStream()
IOUtils.copy(inputStream, bao)
bao.toByteArray().decodeUTF8().decodeJsonArray().objectList().forEach {
_list.add(it)
}
}
} catch (ex: FileNotFoundException) {
log.e(ex, "prepareList: file not found.")
} catch (ex: Throwable) {
log.trace(ex, "TaskList: prepareArray failed.")
}
}
return _list
}
@Synchronized
private fun saveArray(context: Context) {
val list = prepareList(context)
try {
log.d("saveArray size=${list.size}")
val data = JsonArray(list).toString().encodeUTF8()
context.openFileOutput(FILE_TASK_LIST, Context.MODE_PRIVATE)
.use { IOUtils.write(data, it) }
} catch (ex: Throwable) {
log.trace(ex, "TaskList: saveArray failed.size=${list.size}")
}
}
@Synchronized
fun addLast(context: Context, removeOld: Boolean, taskData: JsonObject) {
val list = prepareList(context)
if (removeOld) {
val it = list.iterator()
while (it.hasNext()) {
val item = it.next()
if (taskData == item) it.remove()
}
}
list.addLast(taskData)
saveArray(context)
}
@Suppress("unused")
@Synchronized
fun hasNext(context: Context): Boolean {
return prepareList(context).isNotEmpty()
}
@Synchronized
fun next(context: Context): JsonObject? {
val list = prepareList(context)
val item = if (list.isEmpty()) null else list.removeFirst()
saveArray(context)
return item
}
}

View File

@ -11,42 +11,42 @@ import jp.juggler.subwaytooter.StatusButtonsPopup
import jp.juggler.util.LogCategory
class MyListView : ListView {
companion object {
private val log = LogCategory("MyListView")
}
constructor(context : Context) : super(context)
constructor(context : Context, attrs : AttributeSet) : super(context, attrs)
constructor(context : Context, attrs : AttributeSet, defStyleAttr : Int) : super(context, attrs, defStyleAttr)
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(ev : MotionEvent) : Boolean {
// ポップアップを閉じた時にクリックでリストを触ったことになってしまう不具合の回避
val now = SystemClock.elapsedRealtime()
if(now - StatusButtonsPopup.last_popup_close < 30L) {
val action = ev.action
if(action == MotionEvent.ACTION_DOWN) {
// ポップアップを閉じた直後はタッチダウンを無視する
return false
}
val rv = super.onTouchEvent(ev)
log.d("onTouchEvent action=%s, rv=%s", action, rv)
return rv
}
return super.onTouchEvent(ev)
}
override fun layoutChildren() {
try {
super.layoutChildren()
} catch(ex : Throwable) {
log.trace(ex)
}
}
companion object {
private val log = LogCategory("MyListView")
}
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(ev: MotionEvent): Boolean {
// ポップアップを閉じた時にクリックでリストを触ったことになってしまう不具合の回避
val now = SystemClock.elapsedRealtime()
if (now - StatusButtonsPopup.last_popup_close < 30L) {
val action = ev.action
if (action == MotionEvent.ACTION_DOWN) {
// ポップアップを閉じた直後はタッチダウンを無視する
return false
}
val rv = super.onTouchEvent(ev)
log.d("onTouchEvent action=${action}, rv=${rv}")
return rv
}
return super.onTouchEvent(ev)
}
override fun layoutChildren() {
try {
super.layoutChildren()
} catch (ex: Throwable) {
log.trace(ex)
}
}
}

View File

@ -17,490 +17,491 @@ import kotlin.math.abs
import kotlin.math.max
import kotlin.math.sqrt
class PinchBitmapView(context : Context, attrs : AttributeSet?, defStyle : Int) :
View(context, attrs, defStyle) {
companion object {
internal val log = LogCategory("PinchImageView")
// 数値を範囲内にクリップする
private fun clip(min : Float, max : Float, v : Float) : Float {
return if(v < min) min else if(v > max) max else v
}
// ビューの幅と画像の描画サイズを元に描画位置をクリップする
private fun clipTranslate(
view_w : Float // ビューの幅
, bitmap_w : Float // 画像の幅
, current_scale : Float // 画像の拡大率
, trans_x : Float // タッチ操作による表示位置
) : Float {
// 余白(拡大率が小さい場合はプラス、拡大率が大きい場合はマイナス)
val padding = view_w - bitmap_w * current_scale
// 余白が>=0なら画像を中心に表示する。 <0なら操作された位置をクリップする。
return if(padding >= 0f) padding / 2f else clip(padding, 0f, trans_x)
}
}
private var callback : Callback? = null
private var bitmap : Bitmap? = null
private var bitmap_w : Float = 0.toFloat()
private var bitmap_h : Float = 0.toFloat()
private var bitmap_aspect : Float = 0.toFloat()
// 画像を表示する位置と拡大率
private var current_trans_x : Float = 0.toFloat()
private var current_trans_y : Float = 0.toFloat()
private var current_scale : Float = 0.toFloat()
// 画像表示に使う構造体
private val drawMatrix = Matrix()
internal val paint = Paint()
// タッチ操作中に指を動かした
private var bDrag : Boolean = false
// タッチ操作中に指の数を変えた
private var bPointerCountChanged : Boolean = false
// ページめくりに必要なスワイプ強度
private var swipe_velocity = 0f
private var swipe_velocity2 = 0f
// 指を動かしたと判断する距離
private var drag_length = 0f
private var time_touch_start = 0L
// フリック操作の検出に使う
private var velocityTracker : VelocityTracker? = null
private var click_time = 0L
private var click_count = 0
// 移動後の指の位置
internal val pos = PointerAvg()
// 移動開始時の指の位置
private val start_pos = PointerAvg()
// 移動開始時の画像の位置
private var start_image_trans_x : Float = 0.toFloat()
private var start_image_trans_y : Float = 0.toFloat()
private var start_image_scale : Float = 0.toFloat()
private var scale_min : Float = 0.toFloat()
private var scale_max : Float = 0.toFloat()
private var view_w : Float = 0.toFloat()
private var view_h : Float = 0.toFloat()
private var view_aspect : Float = 0.toFloat()
private val tracking_matrix = Matrix()
private val tracking_matrix_inv = Matrix()
private val avg_on_image1 = FloatArray(2)
private val avg_on_image2 = FloatArray(2)
constructor(context : Context) : this(context, null) {
init(context)
}
constructor(context : Context, attrs : AttributeSet?) : this(context, attrs, 0) {
init(context)
}
init {
init(context)
}
internal fun init(context : Context) {
// 定数をdpからpxに変換
val density = context.resources.displayMetrics.density
swipe_velocity = 1000f * density
swipe_velocity2 = 250f * density
drag_length = 4f * density // 誤反応しがちなのでやや厳しめ
}
// ページめくり操作のコールバック
interface Callback {
fun onSwipe(deltaX : Int, deltaY : Int)
fun onMove(bitmap_w : Float, bitmap_h : Float, tx : Float, ty : Float, scale : Float)
}
fun setCallback(callback : Callback?) {
this.callback = callback
}
fun setBitmap(b : Bitmap?) {
bitmap?.recycle()
this.bitmap = b
initializeScale()
}
override fun onDraw(canvas : Canvas) {
super.onDraw(canvas)
val bitmap = this.bitmap
if(bitmap != null && ! bitmap.isRecycled) {
drawMatrix.reset()
drawMatrix.postScale(current_scale, current_scale)
drawMatrix.postTranslate(current_trans_x, current_trans_y)
paint.isFilterBitmap = current_scale < 4f
canvas.drawBitmap(bitmap, drawMatrix, paint)
}
}
override fun onSizeChanged(w : Int, h : Int, oldw : Int, oldh : Int) {
super.onSizeChanged(w, h, oldw, oldh)
view_w = max(1f, w.toFloat())
view_h = max(1f, h.toFloat())
view_aspect = view_w / view_h
initializeScale()
}
override fun performClick() : Boolean {
super.performClick()
initializeScale()
return true
}
private var defaultScale : Float = 1f
// 表示位置の初期化
// 呼ばれるのは、ビットマップを変更した時、ビューのサイズが変わった時、画像をクリックした時
private fun initializeScale() {
val bitmap = this.bitmap
if(bitmap != null && ! bitmap.isRecycled && view_w >= 1f) {
bitmap_w = max(1f, bitmap.width.toFloat())
bitmap_h = max(1f, bitmap.height.toFloat())
bitmap_aspect = bitmap_w / bitmap_h
if(view_aspect > bitmap_aspect) {
scale_min = view_h / bitmap_h / 2f
scale_max = view_w / bitmap_w * 8f
} else {
scale_min = view_w / bitmap_w / 2f
scale_max = view_h / bitmap_h * 8f
}
if(scale_max < scale_min) scale_max = scale_min * 16f
defaultScale = if(view_aspect > bitmap_aspect) {
view_h / bitmap_h
} else {
view_w / bitmap_w
}
val draw_w = bitmap_w * defaultScale
val draw_h = bitmap_h * defaultScale
current_scale = defaultScale
current_trans_x = (view_w - draw_w) / 2f
current_trans_y = (view_h - draw_h) / 2f
callback?.onMove(bitmap_w, bitmap_h, current_trans_x, current_trans_y, current_scale)
} else {
defaultScale = 1f
scale_min = 1f
scale_max = 1f
current_scale = defaultScale
current_trans_y = 0f
current_trans_x = 0f
callback?.onMove(0f, 0f, current_trans_x, current_trans_y, current_scale)
}
// 画像がnullに変化した時も再描画が必要
invalidate()
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(ev : MotionEvent) : Boolean {
val bitmap = this.bitmap
if(bitmap == null
|| bitmap.isRecycled
|| view_w < 1f)
return false
val action = ev.action
if(action == MotionEvent.ACTION_DOWN) {
time_touch_start = SystemClock.elapsedRealtime()
velocityTracker?.clear()
velocityTracker = VelocityTracker.obtain()
velocityTracker?.addMovement(ev)
bPointerCountChanged = false
bDrag = bPointerCountChanged
trackStart(ev)
return true
}
velocityTracker?.addMovement(ev)
when(action) {
MotionEvent.ACTION_POINTER_DOWN, MotionEvent.ACTION_POINTER_UP -> {
// タッチ操作中に指の数を変えた
bPointerCountChanged = true
bDrag = bPointerCountChanged
trackStart(ev)
}
MotionEvent.ACTION_MOVE -> trackNext(ev)
MotionEvent.ACTION_UP -> {
trackNext(ev)
checkClickOrPaging()
velocityTracker?.recycle()
velocityTracker = null
}
}
return true
}
private fun checkClickOrPaging() {
if(! bDrag) {
// 指を動かしていないなら
val now = SystemClock.elapsedRealtime()
if(now - time_touch_start >= 1000L) {
// ロングタップはタップカウントをリセットする
log.d("click count reset by long tap")
click_count = 0
return
}
val delta = now - click_time
click_time = now
if(delta > 334L) {
// 前回のタップからの時刻が長いとタップカウントをリセットする
log.d("click count reset by long interval")
click_count = 0
}
++ click_count
log.d("click %d %d", click_count, delta)
if(click_count >= 2) {
// ダブルタップでクリック操作
click_count = 0
performClick()
}
return
}
click_count = 0
val velocityTracker = this.velocityTracker
if(! bPointerCountChanged && velocityTracker != null) {
// 指の数を変えていないならページめくり操作かもしれない
// 「画像を動かした」かどうかのチェック
val image_moved = max(
abs(current_trans_x - start_image_trans_x),
abs(current_trans_y - start_image_trans_y)
)
if(image_moved >= drag_length) {
log.d("image moved. not flick action. $image_moved")
return
}
velocityTracker.computeCurrentVelocity(1000)
val vx = velocityTracker.xVelocity
val vy = velocityTracker.yVelocity
val avx = abs(vx)
val avy = abs(vy)
val velocity = sqrt(vx * vx + vy * vy)
val aspect = try {
avx / avy
} catch(ex : Throwable) {
Float.MAX_VALUE
}
when {
aspect >= 0.9f -> {
// 指を動かした方向が左右だった
val vMin = when {
current_scale * bitmap_w <= view_w -> swipe_velocity2
else -> swipe_velocity
}
if(velocity < vMin) {
log.d("velocity $velocity not enough to pagingX")
return
}
log.d("pagingX! m=$image_moved a=$aspect v=$velocity")
runOnMainLooper { callback?.onSwipe(if(vx >= 0f) - 1 else 1, 0) }
}
aspect <= 0.333f -> {
// 指を動かした方向が上下だった
val vMin = when {
current_scale * bitmap_h <= view_h -> swipe_velocity2
else -> swipe_velocity
}
if(velocity < vMin) {
log.d("velocity $velocity not enough to pagingY")
return
}
log.d("pagingY! m=$image_moved a=$aspect v=$velocity")
runOnMainLooper { callback?.onSwipe(0, if(vy >= 0f) - 1 else 1) }
}
else -> log.d("flick is not horizontal/vertical. aspect=$aspect")
}
}
}
// マルチタッチの中心位置の計算
internal class PointerAvg {
// タッチ位置の数
var count : Int = 0
// タッチ位置の平均
val avg = FloatArray(2)
// 中心と、中心から最も離れたタッチ位置の間の距離
var max_radius : Float = 0.toFloat()
fun update(ev : MotionEvent) {
count = ev.pointerCount
if(count <= 1) {
avg[0] = ev.x
avg[1] = ev.y
max_radius = 0f
} else {
avg[0] = 0f
avg[1] = 0f
for(i in 0 until count) {
avg[0] += ev.getX(i)
avg[1] += ev.getY(i)
}
avg[0] /= count.toFloat()
avg[1] /= count.toFloat()
max_radius = 0f
for(i in 0 until count) {
val dx = ev.getX(i) - avg[0]
val dy = ev.getY(i) - avg[1]
val radius = dx * dx + dy * dy
if(radius > max_radius) max_radius = radius
}
max_radius = sqrt(max_radius.toDouble()).toFloat()
if(max_radius < 1f) max_radius = 1f
}
}
}
private fun trackStart(ev : MotionEvent) {
// 追跡開始時の指の位置
start_pos.update(ev)
// 追跡開始時の画像の位置
start_image_trans_x = current_trans_x
start_image_trans_y = current_trans_y
start_image_scale = current_scale
}
// 画面上の指の位置から画像中の指の位置を調べる
private fun getCoordinateOnImage(dst : FloatArray, src : FloatArray) {
tracking_matrix.reset()
tracking_matrix.postScale(current_scale, current_scale)
tracking_matrix.postTranslate(current_trans_x, current_trans_y)
tracking_matrix.invert(tracking_matrix_inv)
tracking_matrix_inv.mapPoints(dst, src)
}
private fun trackNext(ev : MotionEvent) {
pos.update(ev)
if(pos.count != start_pos.count) {
// タッチ操作中に指の数が変わった
log.d("nextTracking: pointer count changed")
bPointerCountChanged = true
bDrag = bPointerCountChanged
trackStart(ev)
return
}
// ズーム操作
if(pos.count > 1) {
// タッチ位置にある絵柄の座標を調べる
getCoordinateOnImage(avg_on_image1, pos.avg)
// ズーム率を変更する
current_scale = clip(
scale_min,
scale_max,
start_image_scale * pos.max_radius / start_pos.max_radius
)
// 再び調べる
getCoordinateOnImage(avg_on_image2, pos.avg)
// ズーム変更の前後で位置がズレた分だけ移動させると、タッチ位置にある絵柄がズレない
start_image_trans_x += current_scale * (avg_on_image2[0] - avg_on_image1[0])
start_image_trans_y += current_scale * (avg_on_image2[1] - avg_on_image1[1])
}
// 平行移動
run {
// start時から指を動かした量
val move_x = pos.avg[0] - start_pos.avg[0]
val move_y = pos.avg[1] - start_pos.avg[1]
// 「指を動かした」と判断したらフラグを立てる
if(abs(move_x) >= drag_length || abs(move_y) >= drag_length) {
bDrag = true
}
// 画像の表示位置を更新
current_trans_x =
clipTranslate(view_w, bitmap_w, current_scale, start_image_trans_x + move_x)
current_trans_y =
clipTranslate(view_h, bitmap_h, current_scale, start_image_trans_y + move_y)
}
callback?.onMove(bitmap_w, bitmap_h, current_trans_x, current_trans_y, current_scale)
invalidate()
}
class PinchBitmapView(context: Context, attrs: AttributeSet?, defStyle: Int) :
View(context, attrs, defStyle) {
companion object {
internal val log = LogCategory("PinchImageView")
// 数値を範囲内にクリップする
private fun clip(min: Float, max: Float, v: Float): Float {
return if (v < min) min else if (v > max) max else v
}
// ビューの幅と画像の描画サイズを元に描画位置をクリップする
private fun clipTranslate(
view_w: Float // ビューの幅
, bitmap_w: Float // 画像の幅
, current_scale: Float // 画像の拡大率
, trans_x: Float // タッチ操作による表示位置
): Float {
// 余白(拡大率が小さい場合はプラス、拡大率が大きい場合はマイナス)
val padding = view_w - bitmap_w * current_scale
// 余白が>=0なら画像を中心に表示する。 <0なら操作された位置をクリップする。
return if (padding >= 0f) padding / 2f else clip(padding, 0f, trans_x)
}
}
private var callback: Callback? = null
private var bitmap: Bitmap? = null
private var bitmap_w: Float = 0.toFloat()
private var bitmap_h: Float = 0.toFloat()
private var bitmap_aspect: Float = 0.toFloat()
// 画像を表示する位置と拡大率
private var current_trans_x: Float = 0.toFloat()
private var current_trans_y: Float = 0.toFloat()
private var current_scale: Float = 0.toFloat()
// 画像表示に使う構造体
private val drawMatrix = Matrix()
internal val paint = Paint()
// タッチ操作中に指を動かした
private var bDrag: Boolean = false
// タッチ操作中に指の数を変えた
private var bPointerCountChanged: Boolean = false
// ページめくりに必要なスワイプ強度
private var swipe_velocity = 0f
private var swipe_velocity2 = 0f
// 指を動かしたと判断する距離
private var drag_length = 0f
private var time_touch_start = 0L
// フリック操作の検出に使う
private var velocityTracker: VelocityTracker? = null
private var click_time = 0L
private var click_count = 0
// 移動後の指の位置
internal val pos = PointerAvg()
// 移動開始時の指の位置
private val start_pos = PointerAvg()
// 移動開始時の画像の位置
private var start_image_trans_x: Float = 0.toFloat()
private var start_image_trans_y: Float = 0.toFloat()
private var start_image_scale: Float = 0.toFloat()
private var scale_min: Float = 0.toFloat()
private var scale_max: Float = 0.toFloat()
private var view_w: Float = 0.toFloat()
private var view_h: Float = 0.toFloat()
private var view_aspect: Float = 0.toFloat()
private val tracking_matrix = Matrix()
private val tracking_matrix_inv = Matrix()
private val avg_on_image1 = FloatArray(2)
private val avg_on_image2 = FloatArray(2)
constructor(context: Context) : this(context, null) {
init(context)
}
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) {
init(context)
}
init {
init(context)
}
internal fun init(context: Context) {
// 定数をdpからpxに変換
val density = context.resources.displayMetrics.density
swipe_velocity = 1000f * density
swipe_velocity2 = 250f * density
drag_length = 4f * density // 誤反応しがちなのでやや厳しめ
}
// ページめくり操作のコールバック
interface Callback {
fun onSwipe(deltaX: Int, deltaY: Int)
fun onMove(bitmap_w: Float, bitmap_h: Float, tx: Float, ty: Float, scale: Float)
}
fun setCallback(callback: Callback?) {
this.callback = callback
}
fun setBitmap(b: Bitmap?) {
bitmap?.recycle()
this.bitmap = b
initializeScale()
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val bitmap = this.bitmap
if (bitmap != null && !bitmap.isRecycled) {
drawMatrix.reset()
drawMatrix.postScale(current_scale, current_scale)
drawMatrix.postTranslate(current_trans_x, current_trans_y)
paint.isFilterBitmap = current_scale < 4f
canvas.drawBitmap(bitmap, drawMatrix, paint)
}
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
view_w = max(1f, w.toFloat())
view_h = max(1f, h.toFloat())
view_aspect = view_w / view_h
initializeScale()
}
override fun performClick(): Boolean {
super.performClick()
initializeScale()
return true
}
private var defaultScale: Float = 1f
// 表示位置の初期化
// 呼ばれるのは、ビットマップを変更した時、ビューのサイズが変わった時、画像をクリックした時
private fun initializeScale() {
val bitmap = this.bitmap
if (bitmap != null && !bitmap.isRecycled && view_w >= 1f) {
bitmap_w = max(1f, bitmap.width.toFloat())
bitmap_h = max(1f, bitmap.height.toFloat())
bitmap_aspect = bitmap_w / bitmap_h
if (view_aspect > bitmap_aspect) {
scale_min = view_h / bitmap_h / 2f
scale_max = view_w / bitmap_w * 8f
} else {
scale_min = view_w / bitmap_w / 2f
scale_max = view_h / bitmap_h * 8f
}
if (scale_max < scale_min) scale_max = scale_min * 16f
defaultScale = if (view_aspect > bitmap_aspect) {
view_h / bitmap_h
} else {
view_w / bitmap_w
}
val draw_w = bitmap_w * defaultScale
val draw_h = bitmap_h * defaultScale
current_scale = defaultScale
current_trans_x = (view_w - draw_w) / 2f
current_trans_y = (view_h - draw_h) / 2f
callback?.onMove(bitmap_w, bitmap_h, current_trans_x, current_trans_y, current_scale)
} else {
defaultScale = 1f
scale_min = 1f
scale_max = 1f
current_scale = defaultScale
current_trans_y = 0f
current_trans_x = 0f
callback?.onMove(0f, 0f, current_trans_x, current_trans_y, current_scale)
}
// 画像がnullに変化した時も再描画が必要
invalidate()
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(ev: MotionEvent): Boolean {
val bitmap = this.bitmap
if (bitmap == null
|| bitmap.isRecycled
|| view_w < 1f
)
return false
val action = ev.action
if (action == MotionEvent.ACTION_DOWN) {
time_touch_start = SystemClock.elapsedRealtime()
velocityTracker?.clear()
velocityTracker = VelocityTracker.obtain()
velocityTracker?.addMovement(ev)
bPointerCountChanged = false
bDrag = bPointerCountChanged
trackStart(ev)
return true
}
velocityTracker?.addMovement(ev)
when (action) {
MotionEvent.ACTION_POINTER_DOWN, MotionEvent.ACTION_POINTER_UP -> {
// タッチ操作中に指の数を変えた
bPointerCountChanged = true
bDrag = bPointerCountChanged
trackStart(ev)
}
MotionEvent.ACTION_MOVE -> trackNext(ev)
MotionEvent.ACTION_UP -> {
trackNext(ev)
checkClickOrPaging()
velocityTracker?.recycle()
velocityTracker = null
}
}
return true
}
private fun checkClickOrPaging() {
if (!bDrag) {
// 指を動かしていないなら
val now = SystemClock.elapsedRealtime()
if (now - time_touch_start >= 1000L) {
// ロングタップはタップカウントをリセットする
log.d("click count reset by long tap")
click_count = 0
return
}
val delta = now - click_time
click_time = now
if (delta > 334L) {
// 前回のタップからの時刻が長いとタップカウントをリセットする
log.d("click count reset by long interval")
click_count = 0
}
++click_count
log.d("click ${click_count} ${delta}")
if (click_count >= 2) {
// ダブルタップでクリック操作
click_count = 0
performClick()
}
return
}
click_count = 0
val velocityTracker = this.velocityTracker
if (!bPointerCountChanged && velocityTracker != null) {
// 指の数を変えていないならページめくり操作かもしれない
// 「画像を動かした」かどうかのチェック
val image_moved = max(
abs(current_trans_x - start_image_trans_x),
abs(current_trans_y - start_image_trans_y)
)
if (image_moved >= drag_length) {
log.d("image moved. not flick action. $image_moved")
return
}
velocityTracker.computeCurrentVelocity(1000)
val vx = velocityTracker.xVelocity
val vy = velocityTracker.yVelocity
val avx = abs(vx)
val avy = abs(vy)
val velocity = sqrt(vx * vx + vy * vy)
val aspect = try {
avx / avy
} catch (ex: Throwable) {
Float.MAX_VALUE
}
when {
aspect >= 0.9f -> {
// 指を動かした方向が左右だった
val vMin = when {
current_scale * bitmap_w <= view_w -> swipe_velocity2
else -> swipe_velocity
}
if (velocity < vMin) {
log.d("velocity $velocity not enough to pagingX")
return
}
log.d("pagingX! m=$image_moved a=$aspect v=$velocity")
runOnMainLooper { callback?.onSwipe(if (vx >= 0f) -1 else 1, 0) }
}
aspect <= 0.333f -> {
// 指を動かした方向が上下だった
val vMin = when {
current_scale * bitmap_h <= view_h -> swipe_velocity2
else -> swipe_velocity
}
if (velocity < vMin) {
log.d("velocity $velocity not enough to pagingY")
return
}
log.d("pagingY! m=$image_moved a=$aspect v=$velocity")
runOnMainLooper { callback?.onSwipe(0, if (vy >= 0f) -1 else 1) }
}
else -> log.d("flick is not horizontal/vertical. aspect=$aspect")
}
}
}
// マルチタッチの中心位置の計算
internal class PointerAvg {
// タッチ位置の数
var count: Int = 0
// タッチ位置の平均
val avg = FloatArray(2)
// 中心と、中心から最も離れたタッチ位置の間の距離
var max_radius: Float = 0.toFloat()
fun update(ev: MotionEvent) {
count = ev.pointerCount
if (count <= 1) {
avg[0] = ev.x
avg[1] = ev.y
max_radius = 0f
} else {
avg[0] = 0f
avg[1] = 0f
for (i in 0 until count) {
avg[0] += ev.getX(i)
avg[1] += ev.getY(i)
}
avg[0] /= count.toFloat()
avg[1] /= count.toFloat()
max_radius = 0f
for (i in 0 until count) {
val dx = ev.getX(i) - avg[0]
val dy = ev.getY(i) - avg[1]
val radius = dx * dx + dy * dy
if (radius > max_radius) max_radius = radius
}
max_radius = sqrt(max_radius.toDouble()).toFloat()
if (max_radius < 1f) max_radius = 1f
}
}
}
private fun trackStart(ev: MotionEvent) {
// 追跡開始時の指の位置
start_pos.update(ev)
// 追跡開始時の画像の位置
start_image_trans_x = current_trans_x
start_image_trans_y = current_trans_y
start_image_scale = current_scale
}
// 画面上の指の位置から画像中の指の位置を調べる
private fun getCoordinateOnImage(dst: FloatArray, src: FloatArray) {
tracking_matrix.reset()
tracking_matrix.postScale(current_scale, current_scale)
tracking_matrix.postTranslate(current_trans_x, current_trans_y)
tracking_matrix.invert(tracking_matrix_inv)
tracking_matrix_inv.mapPoints(dst, src)
}
private fun trackNext(ev: MotionEvent) {
pos.update(ev)
if (pos.count != start_pos.count) {
// タッチ操作中に指の数が変わった
log.d("nextTracking: pointer count changed")
bPointerCountChanged = true
bDrag = bPointerCountChanged
trackStart(ev)
return
}
// ズーム操作
if (pos.count > 1) {
// タッチ位置にある絵柄の座標を調べる
getCoordinateOnImage(avg_on_image1, pos.avg)
// ズーム率を変更する
current_scale = clip(
scale_min,
scale_max,
start_image_scale * pos.max_radius / start_pos.max_radius
)
// 再び調べる
getCoordinateOnImage(avg_on_image2, pos.avg)
// ズーム変更の前後で位置がズレた分だけ移動させると、タッチ位置にある絵柄がズレない
start_image_trans_x += current_scale * (avg_on_image2[0] - avg_on_image1[0])
start_image_trans_y += current_scale * (avg_on_image2[1] - avg_on_image1[1])
}
// 平行移動
run {
// start時から指を動かした量
val move_x = pos.avg[0] - start_pos.avg[0]
val move_y = pos.avg[1] - start_pos.avg[1]
// 「指を動かした」と判断したらフラグを立てる
if (abs(move_x) >= drag_length || abs(move_y) >= drag_length) {
bDrag = true
}
// 画像の表示位置を更新
current_trans_x =
clipTranslate(view_w, bitmap_w, current_scale, start_image_trans_x + move_x)
current_trans_y =
clipTranslate(view_h, bitmap_h, current_scale, start_image_trans_y + move_y)
}
callback?.onMove(bitmap_w, bitmap_h, current_trans_x, current_trans_y, current_scale)
invalidate()
}
}

View File

@ -248,11 +248,7 @@ fun createResizedBitmap(
val paint = Paint()
paint.isFilterBitmap = true
canvas.drawBitmap(sourceBitmap, matrix, paint)
log.d(
"createResizedBitmap: resized to %sx%s",
dstSizeInt.x,
dstSizeInt.y
)
log.d("createResizedBitmap: resized to ${dstSizeInt.x}x${dstSizeInt.y}")
val tmp = dst
dst = null
tmp

View File

@ -167,66 +167,66 @@ import java.util.*
private const val MIME_TYPE_APPLICATION_OCTET_STREAM = "application/octet-stream"
private val mimeTypeExMap : HashMap<String, String> by lazy {
val map = HashMap<String, String>()
map["BDM"] = "application/vnd.syncml.dm+wbxml"
map["DAT"] = ""
map["TID"] = ""
map["js"] = "text/javascript"
map["sh"] = "application/x-sh"
map["lua"] = "text/x-lua"
map
private val mimeTypeExMap: HashMap<String, String> by lazy {
val map = HashMap<String, String>()
map["BDM"] = "application/vnd.syncml.dm+wbxml"
map["DAT"] = ""
map["TID"] = ""
map["js"] = "text/javascript"
map["sh"] = "application/x-sh"
map["lua"] = "text/x-lua"
map
}
@Suppress("unused")
fun getMimeType(log : LogCategory?, src : String) : String {
var ext = MimeTypeMap.getFileExtensionFromUrl(src)
if(ext != null && ext.isNotEmpty()) {
ext = ext.lowercase()
//
var mime_type : String? = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext)
if(mime_type?.isNotEmpty() == true) return mime_type
//
mime_type = mimeTypeExMap[ext]
if(mime_type?.isNotEmpty() == true) return mime_type
// 戻り値が空文字列の場合とnullの場合があり、空文字列の場合は既知なのでログ出力しない
if(mime_type == null && log != null) {
log.w("getMimeType(): unknown file extension '%s'", ext)
}
}
return MIME_TYPE_APPLICATION_OCTET_STREAM
fun getMimeType(log: LogCategory?, src: String): String {
var ext = MimeTypeMap.getFileExtensionFromUrl(src)
if (ext != null && ext.isNotEmpty()) {
ext = ext.lowercase()
//
var mime_type: String? = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext)
if (mime_type?.isNotEmpty() == true) return mime_type
//
mime_type = mimeTypeExMap[ext]
if (mime_type?.isNotEmpty() == true) return mime_type
// 戻り値が空文字列の場合とnullの場合があり、空文字列の場合は既知なのでログ出力しない
if (mime_type == null && log != null) {
log.w("getMimeType(): unknown file extension '${ext}'")
}
}
return MIME_TYPE_APPLICATION_OCTET_STREAM
}
fun getDocumentName(contentResolver : ContentResolver, uri : Uri) : String {
val errorName = "no_name"
return contentResolver.query(uri, null, null, null, null, null)
?.use { cursor ->
return if(! cursor.moveToFirst()) {
errorName
} else {
cursor.getStringOrNull(OpenableColumns.DISPLAY_NAME) ?: errorName
}
}
?: errorName
fun getDocumentName(contentResolver: ContentResolver, uri: Uri): String {
val errorName = "no_name"
return contentResolver.query(uri, null, null, null, null, null)
?.use { cursor ->
return if (!cursor.moveToFirst()) {
errorName
} else {
cursor.getStringOrNull(OpenableColumns.DISPLAY_NAME) ?: errorName
}
}
?: errorName
}
fun getStreamSize(bClose : Boolean, inStream : InputStream) : Long {
try {
var size = 0L
while(true) {
val r = IOUtils.skip(inStream, 16384)
if(r <= 0) break
size += r
}
return size
} finally {
@Suppress("DEPRECATION")
if(bClose) IOUtils.closeQuietly(inStream)
}
fun getStreamSize(bClose: Boolean, inStream: InputStream): Long {
try {
var size = 0L
while (true) {
val r = IOUtils.skip(inStream, 16384)
if (r <= 0) break
size += r
}
return size
} finally {
@Suppress("DEPRECATION")
if (bClose) IOUtils.closeQuietly(inStream)
}
}
//fun File.loadByteArray() : ByteArray {
@ -242,84 +242,84 @@ fun getStreamSize(bClose : Boolean, inStream : InputStream) : Long {
// }
//}
fun Context.loadRawResource(resId : Int) : ByteArray {
resources.openRawResource(resId).use { inStream ->
val bao = ByteArrayOutputStream(inStream.available())
IOUtils.copy(inStream, bao)
return bao.toByteArray()
}
fun Context.loadRawResource(resId: Int): ByteArray {
resources.openRawResource(resId).use { inStream ->
val bao = ByteArrayOutputStream(inStream.available())
IOUtils.copy(inStream, bao)
return bao.toByteArray()
}
}
fun intentOpenDocument(mimeType : String) : Intent {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = mimeType // "image/*"
return intent
fun intentOpenDocument(mimeType: String): Intent {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = mimeType // "image/*"
return intent
}
fun intentGetContent(
allowMultiple : Boolean,
caption : String,
mimeTypes : Array<out String>
) : Intent {
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
if(allowMultiple) {
// EXTRA_ALLOW_MULTIPLE は API 18 (4.3)以降。ACTION_GET_CONTENT でも ACTION_OPEN_DOCUMENT でも指定できる
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
}
// EXTRA_MIME_TYPES は API 19以降。ACTION_GET_CONTENT でも ACTION_OPEN_DOCUMENT でも指定できる
intent.putExtra("android.intent.extra.MIME_TYPES", mimeTypes)
intent.type = when {
mimeTypes.size == 1 -> mimeTypes[0]
// On Android 6.0 and above using "video/* image/" or "image/ video/*" type doesn't work
// it only recognizes the first filter you specify.
Build.VERSION.SDK_INT >= 23 -> "*/*"
else -> mimeTypes.joinToString(" ")
}
return Intent.createChooser(intent, caption)
allowMultiple: Boolean,
caption: String,
mimeTypes: Array<out String>
): Intent {
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
if (allowMultiple) {
// EXTRA_ALLOW_MULTIPLE は API 18 (4.3)以降。ACTION_GET_CONTENT でも ACTION_OPEN_DOCUMENT でも指定できる
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
}
// EXTRA_MIME_TYPES は API 19以降。ACTION_GET_CONTENT でも ACTION_OPEN_DOCUMENT でも指定できる
intent.putExtra("android.intent.extra.MIME_TYPES", mimeTypes)
intent.type = when {
mimeTypes.size == 1 -> mimeTypes[0]
// On Android 6.0 and above using "video/* image/" or "image/ video/*" type doesn't work
// it only recognizes the first filter you specify.
Build.VERSION.SDK_INT >= 23 -> "*/*"
else -> mimeTypes.joinToString(" ")
}
return Intent.createChooser(intent, caption)
}
data class GetContentResultEntry(
val uri : Uri,
val mimeType : String? = null,
var time : Long? = null
val uri: Uri,
val mimeType: String? = null,
var time: Long? = null
)
// returns list of pair of uri and mime-type.
fun Intent.handleGetContentResult(contentResolver : ContentResolver) : ArrayList<GetContentResultEntry> {
val urlList = ArrayList<GetContentResultEntry>()
// 単一選択
this.data?.let {
urlList.add(GetContentResultEntry(it, this.type))
}
// 複数選択
val cd = this.clipData
if(cd != null) {
for(i in 0 until cd.itemCount) {
cd.getItemAt(i)?.uri?.let { uri ->
if(null == urlList.find { it.uri == uri }) {
urlList.add(GetContentResultEntry(uri))
}
}
}
}
urlList.forEach {
try {
contentResolver.takePersistableUriPermission(
it.uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION
)
} catch(_ : Throwable) {
}
}
return urlList
fun Intent.handleGetContentResult(contentResolver: ContentResolver): ArrayList<GetContentResultEntry> {
val urlList = ArrayList<GetContentResultEntry>()
// 単一選択
this.data?.let {
urlList.add(GetContentResultEntry(it, this.type))
}
// 複数選択
val cd = this.clipData
if (cd != null) {
for (i in 0 until cd.itemCount) {
cd.getItemAt(i)?.uri?.let { uri ->
if (null == urlList.find { it.uri == uri }) {
urlList.add(GetContentResultEntry(uri))
}
}
}
}
urlList.forEach {
try {
contentResolver.takePersistableUriPermission(
it.uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION
)
} catch (_: Throwable) {
}
}
return urlList
}