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) { private fun importAppData2(bConfirm: Boolean, uri: Uri) {
val type = contentResolver.getType(uri) val type = contentResolver.getType(uri)
log.d("importAppData type=%s", type) log.d("importAppData type=$type")
if (!bConfirm) { if (!bConfirm) {
AlertDialog.Builder(this) AlertDialog.Builder(this)

View File

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

View File

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

View File

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

View File

@ -329,7 +329,7 @@ fun Column.isFiltered(item: TootNotification): Boolean {
TootNotification.TYPE_FOLLOW_REQUEST_ACCEPTED_MISSKEY -> { TootNotification.TYPE_FOLLOW_REQUEST_ACCEPTED_MISSKEY -> {
val who = item.account val who = item.account
if (who != null && favMuteSet?.contains(access_info.getFullAcct(who)) == true) { 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 return true
} }
} }
@ -404,21 +404,21 @@ fun Column.checkFiltersForListData(trees: FilterTrees?) {
} }
fun reloadFilter(context: Context, access_info: SavedAccount) { fun reloadFilter(context: Context, access_info: SavedAccount) {
launchMain{ launchMain {
var resultList: ArrayList<TootFilter>? = null var resultList: ArrayList<TootFilter>? = null
context.runApiTask( context.runApiTask(
access_info, access_info,
progressStyle = ApiTask.PROGRESS_NONE progressStyle = ApiTask.PROGRESS_NONE
){client-> ) { client ->
client.request(ApiPath.PATH_FILTERS)?.also{ result-> client.request(ApiPath.PATH_FILTERS)?.also { result ->
result.jsonArray?.let{ result.jsonArray?.let {
resultList = TootFilter.parseList(it) resultList = TootFilter.parseList(it)
} }
} }
} }
resultList?.let{ resultList?.let {
Column.log.d("update filters for ${access_info.acct.pretty}") Column.log.d("update filters for ${access_info.acct.pretty}")
for (column in App1.getAppState(context).columnList) { for (column in App1.getAppState(context).columnList) {
if (column.access_info == access_info) { 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) column.saveRange(bBottom = true, bTop = true, result = result, list = src)
} else { } else {
column.saveRangeTop(result, src) column.saveRangeTop(result, src)
true
} }
return when { return when {
@ -416,6 +417,7 @@ class ColumnTask_Loading(
column.saveRange(bBottom = true, bTop = true, result = result, list = src) column.saveRange(bBottom = true, bTop = true, result = result, list = src)
} else { } else {
column.saveRangeTop(result, src) column.saveRangeTop(result, src)
true
} }
return when { return when {
@ -998,7 +1000,7 @@ class ColumnTask_Loading(
target_status.conversation_main = true target_status.conversation_main = true
// 祖先 // 祖先
val list_asc = java.util.ArrayList<TootStatus>() val list_asc = ArrayList<TootStatus>()
while (true) { while (true) {
if (client.isApiCancelled) return null if (client.isApiCancelled) return null
queryParams["offset"] = list_asc.size 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>() val idSet = HashSet<EntityId>()
var untilId: EntityId? = null 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 list_asc.size + list_desc.size + 2
).apply { ).apply {
addAll(list_asc.sortedBy { it.time_created_at }) addAll(list_asc.sortedBy { it.time_created_at })
@ -1084,7 +1086,7 @@ class ColumnTask_Loading(
target_status.conversation_main = true target_status.conversation_main = true
if (conversation_context != null) { if (conversation_context != null) {
this.list_tmp = java.util.ArrayList( this.list_tmp = ArrayList(
1 1
+ (conversation_context.ancestors?.size ?: 0) + (conversation_context.ancestors?.size ?: 0)
+ (conversation_context.descendants?.size ?: 0) + (conversation_context.descendants?.size ?: 0)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,33 +5,33 @@ import androidx.recyclerview.widget.RecyclerView
import jp.juggler.util.LogCategory import jp.juggler.util.LogCategory
internal class TabletColumnViewHolder( internal class TabletColumnViewHolder(
activity : ActMain, activity: ActMain,
parent: ViewGroup, parent: ViewGroup,
val columnViewHolder : ColumnViewHolder = ColumnViewHolder(activity,parent) val columnViewHolder: ColumnViewHolder = ColumnViewHolder(activity, parent)
) : RecyclerView.ViewHolder(columnViewHolder.viewRoot) { ) : RecyclerView.ViewHolder(columnViewHolder.viewRoot) {
companion object { companion object {
val log = LogCategory("TabletColumnViewHolder") val log = LogCategory("TabletColumnViewHolder")
} }
private var pageIndex = - 1 private var pageIndex = -1
fun bind(column : Column, pageIndex : Int, column_count : Int) { fun bind(column: Column, pageIndex: Int, column_count: Int) {
log.d("bind. %d => %d ", this.pageIndex, pageIndex) log.d("bind. ${this.pageIndex} => ${pageIndex}")
columnViewHolder.onPageDestroy(this.pageIndex) columnViewHolder.onPageDestroy(this.pageIndex)
this.pageIndex = pageIndex this.pageIndex = pageIndex
columnViewHolder.onPageCreate(column, pageIndex, column_count) columnViewHolder.onPageCreate(column, pageIndex, column_count)
if(! column.bFirstInitialized) { if (!column.bFirstInitialized) {
column.startLoading() column.startLoading()
} }
} }
fun onViewRecycled() { fun onViewRecycled() {
log.d("onViewRecycled %d", pageIndex) log.d("onViewRecycled ${pageIndex}")
columnViewHolder.onPageDestroy(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) UserRelation.saveListMisskey(now, column.access_info.db_id, who_list, start, step)
start += step start += step
} }
Column.log.d("updateRelation: update %d relations.", end) Column.log.d("updateRelation: update ${end} relations.")
} }
// 2018/11/1 Misskeyにもリレーション取得APIができた // 2018/11/1 Misskeyにもリレーション取得APIができた
@ -108,7 +108,7 @@ class UpdateRelationEnv(val column: Column) {
UserRelation.saveList2(now, column.access_info.db_id, list) 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 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) AcctSet.saveList(now, acct_list, n, length)
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) TagSet.saveList(now, tag_list, n, length)
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() val response = call.await()
log.e("appServerUnregister: %s", response) log.e("appServerUnregister: ${response}")
} catch (ex: Throwable) { } catch (ex: Throwable) {
log.trace(ex, "appServerUnregister failed.") 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) val (result, status) = client.syncStatus(access_info, remote_status_url)
if (status != null) { if (status != null) {
local_status_id = status.id 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 result
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -77,7 +77,7 @@ object NotestockHelper {
parseList(parser, data) parseList(parser, data)
.also { .also {
if (it.isEmpty()) 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) parseList(parser, root)
.also { .also {
if (it.isEmpty()) 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.e(ex, "load failed.")
} }
log.d( log.d("lruCache size=${mMemoryCache.size()},hit=${mMemoryCache.hitCount()},miss=${mMemoryCache.missCount()}")
"lruCache size=%s,hit=%s,miss=%s",
mMemoryCache.size(),
mMemoryCache.hitCount(),
mMemoryCache.missCount()
)
val ac = AcctColor(key, acctPretty) val ac = AcctColor(key, acctPretty)
mMemoryCache.put(key, ac) mMemoryCache.put(key, ac)
return ac return ac

View File

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

View File

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

View File

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

View File

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

View File

@ -350,7 +350,7 @@ class PostHelper(
if (visibility == checkVisibility && !checkFun(instance)) { if (visibility == checkVisibility && !checkFun(instance)) {
val strVisibility = Styler.getVisibilityString(activity, account.isMisskey, checkVisibility) val strVisibility = Styler.getVisibilityString(activity, account.isMisskey, checkVisibility)
return@runApiTask TootApiResult( 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 limit = 100
val s = src.substring(start, end) val s = src.substring(start, end)
val acct_list = AcctSet.searchPrefix(s, limit) 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()) { if (acct_list.isEmpty()) {
closeAcctPopup() closeAcctPopup()
} else { } else {
@ -811,7 +811,7 @@ class PostHelper(
val limit = 100 val limit = 100
val s = src.substring(last_sharp + 1, end) val s = src.substring(last_sharp + 1, end)
val tag_list = TagSet.searchPrefix(s, limit) 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()) { if (tag_list.isEmpty()) {
closeAcctPopup() closeAcctPopup()
} else { } else {
@ -863,7 +863,7 @@ class PostHelper(
val s = val s =
src.substring(last_colon + 1, end).lowercase().replace('-', '_') src.substring(last_colon + 1, end).lowercase().replace('-', '_')
val matches = EmojiDecoder.searchShortCode(activity, s, remain) 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) code_list.addAll(matches)
} }

View File

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

View File

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

View File

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

View File

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

View File

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