From 5f7f486471edf66ed55a97b210f830b0ad8a3f41 Mon Sep 17 00:00:00 2001 From: tateisu Date: Sun, 13 Jun 2021 20:48:48 +0900 Subject: [PATCH] refactor --- .../jp/juggler/subwaytooter/ActAppSetting.kt | 2 +- .../jp/juggler/subwaytooter/ActCallback.kt | 3 +- .../jp/juggler/subwaytooter/ActMediaViewer.kt | 2 +- .../juggler/subwaytooter/AppDataExporter.kt | 4 +- .../java/jp/juggler/subwaytooter/AppState.kt | 74 +- .../jp/juggler/subwaytooter/ColumnFilters.kt | 12 +- .../subwaytooter/ColumnTask_Loading.kt | 10 +- .../subwaytooter/ColumnTask_Refresh.kt | 2 - .../juggler/subwaytooter/ColumnViewHolder.kt | 32 +- .../subwaytooter/ColumnViewHolderLifecycle.kt | 6 +- .../subwaytooter/ColumnViewHolderLoading.kt | 23 +- .../jp/juggler/subwaytooter/EventReceiver.kt | 2 +- .../MyFirebaseMessagingService.kt | 76 +- .../subwaytooter/TabletColumnViewHolder.kt | 56 +- .../juggler/subwaytooter/UpdateRelationEnv.kt | 10 +- .../subwaytooter/action/Action_Account.kt | 2 +- .../action/Action_Conversation.kt | 2 +- .../subwaytooter/api/entity/TootList.kt | 231 +- .../subwaytooter/api/entity/TootPayload.kt | 162 +- .../subwaytooter/api/entity/TootStatus.kt | 28 +- .../notification/PollingForegrounder.kt | 2 +- .../notification/PollingWorker.kt | 7 +- .../subwaytooter/search/NotestockHelper.kt | 2 +- .../subwaytooter/search/TootsearchHelper.kt | 2 +- .../juggler/subwaytooter/table/AcctColor.kt | 7 +- .../table/NotificationTracking.kt | 9 +- .../juggler/subwaytooter/table/PostDraft.kt | 324 +-- .../subwaytooter/util/CustomEmojiCache.kt | 139 +- .../subwaytooter/util/CustomEmojiLister.kt | 594 ++--- .../util/MisskeyMarkdownDecoder.kt | 1961 +++++++++-------- .../juggler/subwaytooter/util/PostHelper.kt | 8 +- .../jp/juggler/subwaytooter/util/TaskList.kt | 149 +- .../juggler/subwaytooter/view/MyListView.kt | 74 +- .../subwaytooter/view/MyNetworkImageView.kt | 1090 ++++----- .../subwaytooter/view/PinchBitmapView.kt | 973 ++++---- .../main/java/jp/juggler/util/BitmapUtils.kt | 6 +- .../main/java/jp/juggler/util/StorageUtils.kt | 240 +- 37 files changed, 3140 insertions(+), 3186 deletions(-) diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActAppSetting.kt b/app/src/main/java/jp/juggler/subwaytooter/ActAppSetting.kt index f2b0a8fb..3bc30809 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActAppSetting.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ActAppSetting.kt @@ -840,7 +840,7 @@ class ActAppSetting : AppCompatActivity(), ColorPickerDialogListener, View.OnCli private fun importAppData2(bConfirm: Boolean, uri: Uri) { val type = contentResolver.getType(uri) - log.d("importAppData type=%s", type) + log.d("importAppData type=$type") if (!bConfirm) { AlertDialog.Builder(this) diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActCallback.kt b/app/src/main/java/jp/juggler/subwaytooter/ActCallback.kt index 7fd63144..df5840d1 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActCallback.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ActCallback.kt @@ -7,6 +7,7 @@ import androidx.appcompat.app.AppCompatActivity import androidx.core.content.FileProvider import jp.juggler.util.LogCategory import jp.juggler.util.digestSHA256Hex +import okhttp3.internal.toHexString import org.apache.commons.io.IOUtils import java.io.File import java.io.FileOutputStream @@ -32,7 +33,7 @@ class ActCallback : AppCompatActivity() { } override fun onCreate(savedInstanceState : Bundle?) { - log.d("onCreate flags=%x", intent.flags) + log.d("onCreate flags=0x${intent.flags.toHexString()}") super.onCreate(savedInstanceState) var intent : Intent? = intent diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActMediaViewer.kt b/app/src/main/java/jp/juggler/subwaytooter/ActMediaViewer.kt index bee2c412..fea26bd6 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActMediaViewer.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ActMediaViewer.kt @@ -141,7 +141,7 @@ class ActMediaViewer : AppCompatActivity(), View.OnClickListener { } override fun onRepeatModeChanged(repeatMode: Int) { - log.d("exoPlayer onRepeatModeChanged %d", repeatMode) + log.d("exoPlayer onRepeatModeChanged $repeatMode", ) } override fun onPlayerError(error: ExoPlaybackException) { diff --git a/app/src/main/java/jp/juggler/subwaytooter/AppDataExporter.kt b/app/src/main/java/jp/juggler/subwaytooter/AppDataExporter.kt index 3e909af4..01d2e1b8 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/AppDataExporter.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/AppDataExporter.kt @@ -135,14 +135,14 @@ object AppDataExporter { Cursor.FIELD_TYPE_FLOAT -> { val d = cursor.getDouble(i) if(d.isNaN() || d.isInfinite()) { - log.w("column %s is nan or infinite value.", names[i]) + log.w("column ${names[i]} is nan or infinite value.") } else { writer.name(names[i]) writer.value(d) } } - Cursor.FIELD_TYPE_BLOB -> log.w("column %s is blob.", names[i]) + Cursor.FIELD_TYPE_BLOB -> log.w("column ${names[i]} is blob." ) } } diff --git a/app/src/main/java/jp/juggler/subwaytooter/AppState.kt b/app/src/main/java/jp/juggler/subwaytooter/AppState.kt index 6b3a43e0..36be3c5f 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/AppState.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/AppState.kt @@ -20,8 +20,6 @@ import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.subwaytooter.util.NetworkStateTracker import jp.juggler.subwaytooter.util.PostAttachment import jp.juggler.util.* -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch import org.apache.commons.io.IOUtils import java.io.ByteArrayOutputStream import java.io.File @@ -37,14 +35,14 @@ enum class DedupMode { } class DedupItem( - val text: String, - var time: Long = SystemClock.elapsedRealtime() + val text: String, + var time: Long = SystemClock.elapsedRealtime() ) class AppState( - internal val context: Context, - internal val handler: Handler, - internal val pref: SharedPreferences + internal val context: Context, + internal val handler: Handler, + internal val pref: SharedPreferences ) { companion object { @@ -129,9 +127,9 @@ class AppState( private val _columnList = ArrayList() - // make shallow copy - val columnList: List - get() = synchronized(_columnList) { ArrayList(_columnList) } + // make shallow copy + val columnList: List + get() = synchronized(_columnList) { ArrayList(_columnList) } val columnCount: Int get() = synchronized(_columnList) { _columnList.size } @@ -140,7 +138,7 @@ class AppState( synchronized(_columnList) { _columnList.elementAtOrNull(i) } fun columnIndex(column: Column?) = - synchronized(_columnList) { _columnList.indexOf(column).takeIf{ it != -1 } } + synchronized(_columnList) { _columnList.indexOf(column).takeIf { it != -1 } } fun editColumnList(save: Boolean = true, block: (ArrayList) -> Unit) { synchronized(_columnList) { @@ -230,10 +228,10 @@ class AppState( restartTTS() } else { log.d( - "proc_flushSpeechQueue: tts is speaking. queue_count=%d, expire_remain=%.3f", - queue_count, - expire_remain / 1000f - ) + "proc_flushSpeechQueue: tts is speaking. queue_count=${queue_count}, expire_remain=${ + String.format("%.3f",expire_remain / 1000f) + }" + ) handler.postDelayed(this, expire_remain) return } @@ -241,7 +239,7 @@ class AppState( } val sv = tts_queue.removeFirst() - log.d("proc_flushSpeechQueue: speak %s", sv) + log.d("proc_flushSpeechQueue: speak ${sv}") val voice_count = voice_list.size if (voice_count > 0) { @@ -251,11 +249,11 @@ class AppState( tts_speak_start = now tts.speak( - sv, - TextToSpeech.QUEUE_ADD, - null, // Bundle params - (++utteranceIdSeed).toString() // String utteranceId - ) + sv, + TextToSpeech.QUEUE_ADD, + null, // Bundle params + (++utteranceIdSeed).toString() // String utteranceId + ) } catch (ex: Throwable) { log.trace(ex) log.e(ex, "proc_flushSpeechQueue catch exception.") @@ -277,7 +275,7 @@ class AppState( columnList.mapIndexedNotNull { index, column -> try { val dst = JsonObject() - ColumnEncoder.encode(column,dst, index) + ColumnEncoder.encode(column, dst, index) dst } catch (ex: JsonException) { log.trace(ex) @@ -381,7 +379,7 @@ class AppState( context.showToast(false, R.string.text_to_speech_initializing) log.d("initializing TextToSpeech…") - launchIO{ + launchIO { var tmp_tts: TextToSpeech? = null @@ -391,11 +389,11 @@ class AppState( val tts = tmp_tts if (tts == null || TextToSpeech.SUCCESS != status) { context.showToast( - false, - R.string.text_to_speech_initialize_failed, - status - ) - log.d("speech initialize failed. status=%s", status) + false, + R.string.text_to_speech_initialize_failed, + status + ) + log.d("speech initialize failed. status=${status}" ) return@OnInitListener } @@ -423,15 +421,8 @@ class AppState( } else { val lang = defaultLocale(context).toLanguageTag() for (v in voice_set) { - log.d( - "Voice %s %s %s", - v.name, - v.locale.toLanguageTag(), - lang - ) - if (lang != v.locale.toLanguageTag()) { - continue - } + log.d( "Voice ${ v.name} ${ v.locale.toLanguageTag()} ${lang}" ) + if (lang != v.locale.toLanguageTag()) continue voice_list.add(v) } } @@ -443,9 +434,9 @@ class AppState( handler.post(proc_flushSpeechQueue) context.registerReceiver( - tts_receiver, - IntentFilter(TextToSpeech.ACTION_TTS_QUEUE_PROCESSING_COMPLETED) - ) + tts_receiver, + IntentFilter(TextToSpeech.ACTION_TTS_QUEUE_PROCESSING_COMPLETED) + ) // tts.setOnUtteranceProgressListener( new UtteranceProgressListener() { // @Override public void onStart( String utteranceId ){ @@ -603,7 +594,8 @@ class AppState( } if (item.sound_type == HighlightWord.SOUND_TYPE_CUSTOM && item.sound_uri.mayUri() - .tryRingtone()) return + .tryRingtone() + ) return RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION).tryRingtone() } diff --git a/app/src/main/java/jp/juggler/subwaytooter/ColumnFilters.kt b/app/src/main/java/jp/juggler/subwaytooter/ColumnFilters.kt index 9c854046..22a8c6d4 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ColumnFilters.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ColumnFilters.kt @@ -329,7 +329,7 @@ fun Column.isFiltered(item: TootNotification): Boolean { TootNotification.TYPE_FOLLOW_REQUEST_ACCEPTED_MISSKEY -> { val who = item.account if (who != null && favMuteSet?.contains(access_info.getFullAcct(who)) == true) { - Column.log.d("%s is in favMuteSet.", access_info.getFullAcct(who)) + Column.log.d("${access_info.getFullAcct(who)} is in favMuteSet.") return true } } @@ -404,21 +404,21 @@ fun Column.checkFiltersForListData(trees: FilterTrees?) { } fun reloadFilter(context: Context, access_info: SavedAccount) { - launchMain{ + launchMain { var resultList: ArrayList? = null context.runApiTask( access_info, progressStyle = ApiTask.PROGRESS_NONE - ){client-> - client.request(ApiPath.PATH_FILTERS)?.also{ result-> - result.jsonArray?.let{ + ) { client -> + client.request(ApiPath.PATH_FILTERS)?.also { result -> + result.jsonArray?.let { resultList = TootFilter.parseList(it) } } } - resultList?.let{ + resultList?.let { Column.log.d("update filters for ${access_info.acct.pretty}") for (column in App1.getAppState(context).columnList) { if (column.access_info == access_info) { diff --git a/app/src/main/java/jp/juggler/subwaytooter/ColumnTask_Loading.kt b/app/src/main/java/jp/juggler/subwaytooter/ColumnTask_Loading.kt index 406cd046..dc8c1cb1 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ColumnTask_Loading.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ColumnTask_Loading.kt @@ -258,6 +258,7 @@ class ColumnTask_Loading( column.saveRange(bBottom = true, bTop = true, result = result, list = src) } else { column.saveRangeTop(result, src) + true } return when { @@ -416,6 +417,7 @@ class ColumnTask_Loading( column.saveRange(bBottom = true, bTop = true, result = result, list = src) } else { column.saveRangeTop(result, src) + true } return when { @@ -998,7 +1000,7 @@ class ColumnTask_Loading( target_status.conversation_main = true // 祖先 - val list_asc = java.util.ArrayList() + val list_asc = ArrayList() while (true) { if (client.isApiCancelled) return null queryParams["offset"] = list_asc.size @@ -1012,7 +1014,7 @@ class ColumnTask_Loading( } // 直接の子リプライ。(子孫をたどることまではしない) - val list_desc = java.util.ArrayList() + val list_desc = ArrayList() val idSet = HashSet() var untilId: EntityId? = null @@ -1048,7 +1050,7 @@ class ColumnTask_Loading( } // 一つのリストにまとめる - this.list_tmp = java.util.ArrayList( + this.list_tmp = ArrayList( list_asc.size + list_desc.size + 2 ).apply { addAll(list_asc.sortedBy { it.time_created_at }) @@ -1084,7 +1086,7 @@ class ColumnTask_Loading( target_status.conversation_main = true if (conversation_context != null) { - this.list_tmp = java.util.ArrayList( + this.list_tmp = ArrayList( 1 + (conversation_context.ancestors?.size ?: 0) + (conversation_context.descendants?.size ?: 0) diff --git a/app/src/main/java/jp/juggler/subwaytooter/ColumnTask_Refresh.kt b/app/src/main/java/jp/juggler/subwaytooter/ColumnTask_Refresh.kt index 3e8d76b2..7ca5cbcc 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ColumnTask_Refresh.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ColumnTask_Refresh.kt @@ -103,8 +103,6 @@ class ColumnTask_Refresh( sp = holder.scrollPosition } - - if (bBottom) { val changeList = listOf( AdapterChange( diff --git a/app/src/main/java/jp/juggler/subwaytooter/ColumnViewHolder.kt b/app/src/main/java/jp/juggler/subwaytooter/ColumnViewHolder.kt index 08663c12..40a39e7b 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ColumnViewHolder.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ColumnViewHolder.kt @@ -237,21 +237,17 @@ class ColumnViewHolder( val column = this@ColumnViewHolder.column if (column == null) { - log.d("restoreScrollPosition [%d], column==null", page_idx) + log.d("restoreScrollPosition [${page_idx}], column==null") return } if (column.is_dispose.get()) { - log.d("restoreScrollPosition [%d], column is disposed", page_idx) + log.d("restoreScrollPosition [${page_idx}], column is disposed") return } if (column.hasMultipleViewHolder()) { - log.d( - "restoreScrollPosition [%d] %s , column has multiple view holder. retry later.", - page_idx, - column.getColumnName(true) - ) + log.d("restoreScrollPosition [${page_idx}] ${column.getColumnName(true)}, column has multiple view holder. retry later.") // タブレットモードでカラムを追加/削除した際に発生する。 // このタイミングでスクロール位置を復元してもうまくいかないので延期する @@ -277,30 +273,16 @@ class ColumnViewHolder( // } // } - log.d( - "restoreScrollPosition [$page_idx] %s , column has no saved scroll position.", - column.getColumnName(true) - ) + log.d("restoreScrollPosition [$page_idx] ${column.getColumnName(true)} , column has no saved scroll position.") return } column.scroll_save = null if (listView.visibility != View.VISIBLE) { - log.d( - "restoreScrollPosition [$page_idx] %s , listView is not visible. saved position %s,%s is dropped.", - column.getColumnName(true), - sp.adapterIndex, - sp.offset - ) + log.d("restoreScrollPosition [$page_idx] ${column.getColumnName(true)} , listView is not visible. saved position ${sp.adapterIndex},${sp.offset} is dropped.") } else { - log.d( - "restoreScrollPosition [%d] %s , listView is visible. resume %s,%s", - page_idx, - column.getColumnName(true), - sp.adapterIndex, - sp.offset - ) + log.d("restoreScrollPosition [${page_idx}] ${column.getColumnName(true)} , listView is visible. resume ${sp.adapterIndex},${sp.offset}") sp.restore(this@ColumnViewHolder) } @@ -418,7 +400,7 @@ class ColumnViewHolder( llColumnHeader, llRefreshError, - ).forEach { it.setOnClickListener(this) } + ).forEach { it.setOnClickListener(this) } btnColumnClose.setOnLongClickListener(this) diff --git a/app/src/main/java/jp/juggler/subwaytooter/ColumnViewHolderLifecycle.kt b/app/src/main/java/jp/juggler/subwaytooter/ColumnViewHolderLifecycle.kt index cdbdca70..7f46e90f 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ColumnViewHolderLifecycle.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ColumnViewHolderLifecycle.kt @@ -53,7 +53,7 @@ fun ColumnViewHolder.loadBackgroundImage(iv: ImageView, url: String?) { val screen_h = iv.resources.displayMetrics.heightPixels // 非同期処理を開始 - last_image_task = launchMain{ + last_image_task = launchMain { val bitmap = try { withContext(Dispatchers.IO) { try { @@ -93,7 +93,7 @@ fun ColumnViewHolder.onPageDestroy(page_idx: Int) { // タブレットモードの場合、onPageCreateより前に呼ばれる val column = this.column if (column != null) { - ColumnViewHolder.log.d("onPageDestroy [%d] %s", page_idx, tvColumnName.text) + ColumnViewHolder.log.d("onPageDestroy [${page_idx}] ${tvColumnName.text}") saveScrollPosition() listView.adapter = null column.removeColumnViewHolder(this) @@ -111,7 +111,7 @@ fun ColumnViewHolder.onPageCreate(column: Column, page_idx: Int, page_count: Int this.column = column this.page_idx = page_idx - ColumnViewHolder.log.d("onPageCreate [%d] %s", page_idx, column.getColumnName(true)) + ColumnViewHolder.log.d("onPageCreate [${page_idx}] ${column.getColumnName(true)}") val bSimpleList = column.type != ColumnType.CONVERSATION && Pref.bpSimpleList(activity.pref) diff --git a/app/src/main/java/jp/juggler/subwaytooter/ColumnViewHolderLoading.kt b/app/src/main/java/jp/juggler/subwaytooter/ColumnViewHolderLoading.kt index 3a136073..0ab92235 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ColumnViewHolderLoading.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ColumnViewHolderLoading.kt @@ -209,22 +209,17 @@ fun ColumnViewHolder.scrollToTop2() { fun ColumnViewHolder.saveScrollPosition(): Boolean { val column = this.column when { - column == null -> ColumnViewHolder.log.d("saveScrollPosition [%d] , column==null", page_idx) + column == null -> + ColumnViewHolder.log.d("saveScrollPosition [${page_idx}] , column==null") - column.is_dispose.get() -> ColumnViewHolder.log.d( - "saveScrollPosition [%d] , column is disposed", - page_idx - ) + column.is_dispose.get() -> + ColumnViewHolder.log.d("saveScrollPosition [${page_idx}] , column is disposed") listView.visibility != View.VISIBLE -> { val scroll_save = ScrollPosition() column.scroll_save = scroll_save ColumnViewHolder.log.d( - "saveScrollPosition [%d] %s , listView is not visible, save %s,%s", - page_idx, - column.getColumnName(true), - scroll_save.adapterIndex, - scroll_save.offset + "saveScrollPosition [${page_idx}] ${column.getColumnName(true)} , listView is not visible, save ${scroll_save.adapterIndex},${scroll_save.offset}" ) return true } @@ -233,11 +228,7 @@ fun ColumnViewHolder.saveScrollPosition(): Boolean { val scroll_save = ScrollPosition(this) column.scroll_save = scroll_save ColumnViewHolder.log.d( - "saveScrollPosition [%d] %s , listView is visible, save %s,%s", - page_idx, - column.getColumnName(true), - scroll_save.adapterIndex, - scroll_save.offset + "saveScrollPosition [${page_idx}] ${column.getColumnName(true)} , listView is visible, save ${scroll_save.adapterIndex},${scroll_save.offset}" ) return true } @@ -262,7 +253,7 @@ fun ColumnViewHolder.setScrollPosition(sp: ScrollPosition, deltaDp: Float = 0f) listLayoutManager.scrollVerticallyBy(dy, recycler, state) } catch (ex: Throwable) { ColumnViewHolder.log.trace(ex) - ColumnViewHolder.log.e("can't access field in class %s", RecyclerView::class.java.simpleName) + ColumnViewHolder.log.e("can't access field in class ${RecyclerView::class.java.simpleName}") } }, 20L) } diff --git a/app/src/main/java/jp/juggler/subwaytooter/EventReceiver.kt b/app/src/main/java/jp/juggler/subwaytooter/EventReceiver.kt index dd2b5382..98e7372f 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/EventReceiver.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/EventReceiver.kt @@ -26,7 +26,7 @@ class EventReceiver : BroadcastReceiver() { ACTION_NOTIFICATION_DELETE -> PollingWorker.queueNotificationDeleted( context,intent.data) - else -> log.e("onReceive: unsupported action %s", action) + else -> log.e("onReceive: unsupported action ${action}") } } } diff --git a/app/src/main/java/jp/juggler/subwaytooter/MyFirebaseMessagingService.kt b/app/src/main/java/jp/juggler/subwaytooter/MyFirebaseMessagingService.kt index 741e8531..0a03acad 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/MyFirebaseMessagingService.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/MyFirebaseMessagingService.kt @@ -11,44 +11,44 @@ import jp.juggler.subwaytooter.notification.PollingWorker import jp.juggler.util.LogCategory class MyFirebaseMessagingService : FirebaseMessagingService() { - - companion object { - internal val log = LogCategory("MyFirebaseMessagingService") - } - - override fun onMessageReceived(remoteMessage : RemoteMessage) { - - super.onMessageReceived(remoteMessage) - - var tag : String? = null - val data = remoteMessage.data - for((key, value) in data) { - log.d("onMessageReceived: %s=%s", key, value) - when(key){ - "notification_tag" -> tag = value - "acct" -> tag= "acct<>$value" - } - } - - val context = applicationContext - val intent = Intent(context, PollingForegrounder::class.java) - if(tag != null) intent.putExtra(PollingWorker.EXTRA_TAG, tag) - if(Build.VERSION.SDK_INT >= 26) { - context.startForegroundService(intent) - } else { - context.startService(intent) - } - } - override fun onNewToken(token : String) { - try { - log.d("onTokenRefresh: token=%s", token) - PrefDevice.prefDevice(this).edit().putString(PrefDevice.KEY_DEVICE_TOKEN, token).apply() + companion object { + internal val log = LogCategory("MyFirebaseMessagingService") + } - PollingWorker.queueFCMTokenUpdated(this) - - } catch(ex : Throwable) { - log.trace(ex) - } - } + override fun onMessageReceived(remoteMessage: RemoteMessage) { + + super.onMessageReceived(remoteMessage) + + var tag: String? = null + val data = remoteMessage.data + for ((key, value) in data) { + log.d("onMessageReceived: ${key}=${value}") + when (key) { + "notification_tag" -> tag = value + "acct" -> tag = "acct<>$value" + } + } + + val context = applicationContext + val intent = Intent(context, PollingForegrounder::class.java) + if (tag != null) intent.putExtra(PollingWorker.EXTRA_TAG, tag) + if (Build.VERSION.SDK_INT >= 26) { + context.startForegroundService(intent) + } else { + context.startService(intent) + } + } + + override fun onNewToken(token: String) { + try { + log.d("onTokenRefresh: token=${token}") + PrefDevice.prefDevice(this).edit().putString(PrefDevice.KEY_DEVICE_TOKEN, token).apply() + + PollingWorker.queueFCMTokenUpdated(this) + + } catch (ex: Throwable) { + log.trace(ex) + } + } } diff --git a/app/src/main/java/jp/juggler/subwaytooter/TabletColumnViewHolder.kt b/app/src/main/java/jp/juggler/subwaytooter/TabletColumnViewHolder.kt index dc1874cb..6109500a 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/TabletColumnViewHolder.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/TabletColumnViewHolder.kt @@ -5,33 +5,33 @@ import androidx.recyclerview.widget.RecyclerView import jp.juggler.util.LogCategory internal class TabletColumnViewHolder( - activity : ActMain, - parent: ViewGroup, - val columnViewHolder : ColumnViewHolder = ColumnViewHolder(activity,parent) + activity: ActMain, + parent: ViewGroup, + val columnViewHolder: ColumnViewHolder = ColumnViewHolder(activity, parent) ) : RecyclerView.ViewHolder(columnViewHolder.viewRoot) { - - companion object { - val log = LogCategory("TabletColumnViewHolder") - } - - private var pageIndex = - 1 - - fun bind(column : Column, pageIndex : Int, column_count : Int) { - log.d("bind. %d => %d ", this.pageIndex, pageIndex) - - columnViewHolder.onPageDestroy(this.pageIndex) - - this.pageIndex = pageIndex - - columnViewHolder.onPageCreate(column, pageIndex, column_count) - - if(! column.bFirstInitialized) { - column.startLoading() - } - } - - fun onViewRecycled() { - log.d("onViewRecycled %d", pageIndex) - columnViewHolder.onPageDestroy(pageIndex) - } + + companion object { + val log = LogCategory("TabletColumnViewHolder") + } + + private var pageIndex = -1 + + fun bind(column: Column, pageIndex: Int, column_count: Int) { + log.d("bind. ${this.pageIndex} => ${pageIndex}") + + columnViewHolder.onPageDestroy(this.pageIndex) + + this.pageIndex = pageIndex + + columnViewHolder.onPageCreate(column, pageIndex, column_count) + + if (!column.bFirstInitialized) { + column.startLoading() + } + } + + fun onViewRecycled() { + log.d("onViewRecycled ${pageIndex}") + columnViewHolder.onPageDestroy(pageIndex) + } } diff --git a/app/src/main/java/jp/juggler/subwaytooter/UpdateRelationEnv.kt b/app/src/main/java/jp/juggler/subwaytooter/UpdateRelationEnv.kt index d6ac8f44..a6388afa 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/UpdateRelationEnv.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/UpdateRelationEnv.kt @@ -63,7 +63,7 @@ class UpdateRelationEnv(val column: Column) { UserRelation.saveListMisskey(now, column.access_info.db_id, who_list, start, step) start += step } - Column.log.d("updateRelation: update %d relations.", end) + Column.log.d("updateRelation: update ${end} relations.") } // 2018/11/1 Misskeyにもリレーション取得APIができた @@ -108,7 +108,7 @@ class UpdateRelationEnv(val column: Column) { UserRelation.saveList2(now, column.access_info.db_id, list) } } - Column.log.d("updateRelation: update %d relations.", n) + Column.log.d("updateRelation: update ${n} relations.") } @@ -139,7 +139,7 @@ class UpdateRelationEnv(val column: Column) { list ) } - Column.log.d("updateRelation: update %d relations.", n) + Column.log.d("updateRelation: update ${n} relations.") } } @@ -158,7 +158,7 @@ class UpdateRelationEnv(val column: Column) { AcctSet.saveList(now, acct_list, n, length) n += length } - Column.log.d("updateRelation: update %d acct.", n) + Column.log.d("updateRelation: update ${n} acct.") } @@ -177,7 +177,7 @@ class UpdateRelationEnv(val column: Column) { TagSet.saveList(now, tag_list, n, length) n += length } - Column.log.d("updateRelation: update %d tag.", n) + Column.log.d("updateRelation: update ${n} tag.") } } } diff --git a/app/src/main/java/jp/juggler/subwaytooter/action/Action_Account.kt b/app/src/main/java/jp/juggler/subwaytooter/action/Action_Account.kt index 9756acc4..db149dda 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/action/Action_Account.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/action/Action_Account.kt @@ -231,7 +231,7 @@ private fun appServerUnregister(context: Context, account: SavedAccount) { val response = call.await() - log.e("appServerUnregister: %s", response) + log.e("appServerUnregister: ${response}") } catch (ex: Throwable) { log.trace(ex, "appServerUnregister failed.") } diff --git a/app/src/main/java/jp/juggler/subwaytooter/action/Action_Conversation.kt b/app/src/main/java/jp/juggler/subwaytooter/action/Action_Conversation.kt index 1f9d732d..9a276e30 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/action/Action_Conversation.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/action/Action_Conversation.kt @@ -132,7 +132,7 @@ private fun ActMain.conversationRemote( val (result, status) = client.syncStatus(access_info, remote_status_url) if (status != null) { local_status_id = status.id - log.d("status id conversion %s => %s", remote_status_url, status.id) + log.d("status id conversion ${remote_status_url}=>${status.id}") } result } diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootList.kt b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootList.kt index c374754d..f5993c7d 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootList.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootList.kt @@ -8,120 +8,123 @@ import jp.juggler.util.asciiPattern import jp.juggler.util.groupEx import java.util.* -class TootList(parser:TootParser,src : JsonObject): TimelineItem(), Comparable { +class TootList(parser: TootParser, src: JsonObject) : TimelineItem(), Comparable { - val id : EntityId + val id: EntityId - val title : String? - - // タイトルの数字列部分は数字の大小でソートされるようにしたい - private val title_for_sort : ArrayList? - - - // 内部で使用する - var isRegistered : Boolean = false - - var userIds :ArrayList? = null - - init { - if( parser.serviceType == ServiceType.MISSKEY){ - id = EntityId.mayDefault(src.string("id") ) - title = src.string("name") ?: src.string("title") // v11,v10 - this.title_for_sort = makeTitleForSort(this.title) - val user_list = ArrayList() - userIds = user_list - src.jsonArray("userIds")?.forEach { - val id = EntityId.mayNull( it as? String ) - if(id != null ) user_list.add(id ) - } - }else { - id = EntityId.mayDefault(src.string("id") ) - title = src.string("title") - this.title_for_sort = makeTitleForSort(this.title) - } - } - - override fun getOrderId() = id - - companion object { - private var log = LogCategory("TootList") - - private val reNumber = """(\d+)""".asciiPattern() - - private fun makeTitleForSort(title : String?) : ArrayList { - val list = ArrayList() - if(title != null) { - val m = reNumber.matcher(title) - var last_end = 0 - while(m.find()) { - val match_start = m.start() - val match_end = m.end() - if(match_start > last_end) { - list.add(title.substring(last_end, match_start)) - } - try { - list.add(m.groupEx(1)!!.toLong()) - } catch(ex : Throwable) { - list.clear() - list.add(title) - return list - } - - last_end = match_end - } - val end = title.length - if(end > last_end) { - list.add(title.substring(last_end, end)) - } - } - return list - } - - private fun compareLong(a : Long, b : Long) : Int { - return a.compareTo(b) - } - - private fun compareString(a : String, b : String) : Int { - return a.compareTo(b) - } - } - - override fun compareTo(other : TootList) : Int { - val la = this.title_for_sort - val lb = other.title_for_sort - - if(la == null) { - return if(lb == null) 0 else - 1 - } else if(lb == null) { - return 1 - } - - val sa = la.size - val sb = lb.size - - var i = 0 - while(true) { - val oa = if(i >= sa) null else la[i] - val ob = if(i >= sb) null else lb[i] - - if(oa == null && ob == null) return 0 - - val delta = when { - oa == null -> - 1 - ob == null -> 1 - oa is Long && ob is Long -> compareLong(oa, ob) - oa is String && ob is String -> compareString(oa, ob) - else -> (ob is Long).b2i() - (oa is Long).b2i() - } - log.d( - "%s %s %s" - , oa - , if(delta < 0) "<" else if(delta > 0) ">" else "=" - , ob - ) - if(delta != 0) return delta - ++ i - } - } - + val title: String? + + // タイトルの数字列部分は数字の大小でソートされるようにしたい + private val title_for_sort: ArrayList? + + + // 内部で使用する + var isRegistered: Boolean = false + + var userIds: ArrayList? = null + + init { + if (parser.serviceType == ServiceType.MISSKEY) { + id = EntityId.mayDefault(src.string("id")) + title = src.string("name") ?: src.string("title") // v11,v10 + this.title_for_sort = makeTitleForSort(this.title) + val user_list = ArrayList() + userIds = user_list + src.jsonArray("userIds")?.forEach { + val id = EntityId.mayNull(it as? String) + if (id != null) user_list.add(id) + } + } else { + id = EntityId.mayDefault(src.string("id")) + title = src.string("title") + this.title_for_sort = makeTitleForSort(this.title) + } + } + + override fun getOrderId() = id + + companion object { + private var log = LogCategory("TootList") + + private val reNumber = """(\d+)""".asciiPattern() + + private fun makeTitleForSort(title: String?): ArrayList { + val list = ArrayList() + if (title != null) { + val m = reNumber.matcher(title) + var last_end = 0 + while (m.find()) { + val match_start = m.start() + val match_end = m.end() + if (match_start > last_end) { + list.add(title.substring(last_end, match_start)) + } + try { + list.add(m.groupEx(1)!!.toLong()) + } catch (ex: Throwable) { + list.clear() + list.add(title) + return list + } + + last_end = match_end + } + val end = title.length + if (end > last_end) { + list.add(title.substring(last_end, end)) + } + } + return list + } + + private fun compareLong(a: Long, b: Long): Int { + return a.compareTo(b) + } + + private fun compareString(a: String, b: String): Int { + return a.compareTo(b) + } + } + + override fun compareTo(other: TootList): Int { + val la = this.title_for_sort + val lb = other.title_for_sort + + if (la == null) { + return if (lb == null) 0 else -1 + } else if (lb == null) { + return 1 + } + + val sa = la.size + val sb = lb.size + + var i = 0 + while (true) { + val oa = if (i >= sa) null else la[i] + val ob = if (i >= sb) null else lb[i] + + if (oa == null && ob == null) return 0 + + val delta = when { + oa == null -> -1 + ob == null -> 1 + oa is Long && ob is Long -> compareLong(oa, ob) + oa is String && ob is String -> compareString(oa, ob) + else -> (ob is Long).b2i() - (oa is Long).b2i() + } + log.d( + "${oa} ${ + when { + delta < 0 -> "<" + delta > 0 -> ">" + else -> "=" + } + } ${ob}" + + ) + if (delta != 0) return delta + ++i + } + } } diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootPayload.kt b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootPayload.kt index 9959a4b1..5930cec7 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootPayload.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootPayload.kt @@ -4,88 +4,88 @@ import jp.juggler.subwaytooter.api.TootParser import jp.juggler.util.* object TootPayload { - - val log = LogCategory("TootPayload") - private val reNumber = "([-]?\\d+)".asciiPattern() - - // ストリーミングAPIのペイロード部分をTootStatus,TootNotification,整数IDのどれかに解釈する - fun parsePayload( - parser : TootParser, - event : String, - parent : JsonObject, - parent_text : String - ) : Any? { - try { - val payload = parent["payload"] ?: return null - - if(payload is JsonObject) { - return when(event) { - - // ここを通るケースはまだ確認できていない - "update" -> parser.status(payload) - - // ここを通るケースはまだ確認できていない - "notification" -> parser.notification(payload) - - // ここを通るケースはまだ確認できていない - else -> { - log.e("unknown payload(1). message=%s", parent_text) - null - } - } - } else if(payload is JsonArray) { - log.e("unknown payload(1b). message=%s", parent_text) - return null - } - - if(payload is Number) { - // 2017/8/24 18:37 mastodon.juggler.jpでここを通った - return payload.toLong() - } - - if(payload is String) { - - if(payload[0] == '{') { - val src = payload.decodeJsonObject() - return when(event) { - // 2017/8/24 18:37 mastodon.juggler.jpでここを通った - "update" -> parser.status(src) - - // 2017/8/24 18:37 mastodon.juggler.jpでここを通った - "notification" -> parser.notification(src) - - "conversation" -> parseItem(::TootConversationSummary, parser, src) - - "announcement" -> parseItem(::TootAnnouncement, parser, src) + val log = LogCategory("TootPayload") - "emoji_reaction", - "announcement.reaction" -> parseItem(TootReaction::parseFedibird, src) + private val reNumber = "([-]?\\d+)".asciiPattern() - else -> { - log.e("unknown payload(2). message=%s", parent_text) - // ここを通るケースはまだ確認できていない - } - } - } else if(payload[0] == '[') { - log.e("unknown payload(2b). message=%s", parent_text) - return null - } - - // 2017/8/24 18:37 mdx.ggtea.org でここを通った - val m = reNumber.matcher(payload) - if(m.find()) { - return m.groupEx(1) !!.toLong(10) - } - } - - // ここを通るケースはまだ確認できていない - log.e("unknown payload(3). message=%s", parent_text) - - } catch(ex : Throwable) { - log.trace(ex) - } - - return null - } + // ストリーミングAPIのペイロード部分をTootStatus,TootNotification,整数IDのどれかに解釈する + fun parsePayload( + parser: TootParser, + event: String, + parent: JsonObject, + parent_text: String + ): Any? { + try { + val payload = parent["payload"] ?: return null + + if (payload is JsonObject) { + return when (event) { + + // ここを通るケースはまだ確認できていない + "update" -> parser.status(payload) + + // ここを通るケースはまだ確認できていない + "notification" -> parser.notification(payload) + + // ここを通るケースはまだ確認できていない + else -> { + log.e("unknown payload(1). message=${parent_text}") + null + } + } + } else if (payload is JsonArray) { + log.e("unknown payload(1b). message=${parent_text}") + return null + } + + if (payload is Number) { + // 2017/8/24 18:37 mastodon.juggler.jpでここを通った + return payload.toLong() + } + + if (payload is String) { + + if (payload[0] == '{') { + val src = payload.decodeJsonObject() + return when (event) { + // 2017/8/24 18:37 mastodon.juggler.jpでここを通った + "update" -> parser.status(src) + + // 2017/8/24 18:37 mastodon.juggler.jpでここを通った + "notification" -> parser.notification(src) + + "conversation" -> parseItem(::TootConversationSummary, parser, src) + + "announcement" -> parseItem(::TootAnnouncement, parser, src) + + "emoji_reaction", + "announcement.reaction" -> parseItem(TootReaction::parseFedibird, src) + + else -> { + log.e("unknown payload(2). message=${parent_text}") + // ここを通るケースはまだ確認できていない + } + } + } else if (payload[0] == '[') { + log.e("unknown payload(2b). message=${parent_text}") + return null + } + + // 2017/8/24 18:37 mdx.ggtea.org でここを通った + val m = reNumber.matcher(payload) + if (m.find()) { + return m.groupEx(1)!!.toLong(10) + } + } + + // ここを通るケースはまだ確認できていない + log.e("unknown payload(3). message=${parent_text}") + + } catch (ex: Throwable) { + log.trace(ex) + } + + return null + } } diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootStatus.kt b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootStatus.kt index ebcf7e77..bd5bb945 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootStatus.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootStatus.kt @@ -951,24 +951,24 @@ class TootStatus(parser: TootParser, src: JsonObject) : TimelineItem() { return list } - fun updateReactionMastodon( newReactionSet: TootReactionSet ) { + fun updateReactionMastodon(newReactionSet: TootReactionSet) { synchronized(this) { this.reactionSet = newReactionSet } } - fun updateReactionMastodonByEvent( newReaction: TootReaction ) { + fun updateReactionMastodonByEvent(newReaction: TootReaction) { synchronized(this) { var reactionSet = this.reactionSet - if( newReaction.count <= 0 ){ - reactionSet?.get(newReaction.name)?.let{ reactionSet?.remove(it) } - }else{ + if (newReaction.count <= 0) { + reactionSet?.get(newReaction.name)?.let { reactionSet?.remove(it) } + } else { if (reactionSet == null) { reactionSet = TootReactionSet(isMisskey = false) this.reactionSet = reactionSet } - when(val old = reactionSet[newReaction.name]) { + when (val old = reactionSet[newReaction.name]) { null -> reactionSet.add(newReaction) // 同一オブジェクトならマージは不要 @@ -1007,7 +1007,7 @@ class TootStatus(parser: TootParser, src: JsonObject) : TimelineItem() { if (byMe) { // 自分でリアクションしたらUIで更新した後にストリーミングイベントが届くことがある // その場合はカウントを変更しない - if(reactionSet.any{ it.me && it.name == code}) return false + if (reactionSet.any { it.me && it.name == code }) return false } log.d("increaseReaction noteId=$id byMe=$byMe caller=$caller") @@ -1017,7 +1017,7 @@ class TootStatus(parser: TootParser, src: JsonObject) : TimelineItem() { reactionSet[code]?.also { it.count = max(0, it.count + 1L) } ?: TootReaction(name = code, count = 1L).also { reactionSet.add(it) } - if(byMe) reaction.me = true + if (byMe) reaction.me = true return true } @@ -1037,7 +1037,7 @@ class TootStatus(parser: TootParser, src: JsonObject) : TimelineItem() { if (byMe) { // 自分でリアクションしたらUIで更新した後にストリーミングイベントが届くことがある // その場合はカウントを変更しない - if(reactionSet.any{ !it.me && it.name == code}) return false + if (reactionSet.any { !it.me && it.name == code }) return false } log.d("decreaseReaction noteId=$id byMe=$byMe caller=$caller") @@ -1046,7 +1046,7 @@ class TootStatus(parser: TootParser, src: JsonObject) : TimelineItem() { val reaction = reactionSet[code] ?.also { it.count = max(0L, it.count - 1L) } - if(byMe) reaction?.me = false + if (byMe) reaction?.me = false return true } @@ -1210,10 +1210,10 @@ class TootStatus(parser: TootParser, src: JsonObject) : TimelineItem() { m = reDate.matcher(strTime) if (m.find()) return parseTime("${strTime}T00:00:00.000Z") - log.w("invalid time format: %s", strTime) + log.w("invalid time format: ${strTime}") } catch (ex: Throwable) { // ParseException, ArrayIndexOutOfBoundsException log.trace(ex) - log.e(ex, "TootStatus.parseTime failed. src=%s", strTime) + log.e(ex, "TootStatus.parseTime failed. src=$strTime") } } return 0L @@ -1224,7 +1224,7 @@ class TootStatus(parser: TootParser, src: JsonObject) : TimelineItem() { try { val m = reMSPTime.matcher(strTime) if (!m.find()) { - log.d("invalid time format: %s", strTime) + log.d("invalid time format: $strTime") } else { val g = GregorianCalendar(tz_utc) g.set( @@ -1240,7 +1240,7 @@ class TootStatus(parser: TootParser, src: JsonObject) : TimelineItem() { } } catch (ex: Throwable) { // ParseException, ArrayIndexOutOfBoundsException log.trace(ex) - log.e(ex, "parseTimeMSP failed. src=%s", strTime) + log.e(ex, "parseTimeMSP failed. src=${strTime}" ) } } diff --git a/app/src/main/java/jp/juggler/subwaytooter/notification/PollingForegrounder.kt b/app/src/main/java/jp/juggler/subwaytooter/notification/PollingForegrounder.kt index 1b9280ff..32341708 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/notification/PollingForegrounder.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/notification/PollingForegrounder.kt @@ -103,7 +103,7 @@ class PollingForegrounder : IntentService("PollingForegrounder") { if (sv.isEmpty() || sv == last_status) return@handleFCMMessage // 状況が変化したらログと通知領域に出力する last_status = sv - log.d("onStatus %s", sv) + log.d("onStatus $sv") startForeground(NOTIFICATION_ID_FOREGROUNDER, createNotification(context, sv)) } } diff --git a/app/src/main/java/jp/juggler/subwaytooter/notification/PollingWorker.kt b/app/src/main/java/jp/juggler/subwaytooter/notification/PollingWorker.kt index 391ea159..c565004d 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/notification/PollingWorker.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/notification/PollingWorker.kt @@ -357,10 +357,7 @@ class PollingWorker private constructor(contextArg: Context) { // ジョブが完了した? val now = SystemClock.elapsedRealtime() if (!pw.hasJob(JobId.Push)) { - log.d( - "handleFCMMessage: JOB_FCM completed. time=%.2f", - (now - time_start) / 1000f - ) + log.d("handleFCMMessage: JOB_FCM completed. time=${String.format("%.2f", (now - time_start) / 1000f)}") break } @@ -398,7 +395,7 @@ class PollingWorker private constructor(contextArg: Context) { private val workerNotifier = Channel(capacity = Channel.CONFLATED) fun notifyWorker() = - workerNotifier.trySend(Unit) + workerNotifier.trySend(Unit) init { log.d("init") diff --git a/app/src/main/java/jp/juggler/subwaytooter/search/NotestockHelper.kt b/app/src/main/java/jp/juggler/subwaytooter/search/NotestockHelper.kt index c8221ee9..ccb378bc 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/search/NotestockHelper.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/search/NotestockHelper.kt @@ -77,7 +77,7 @@ object NotestockHelper { parseList(parser, data) .also { if (it.isEmpty()) - log.d("search result is empty. %s", result.bodyString) + log.d("search result is empty. ${result.bodyString}") } ) diff --git a/app/src/main/java/jp/juggler/subwaytooter/search/TootsearchHelper.kt b/app/src/main/java/jp/juggler/subwaytooter/search/TootsearchHelper.kt index ee02445e..a22ab4de 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/search/TootsearchHelper.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/search/TootsearchHelper.kt @@ -72,7 +72,7 @@ object TootsearchHelper { parseList(parser, root) .also { if (it.isEmpty()) - log.d("search result is empty. %s", result.bodyString) + log.d("search result is empty. ${result.bodyString}") } ) } diff --git a/app/src/main/java/jp/juggler/subwaytooter/table/AcctColor.kt b/app/src/main/java/jp/juggler/subwaytooter/table/AcctColor.kt index 662cc124..331e0765 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/table/AcctColor.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/table/AcctColor.kt @@ -164,12 +164,7 @@ class AcctColor { log.e(ex, "load failed.") } - log.d( - "lruCache size=%s,hit=%s,miss=%s", - mMemoryCache.size(), - mMemoryCache.hitCount(), - mMemoryCache.missCount() - ) + log.d("lruCache size=${mMemoryCache.size()},hit=${mMemoryCache.hitCount()},miss=${mMemoryCache.missCount()}") val ac = AcctColor(key, acctPretty) mMemoryCache.put(key, ac) return ac diff --git a/app/src/main/java/jp/juggler/subwaytooter/table/NotificationTracking.kt b/app/src/main/java/jp/juggler/subwaytooter/table/NotificationTracking.kt index 811ef293..89560fbf 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/table/NotificationTracking.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/table/NotificationTracking.kt @@ -69,14 +69,7 @@ class NotificationTracking { post_id.putTo(cv, COL_POST_ID) cv.put(COL_POST_TIME, post_time) val rows = App1.database.update(table, cv, WHERE_AID, arrayOf(account_db_id.toString(),notificationType)) - log.d( - "updatePost account_db_id=%s, nt=%s, post=%s,%s update_rows=%s" - , account_db_id - , notificationType - , post_id - , post_time - , rows - ) + log.d("updatePost account_db_id=${account_db_id}, nt=${notificationType}, post=${post_id},${post_time} update_rows=${rows}") dirty=false clearCache(account_db_id,notificationType) } catch(ex : Throwable) { diff --git a/app/src/main/java/jp/juggler/subwaytooter/table/PostDraft.kt b/app/src/main/java/jp/juggler/subwaytooter/table/PostDraft.kt index b051dda3..7ed65d4b 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/table/PostDraft.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/table/PostDraft.kt @@ -11,166 +11,166 @@ import jp.juggler.util.digestSHA256Hex import jp.juggler.util.decodeJsonObject class PostDraft { - - var id : Long = 0 - var time_save : Long = 0 - var json : JsonObject? = null - var hash : String? = null - - class ColIdx(cursor : Cursor) { - internal val idx_id : Int - internal val idx_time_save : Int - internal val idx_json : Int - internal val idx_hash : Int - - init { - idx_id = cursor.getColumnIndex(COL_ID) - idx_time_save = cursor.getColumnIndex(COL_TIME_SAVE) - idx_json = cursor.getColumnIndex(COL_JSON) - idx_hash = cursor.getColumnIndex(COL_HASH) - } - - } - - fun delete() { - try { - App1.database.delete(table, "$COL_ID=?", arrayOf(id.toString())) - } catch(ex : Throwable) { - log.e(ex, "delete failed.") - } - - } - - companion object : TableCompanion { - - private val log = LogCategory("PostDraft") - - private const val table = "post_draft" - private const val COL_ID = BaseColumns._ID - private const val COL_TIME_SAVE = "time_save" - private const val COL_JSON = "json" - private const val COL_HASH = "hash" - - override fun onDBCreate(db : SQLiteDatabase) { - log.d("onDBCreate!") - db.execSQL( - "create table if not exists " + table - + "(" + COL_ID + " INTEGER PRIMARY KEY" - + "," + COL_TIME_SAVE + " integer not null" - + "," + COL_JSON + " text not null" - + "," + COL_HASH + " text not null" - + ")" - ) - db.execSQL( - "create unique index if not exists " + table + "_hash on " + table + "(" + COL_HASH + ")" - ) - db.execSQL( - "create index if not exists " + table + "_time on " + table + "(" + COL_TIME_SAVE + ")" - ) - } - - override fun onDBUpgrade(db : SQLiteDatabase, oldVersion : Int, newVersion : Int) { - if(oldVersion < 12 && newVersion >= 12) { - onDBCreate(db) - } - } - - private fun deleteOld(now : Long) { - try { - // 古いデータを掃除する - val expire = now - 86400000L * 30 - App1.database.delete(table, "$COL_TIME_SAVE - val v = json[k]?.toString() ?: "(null)" - sb.append("&") - sb.append(k) - sb.append("=") - sb.append(v) - } - val hash = sb.toString().digestSHA256Hex() - - // save to db - App1.database.replace(table, null, ContentValues().apply { - put(COL_TIME_SAVE, now) - put(COL_JSON, json.toString()) - put(COL_HASH, hash) - }) - } catch(ex : Throwable) { - log.e(ex, "save failed.") - } - - } - - fun hasDraft() : Boolean { - try { - App1.database.query(table, arrayOf("count(*)"), null, null, null, null, null) - .use { cursor -> - if(cursor.moveToNext()) { - val count = cursor.getInt(0) - return count > 0 - } - } - } catch(ex : Throwable) { - log.trace(ex) - log.e(ex, "hasDraft failed.") - } - - return false - } - - fun createCursor() : Cursor? { - try { - return App1.database.query( - table, - null, - null, - null, - null, - null, - "$COL_TIME_SAVE desc" - ) - } catch(ex : Throwable) { - log.trace(ex) - log.e(ex, "createCursor failed.") - } - - return null - } - - fun loadFromCursor(cursor : Cursor, colIdxArg : ColIdx?, position : Int) : PostDraft? { - val colIdx = colIdxArg ?: ColIdx(cursor) - - if(! cursor.moveToPosition(position)) { - log.d("loadFromCursor: move failed. position=%s", position) - return null - } - - val dst = PostDraft() - dst.id = cursor.getLong(colIdx.idx_id) - dst.time_save = cursor.getLong(colIdx.idx_time_save) - try { - dst.json = cursor.getString(colIdx.idx_json).decodeJsonObject() - } catch(ex : Throwable) { - log.trace(ex) - dst.json = JsonObject() - } - - dst.hash = cursor.getString(colIdx.idx_hash) - return dst - } - } - + + var id: Long = 0 + var time_save: Long = 0 + var json: JsonObject? = null + var hash: String? = null + + class ColIdx(cursor: Cursor) { + internal val idx_id: Int + internal val idx_time_save: Int + internal val idx_json: Int + internal val idx_hash: Int + + init { + idx_id = cursor.getColumnIndex(COL_ID) + idx_time_save = cursor.getColumnIndex(COL_TIME_SAVE) + idx_json = cursor.getColumnIndex(COL_JSON) + idx_hash = cursor.getColumnIndex(COL_HASH) + } + + } + + fun delete() { + try { + App1.database.delete(table, "$COL_ID=?", arrayOf(id.toString())) + } catch (ex: Throwable) { + log.e(ex, "delete failed.") + } + + } + + companion object : TableCompanion { + + private val log = LogCategory("PostDraft") + + private const val table = "post_draft" + private const val COL_ID = BaseColumns._ID + private const val COL_TIME_SAVE = "time_save" + private const val COL_JSON = "json" + private const val COL_HASH = "hash" + + override fun onDBCreate(db: SQLiteDatabase) { + log.d("onDBCreate!") + db.execSQL( + "create table if not exists " + table + + "(" + COL_ID + " INTEGER PRIMARY KEY" + + "," + COL_TIME_SAVE + " integer not null" + + "," + COL_JSON + " text not null" + + "," + COL_HASH + " text not null" + + ")" + ) + db.execSQL( + "create unique index if not exists " + table + "_hash on " + table + "(" + COL_HASH + ")" + ) + db.execSQL( + "create index if not exists " + table + "_time on " + table + "(" + COL_TIME_SAVE + ")" + ) + } + + override fun onDBUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { + if (oldVersion < 12 && newVersion >= 12) { + onDBCreate(db) + } + } + + private fun deleteOld(now: Long) { + try { + // 古いデータを掃除する + val expire = now - 86400000L * 30 + App1.database.delete(table, "$COL_TIME_SAVE + val v = json[k]?.toString() ?: "(null)" + sb.append("&") + sb.append(k) + sb.append("=") + sb.append(v) + } + val hash = sb.toString().digestSHA256Hex() + + // save to db + App1.database.replace(table, null, ContentValues().apply { + put(COL_TIME_SAVE, now) + put(COL_JSON, json.toString()) + put(COL_HASH, hash) + }) + } catch (ex: Throwable) { + log.e(ex, "save failed.") + } + + } + + fun hasDraft(): Boolean { + try { + App1.database.query(table, arrayOf("count(*)"), null, null, null, null, null) + .use { cursor -> + if (cursor.moveToNext()) { + val count = cursor.getInt(0) + return count > 0 + } + } + } catch (ex: Throwable) { + log.trace(ex) + log.e(ex, "hasDraft failed.") + } + + return false + } + + fun createCursor(): Cursor? { + try { + return App1.database.query( + table, + null, + null, + null, + null, + null, + "$COL_TIME_SAVE desc" + ) + } catch (ex: Throwable) { + log.trace(ex) + log.e(ex, "createCursor failed.") + } + + return null + } + + fun loadFromCursor(cursor: Cursor, colIdxArg: ColIdx?, position: Int): PostDraft? { + val colIdx = colIdxArg ?: ColIdx(cursor) + + if (!cursor.moveToPosition(position)) { + log.d("loadFromCursor: move failed. position=${position}") + return null + } + + val dst = PostDraft() + dst.id = cursor.getLong(colIdx.idx_id) + dst.time_save = cursor.getLong(colIdx.idx_time_save) + try { + dst.json = cursor.getString(colIdx.idx_json).decodeJsonObject() + } catch (ex: Throwable) { + log.trace(ex) + dst.json = JsonObject() + } + + dst.hash = cursor.getString(colIdx.idx_hash) + return dst + } + } + } diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/CustomEmojiCache.kt b/app/src/main/java/jp/juggler/subwaytooter/util/CustomEmojiCache.kt index 2ad75e2d..aa3b9f60 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/util/CustomEmojiCache.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/util/CustomEmojiCache.kt @@ -26,8 +26,8 @@ import java.util.concurrent.TimeUnit import kotlin.math.ceil class CustomEmojiCache( - val context: Context, - private val handler: Handler + val context: Context, + private val handler: Handler ) { companion object { @@ -48,10 +48,10 @@ class CustomEmojiCache( } private class DbCache( - val id: Long, - val timeUsed: Long, - val data: ByteArray - ) { + val id: Long, + val timeUsed: Long, + val data: ByteArray + ) { companion object : TableCompanion { @@ -65,48 +65,48 @@ class CustomEmojiCache( override fun onDBCreate(db: SQLiteDatabase) { db.execSQL( - """create table if not exists $table + """create table if not exists $table ($COL_ID INTEGER PRIMARY KEY ,$COL_TIME_SAVE integer not null ,$COL_TIME_USED integer not null ,$COL_URL text not null ,$COL_DATA blob not null )""".trimIndent() - ) + ) db.execSQL("create unique index if not exists ${table}_url on ${table}($COL_URL)") db.execSQL("create index if not exists ${table}_old on ${table}($COL_TIME_USED)") } override fun onDBUpgrade( - db: SQLiteDatabase, - oldVersion: Int, - newVersion: Int - ) { + db: SQLiteDatabase, + oldVersion: Int, + newVersion: Int + ) { } fun load(db: SQLiteDatabase, url: String, now: Long) = db.rawQuery( - "select $COL_ID,$COL_TIME_USED,$COL_DATA from $table where $COL_URL=?", - arrayOf(url) - )?.use { cursor -> + "select $COL_ID,$COL_TIME_USED,$COL_DATA from $table where $COL_URL=?", + arrayOf(url) + )?.use { cursor -> if (cursor.count == 0) null else { cursor.moveToNext() DbCache( - id = cursor.getLong(cursor.getColumnIndex(COL_ID)), - timeUsed = cursor.getLong(cursor.getColumnIndex(COL_TIME_USED)), - data = cursor.getBlob(cursor.getColumnIndex(COL_DATA)) - ).apply { + id = cursor.getLong(cursor.getColumnIndex(COL_ID)), + timeUsed = cursor.getLong(cursor.getColumnIndex(COL_TIME_USED)), + data = cursor.getBlob(cursor.getColumnIndex(COL_DATA)) + ).apply { if (now - timeUsed >= 5 * 3600000L) { db.update( - table, - ContentValues().apply { - put(COL_TIME_USED, now) - }, - "$COL_ID=?", - arrayOf(id.toString()) - ) + table, + ContentValues().apply { + put(COL_TIME_USED, now) + }, + "$COL_ID=?", + arrayOf(id.toString()) + ) } } } @@ -115,23 +115,23 @@ class CustomEmojiCache( fun sweep(db: SQLiteDatabase, now: Long) { val expire = now - TimeUnit.DAYS.toMillis(30) db.delete( - table, - "$COL_TIME_USED < ?", - arrayOf(expire.toString()) - ) + table, + "$COL_TIME_USED < ?", + arrayOf(expire.toString()) + ) } fun update(db: SQLiteDatabase, url: String, data: ByteArray) { val now = System.currentTimeMillis() db.replace(table, - null, - ContentValues().apply { - put(COL_URL, url) - put(COL_DATA, data) - put(COL_TIME_USED, now) - put(COL_TIME_SAVE, now) - } - ) + null, + ContentValues().apply { + put(COL_URL, url) + put(COL_DATA, data) + put(COL_TIME_USED, now) + put(COL_TIME_SAVE, now) + } + ) } } } @@ -166,10 +166,10 @@ class CustomEmojiCache( } private class Request( - val refTarget: WeakReference, - val url: String, - val onLoadComplete: () -> Unit - ) + val refTarget: WeakReference, + val url: String, + val onLoadComplete: () -> Unit + ) // APNGデコード済のキャッシュデータ private val cache = ConcurrentHashMap() @@ -253,10 +253,10 @@ class CustomEmojiCache( } fun getFrames( - refDrawTarget: WeakReference?, - url: String, - onLoadComplete: () -> Unit - ): ApngFrames? { + refDrawTarget: WeakReference?, + url: String, + onLoadComplete: () -> Unit + ): ApngFrames? { try { if (refDrawTarget?.get() == null) { log.e("draw: DrawTarget is null ") @@ -353,13 +353,7 @@ class CustomEmojiCache( if (cache_used) continue - if (DEBUG) - log.d( - "start get image. queue_size=%d, cache_size=%d url=%s", - queue_size, - cache_size, - request.url - ) + if (DEBUG) log.d("start get image. queue_size=${queue_size}, cache_size=${cache_size} url=${request.url}") val now = System.currentTimeMillis() @@ -377,8 +371,7 @@ class CustomEmojiCache( data = try { App1.getHttpCached(request.url) } catch (ex: Throwable) { - log.e("get failed. url=%s", request.url) - log.trace(ex) + log.trace(ex, "get failed. url=${request.url}") null } te = elapsedTime @@ -473,7 +466,7 @@ class CustomEmojiCache( // fall thru } catch (ex: Throwable) { if (DEBUG) log.trace(ex) - log.e(ex, "PNG decode failed. %s ", url) + log.e(ex, "PNG decode failed. $url ") } // 通常のビットマップでのロードを試みる @@ -483,12 +476,12 @@ class CustomEmojiCache( if (DEBUG) log.d("bitmap decoded.") return ApngFrames(b) } else { - log.e("Bitmap decode returns null. %s", url) + log.e("Bitmap decode returns null. $url") } // fall thru } catch (ex: Throwable) { - log.e(ex, "Bitmap decode failed. %s", url) + log.e(ex, "Bitmap decode failed. $url") } // SVGのロードを試みる @@ -501,7 +494,7 @@ class CustomEmojiCache( // fall thru } catch (ex: Throwable) { - log.e(ex, "SVG decode failed. %s", url) + log.e(ex, "SVG decode failed. $url") } return null @@ -510,9 +503,9 @@ class CustomEmojiCache( private val options = BitmapFactory.Options() private fun decodeBitmap( - data: ByteArray, - @Suppress("SameParameterValue") pixel_max: Int - ): Bitmap? { + data: ByteArray, + @Suppress("SameParameterValue") pixel_max: Int + ): Bitmap? { options.inJustDecodeBounds = true options.inScaled = false options.outWidth = 0 @@ -536,10 +529,10 @@ class CustomEmojiCache( } private fun decodeSVG( - url: String, - data: ByteArray, - @Suppress("SameParameterValue") pixelMax: Float - ): Bitmap? { + url: String, + data: ByteArray, + @Suppress("SameParameterValue") pixelMax: Float + ): Bitmap? { try { val svg = SVG.getFromInputStream(ByteArrayInputStream(data)) @@ -572,13 +565,13 @@ class CustomEmojiCache( val canvas = Canvas(b) svg.renderToCanvas( - canvas, - if (aspect >= 1f) { - RectF(0f, h_ceil - dst_h, dst_w, dst_h) // 後半はw,hを指定する - } else { - RectF(w_ceil - dst_w, 0f, dst_w, dst_h) // 後半はw,hを指定する - } - ) + canvas, + if (aspect >= 1f) { + RectF(0f, h_ceil - dst_h, dst_w, dst_h) // 後半はw,hを指定する + } else { + RectF(w_ceil - dst_w, 0f, dst_w, dst_h) // 後半はw,hを指定する + } + ) return b } catch (ex: Throwable) { log.e(ex, "decodeSVG failed. $url") diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/CustomEmojiLister.kt b/app/src/main/java/jp/juggler/subwaytooter/util/CustomEmojiLister.kt index 171d1b53..6e7fa739 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/util/CustomEmojiLister.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/util/CustomEmojiLister.kt @@ -13,302 +13,302 @@ import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentLinkedQueue class CustomEmojiLister( - val context : Context, - private val handler : Handler + val context: Context, + private val handler: Handler ) { - - companion object { - - private val log = LogCategory("CustomEmojiLister") - - internal const val CACHE_MAX = 50 - - internal const val ERROR_EXPIRE = 60000L * 5 - - private val elapsedTime : Long - get() = SystemClock.elapsedRealtime() - } - - internal class CacheItem( - val key : String, - var list : ArrayList? = null, - var listWithAliases : ArrayList? = null, - // ロードした時刻 - var time_update : Long = elapsedTime, - // 参照された時刻 - var time_used : Long = time_update - ) - - internal class Request( - val accessInfo : SavedAccount, - val reportWithAliases : Boolean = false, - val onListLoaded : (list : ArrayList) -> Unit? - ) - - // 成功キャッシュ - internal val cache = ConcurrentHashMap() - - // エラーキャッシュ - internal val cache_error = ConcurrentHashMap() - - private val cache_error_item = CacheItem("error") - - // ロード要求 - internal val queue = ConcurrentLinkedQueue() - - private val worker : Worker - - init { - this.worker = Worker() - } - - // ネットワーク接続が変化したらエラーキャッシュをクリア - fun onNetworkChanged() { - cache_error.clear() - } - - private fun getCached(now : Long, accessInfo : SavedAccount) : CacheItem? { - val host = accessInfo.apiHost.ascii - - // 成功キャッシュ - val item = cache[host] - if(item != null && now - item.time_update <= ERROR_EXPIRE) { - item.time_used = now - return item - } - - // エラーキャッシュ - val time_error = cache_error[host] - if(time_error != null && now < time_error + ERROR_EXPIRE) { - return cache_error_item - } - - return null - } - - fun getList( - accessInfo : SavedAccount, - onListLoaded : (list : ArrayList) -> Unit - ) : ArrayList? { - try { - synchronized(cache) { - val item = getCached(elapsedTime, accessInfo) - if(item != null) return item.list - } - - queue.add(Request(accessInfo, onListLoaded = onListLoaded)) - worker.notifyEx() - } catch(ex : Throwable) { - log.trace(ex) - } - return null - } - - fun getListWithAliases( - accessInfo : SavedAccount, - onListLoaded : (list : ArrayList) -> Unit - ) : ArrayList? { - try { - synchronized(cache) { - val item = getCached(elapsedTime, accessInfo) - if(item != null) return item.listWithAliases - } - - queue.add( - Request( - accessInfo, - reportWithAliases = true, - onListLoaded = onListLoaded - ) - ) - worker.notifyEx() - } catch(ex : Throwable) { - log.trace(ex) - } - return null - } - - fun getMap(accessInfo : SavedAccount) : HashMap? { - val list = getList(accessInfo) { - // 遅延ロード非対応 - } ?: return null - // - val dst = HashMap() - for(e in list) { - dst[e.shortcode] = e - } - return dst - } - - private inner class Worker : WorkerBase() { - - override fun cancel() { - // このスレッドはキャンセルされない。プロセスが生きている限り動き続ける。 - } - - override suspend fun run() { - while(true) { - try { - // リクエストを取得する - val request = queue.poll() - if(request == null) { - // なければ待機 - waitEx(86400000L) - continue - } - - val cached = synchronized(cache) { - - val item = getCached(elapsedTime, request.accessInfo) - return@synchronized if(item != null) { - val list = item.list - val listWithAliases = item.listWithAliases - if(list != null && listWithAliases != null) { - fireCallback(request, list, listWithAliases) - } - true - } else { - // キャッシュにはなかった - sweep_cache() - false - } - } - if(cached) continue - - val accessInfo = request.accessInfo - val cacheKey = accessInfo.apiHost.ascii - var list : ArrayList? = null - var listWithAlias : ArrayList? = null - try { - val data = if(accessInfo.isMisskey) { - App1.getHttpCachedString( - "https://${cacheKey}/api/meta", - accessInfo = accessInfo - ) { builder -> - builder.post(JsonObject().toRequestBody()) - } - } else { - App1.getHttpCachedString( - "https://${cacheKey}/api/v1/custom_emojis", - accessInfo = accessInfo - ) - } - - if(data != null) { - val a = decodeEmojiList(data, accessInfo) - list = a - listWithAlias = makeListWithAlias(a) - } - - } catch(ex : Throwable) { - log.trace(ex) - } - - synchronized(cache) { - val now = elapsedTime - if(list == null || listWithAlias == null) { - cache_error.put(cacheKey, now) - } else { - var item : CacheItem? = cache[cacheKey] - if(item == null) { - item = CacheItem(cacheKey, list, listWithAlias) - cache[cacheKey] = item - } else { - item.list = list - item.listWithAliases = listWithAlias - item.time_update = now - } - fireCallback(request, list, listWithAlias) - } - } - } catch(ex : Throwable) { - log.trace(ex) - waitEx(3000L) - } - } - } - - private fun fireCallback( - request : Request, - list : ArrayList, - listWithAliases : ArrayList - ) { - handler.post { - request.onListLoaded( - if(request.reportWithAliases) { - listWithAliases - } else { - list - } - ) - } - } - - // キャッシュの掃除 - private fun sweep_cache() { - // 超過してる数 - val over = cache.size - CACHE_MAX - if(over <= 0) return - - // 古い要素を一時リストに集める - val now = elapsedTime - val list = ArrayList(over) - for(item in cache.values) { - if(now - item.time_used > 1000L) list.add(item) - } - - // 昇順ソート - list.sortBy { it.time_used } - - // 古い物から順に捨てる - var removed = 0 - for(item in list) { - cache.remove(item.key) - if(++ removed >= over) break - } - } - - private fun decodeEmojiList( - data : String, - accessInfo : SavedAccount - ) : ArrayList? { - return try { - val list = if(accessInfo.isMisskey) { - parseList( - CustomEmoji.decodeMisskey, - accessInfo.apDomain, - data.decodeJsonObject().jsonArray("emojis") - ) - } else { - parseList( - CustomEmoji.decode, - accessInfo.apDomain, - data.decodeJsonArray() - ) - } - list.sortWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.shortcode }) - list - } catch(ex : Throwable) { - log.e(ex, "decodeEmojiList failed. instance=%s", accessInfo.apiHost.ascii) - null - } - } - - private fun makeListWithAlias(list : ArrayList?) : ArrayList { - val dst = ArrayList() - if(list != null) { - dst.addAll(list) - for(item in list) { - val aliases = item.aliases ?: continue - for(alias in aliases) { - if(alias.equals(item.shortcode, ignoreCase = true)) continue - dst.add(item.makeAlias(alias)) - } - } - dst.sortWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.alias ?: it.shortcode }) - } - return dst - } - } - + + companion object { + + private val log = LogCategory("CustomEmojiLister") + + internal const val CACHE_MAX = 50 + + internal const val ERROR_EXPIRE = 60000L * 5 + + private val elapsedTime: Long + get() = SystemClock.elapsedRealtime() + } + + internal class CacheItem( + val key: String, + var list: ArrayList? = null, + var listWithAliases: ArrayList? = null, + // ロードした時刻 + var time_update: Long = elapsedTime, + // 参照された時刻 + var time_used: Long = time_update + ) + + internal class Request( + val accessInfo: SavedAccount, + val reportWithAliases: Boolean = false, + val onListLoaded: (list: ArrayList) -> Unit? + ) + + // 成功キャッシュ + internal val cache = ConcurrentHashMap() + + // エラーキャッシュ + internal val cache_error = ConcurrentHashMap() + + private val cache_error_item = CacheItem("error") + + // ロード要求 + internal val queue = ConcurrentLinkedQueue() + + private val worker: Worker + + init { + this.worker = Worker() + } + + // ネットワーク接続が変化したらエラーキャッシュをクリア + fun onNetworkChanged() { + cache_error.clear() + } + + private fun getCached(now: Long, accessInfo: SavedAccount): CacheItem? { + val host = accessInfo.apiHost.ascii + + // 成功キャッシュ + val item = cache[host] + if (item != null && now - item.time_update <= ERROR_EXPIRE) { + item.time_used = now + return item + } + + // エラーキャッシュ + val time_error = cache_error[host] + if (time_error != null && now < time_error + ERROR_EXPIRE) { + return cache_error_item + } + + return null + } + + fun getList( + accessInfo: SavedAccount, + onListLoaded: (list: ArrayList) -> Unit + ): ArrayList? { + try { + synchronized(cache) { + val item = getCached(elapsedTime, accessInfo) + if (item != null) return item.list + } + + queue.add(Request(accessInfo, onListLoaded = onListLoaded)) + worker.notifyEx() + } catch (ex: Throwable) { + log.trace(ex) + } + return null + } + + fun getListWithAliases( + accessInfo: SavedAccount, + onListLoaded: (list: ArrayList) -> Unit + ): ArrayList? { + try { + synchronized(cache) { + val item = getCached(elapsedTime, accessInfo) + if (item != null) return item.listWithAliases + } + + queue.add( + Request( + accessInfo, + reportWithAliases = true, + onListLoaded = onListLoaded + ) + ) + worker.notifyEx() + } catch (ex: Throwable) { + log.trace(ex) + } + return null + } + + fun getMap(accessInfo: SavedAccount): HashMap? { + val list = getList(accessInfo) { + // 遅延ロード非対応 + } ?: return null + // + val dst = HashMap() + for (e in list) { + dst[e.shortcode] = e + } + return dst + } + + private inner class Worker : WorkerBase() { + + override fun cancel() { + // このスレッドはキャンセルされない。プロセスが生きている限り動き続ける。 + } + + override suspend fun run() { + while (true) { + try { + // リクエストを取得する + val request = queue.poll() + if (request == null) { + // なければ待機 + waitEx(86400000L) + continue + } + + val cached = synchronized(cache) { + + val item = getCached(elapsedTime, request.accessInfo) + return@synchronized if (item != null) { + val list = item.list + val listWithAliases = item.listWithAliases + if (list != null && listWithAliases != null) { + fireCallback(request, list, listWithAliases) + } + true + } else { + // キャッシュにはなかった + sweep_cache() + false + } + } + if (cached) continue + + val accessInfo = request.accessInfo + val cacheKey = accessInfo.apiHost.ascii + var list: ArrayList? = null + var listWithAlias: ArrayList? = null + try { + val data = if (accessInfo.isMisskey) { + App1.getHttpCachedString( + "https://${cacheKey}/api/meta", + accessInfo = accessInfo + ) { builder -> + builder.post(JsonObject().toRequestBody()) + } + } else { + App1.getHttpCachedString( + "https://${cacheKey}/api/v1/custom_emojis", + accessInfo = accessInfo + ) + } + + if (data != null) { + val a = decodeEmojiList(data, accessInfo) + list = a + listWithAlias = makeListWithAlias(a) + } + + } catch (ex: Throwable) { + log.trace(ex) + } + + synchronized(cache) { + val now = elapsedTime + if (list == null || listWithAlias == null) { + cache_error.put(cacheKey, now) + } else { + var item: CacheItem? = cache[cacheKey] + if (item == null) { + item = CacheItem(cacheKey, list, listWithAlias) + cache[cacheKey] = item + } else { + item.list = list + item.listWithAliases = listWithAlias + item.time_update = now + } + fireCallback(request, list, listWithAlias) + } + } + } catch (ex: Throwable) { + log.trace(ex) + waitEx(3000L) + } + } + } + + private fun fireCallback( + request: Request, + list: ArrayList, + listWithAliases: ArrayList + ) { + handler.post { + request.onListLoaded( + if (request.reportWithAliases) { + listWithAliases + } else { + list + } + ) + } + } + + // キャッシュの掃除 + private fun sweep_cache() { + // 超過してる数 + val over = cache.size - CACHE_MAX + if (over <= 0) return + + // 古い要素を一時リストに集める + val now = elapsedTime + val list = ArrayList(over) + for (item in cache.values) { + if (now - item.time_used > 1000L) list.add(item) + } + + // 昇順ソート + list.sortBy { it.time_used } + + // 古い物から順に捨てる + var removed = 0 + for (item in list) { + cache.remove(item.key) + if (++removed >= over) break + } + } + + private fun decodeEmojiList( + data: String, + accessInfo: SavedAccount + ): ArrayList? { + return try { + val list = if (accessInfo.isMisskey) { + parseList( + CustomEmoji.decodeMisskey, + accessInfo.apDomain, + data.decodeJsonObject().jsonArray("emojis") + ) + } else { + parseList( + CustomEmoji.decode, + accessInfo.apDomain, + data.decodeJsonArray() + ) + } + list.sortWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.shortcode }) + list + } catch (ex: Throwable) { + log.e(ex, "decodeEmojiList failed. instance=${accessInfo.apiHost.ascii}") + null + } + } + + private fun makeListWithAlias(list: ArrayList?): ArrayList { + val dst = ArrayList() + if (list != null) { + dst.addAll(list) + for (item in list) { + val aliases = item.aliases ?: continue + for (alias in aliases) { + if (alias.equals(item.shortcode, ignoreCase = true)) continue + dst.add(item.makeAlias(alias)) + } + } + dst.sortWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.alias ?: it.shortcode }) + } + return dst + } + } + } diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/MisskeyMarkdownDecoder.kt b/app/src/main/java/jp/juggler/subwaytooter/util/MisskeyMarkdownDecoder.kt index eb22fd2d..c023b0ac 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/util/MisskeyMarkdownDecoder.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/util/MisskeyMarkdownDecoder.kt @@ -27,94 +27,94 @@ import java.util.regex.Pattern import kotlin.text.codePointBefore private val brackets = arrayOf( - "()", - "()", - "[]", - "{}", - "“”", - "‘’", - "‹›", - "«»", - "()", - "[]", - "{}", - "⦅⦆", - "⦅⦆", - "〚〛", - "⦃⦄", - "「」", - "〈〉", - "《》", - "【】", - "〔〕", - "⦗⦘", - "『』", - "〖〗", - "〘〙", - "[]", - "「」", - "⟦⟧", - "⟨⟩", - "⟪⟫", - "⟮⟯", - "⟬⟭", - "⌈⌉", - "⌊⌋", - "⦇⦈", - "⦉⦊", - "❛❜", - "❝❞", - "❨❩", - "❪❫", - "❴❵", - "❬❭", - "❮❯", - "❰❱", - "❲❳", - "()", - "﴾﴿", - "〈〉", - "⦑⦒", - "⧼⧽", - "﹙﹚", - "﹛﹜", - "﹝﹞", - "⁽⁾", - "₍₎", - "⦋⦌", - "⦍⦎", - "⦏⦐", - "⁅⁆", - "⸢⸣", - "⸤⸥", - "⟅⟆", - "⦓⦔", - "⦕⦖", - "⸦⸧", - "⸨⸩", - "⧘⧙", - "⧚⧛", - "⸜⸝", - "⸌⸍", - "⸂⸃", - "⸄⸅", - "⸉⸊", - "᚛᚜", - "༺༻", - "༼༽", - "⏜⏝", - "⎴⎵", - "⏞⏟", - "⏠⏡", - "﹁﹂", - "﹃﹄", - "︹︺", - "︻︼", - "︗︘", - "︿﹀", - "︽︾", - "﹇﹈", - "︷︸" + "()", + "()", + "[]", + "{}", + "“”", + "‘’", + "‹›", + "«»", + "()", + "[]", + "{}", + "⦅⦆", + "⦅⦆", + "〚〛", + "⦃⦄", + "「」", + "〈〉", + "《》", + "【】", + "〔〕", + "⦗⦘", + "『』", + "〖〗", + "〘〙", + "[]", + "「」", + "⟦⟧", + "⟨⟩", + "⟪⟫", + "⟮⟯", + "⟬⟭", + "⌈⌉", + "⌊⌋", + "⦇⦈", + "⦉⦊", + "❛❜", + "❝❞", + "❨❩", + "❪❫", + "❴❵", + "❬❭", + "❮❯", + "❰❱", + "❲❳", + "()", + "﴾﴿", + "〈〉", + "⦑⦒", + "⧼⧽", + "﹙﹚", + "﹛﹜", + "﹝﹞", + "⁽⁾", + "₍₎", + "⦋⦌", + "⦍⦎", + "⦏⦐", + "⁅⁆", + "⸢⸣", + "⸤⸥", + "⟅⟆", + "⦓⦔", + "⦕⦖", + "⸦⸧", + "⸨⸩", + "⧘⧙", + "⧚⧛", + "⸜⸝", + "⸌⸍", + "⸂⸃", + "⸄⸅", + "⸉⸊", + "᚛᚜", + "༺༻", + "༼༽", + "⏜⏝", + "⎴⎵", + "⏞⏟", + "⏠⏡", + "﹁﹂", + "﹃﹄", + "︹︺", + "︻︼", + "︗︘", + "︿﹀", + "︽︾", + "﹇﹈", + "︷︸" ) private val bracketsMap = HashMap().apply { @@ -136,10 +136,10 @@ private val bracketsMapUrlSafe = HashMap().apply { fun String.removeOrphanedBrackets(urlSafe: Boolean = false): String { var last = 0 val nests = when (urlSafe) { - true -> this.map { - last += bracketsMapUrlSafe[it] ?: 0 - last - } + true -> this.map { + last += bracketsMapUrlSafe[it] ?: 0 + last + } else -> this.map { last += bracketsMap[it] ?: 0 @@ -178,31 +178,35 @@ class SpanList { src.list.forEach { addLast(it.start + offset, it.end + offset, it.span) } } - fun addFirst(start: Int, end: Int, span: Any) = when { - start == end -> { - // empty span allowed - } + fun addFirst(start: Int, end: Int, span: Any) { + when { + start == end -> { + // empty span allowed + } - start > end -> { - MisskeyMarkdownDecoder.log.e("SpanList.add: range error! start=$start,end=$end,span=$span") - } + start > end -> { + MisskeyMarkdownDecoder.log.e("SpanList.add: range error! start=$start,end=$end,span=$span") + } - else -> { - list.addFirst(SpanPos(start, end, span)) + else -> { + list.addFirst(SpanPos(start, end, span)) + } } } - fun addLast(start: Int, end: Int, span: Any) = when { - start == end -> { - // empty span allowed - } + fun addLast(start: Int, end: Int, span: Any) { + when { + start == end -> { + // empty span allowed + } - start > end -> { - MisskeyMarkdownDecoder.log.e("SpanList.add: range error! start=$start,end=$end,span=$span") - } + start > end -> { + MisskeyMarkdownDecoder.log.e("SpanList.add: range error! start=$start,end=$end,span=$span") + } - else -> { - list.addLast(SpanPos(start, end, span)) + else -> { + list.addLast(SpanPos(start, end, span)) + } } } @@ -235,10 +239,10 @@ class SpanList { internal object MatcherCache { private class MatcherCacheItem( - var matcher: Matcher, - var text: String, - var textHashCode: Int - ) + var matcher: Matcher, + var text: String, + var textHashCode: Int + ) // スレッドごとにキャッシュ用のマップを持つ private val matcherCache = @@ -247,11 +251,11 @@ internal object MatcherCache { } internal fun matcher( - pattern: Pattern, - text: String, - start: Int = 0, - end: Int = text.length - ): Matcher { + pattern: Pattern, + text: String, + start: Int = 0, + end: Int = text.length + ): Matcher { val m: Matcher val textHashCode = text.hashCode() val map = matcherCache.get()!! @@ -282,86 +286,86 @@ object MisskeySyntaxHighlighter { private val keywords = HashSet().apply { val _keywords = arrayOf( - "true", - "false", - "null", - "nil", - "undefined", - "void", - "var", - "const", - "let", - "mut", - "dim", - "if", - "then", - "else", - "switch", - "match", - "case", - "default", - "for", - "each", - "in", - "while", - "loop", - "continue", - "break", - "do", - "goto", - "next", - "end", - "sub", - "throw", - "try", - "catch", - "finally", - "enum", - "delegate", - "function", - "func", - "fun", - "fn", - "return", - "yield", - "async", - "await", - "require", - "include", - "import", - "imports", - "export", - "exports", - "from", - "as", - "using", - "use", - "internal", - "module", - "namespace", - "where", - "select", - "struct", - "union", - "new", - "delete", - "this", - "super", - "base", - "class", - "interface", - "abstract", - "static", - "public", - "private", - "protected", - "virtual", - "partial", - "override", - "extends", - "implements", - "constructor" - ) + "true", + "false", + "null", + "nil", + "undefined", + "void", + "var", + "const", + "let", + "mut", + "dim", + "if", + "then", + "else", + "switch", + "match", + "case", + "default", + "for", + "each", + "in", + "while", + "loop", + "continue", + "break", + "do", + "goto", + "next", + "end", + "sub", + "throw", + "try", + "catch", + "finally", + "enum", + "delegate", + "function", + "func", + "fun", + "fn", + "return", + "yield", + "async", + "await", + "require", + "include", + "import", + "imports", + "export", + "exports", + "from", + "as", + "using", + "use", + "internal", + "module", + "namespace", + "where", + "select", + "struct", + "union", + "new", + "delete", + "this", + "super", + "base", + "class", + "interface", + "abstract", + "static", + "public", + "private", + "protected", + "virtual", + "partial", + "override", + "extends", + "implements", + "constructor" + ) // lower addAll(_keywords) @@ -387,17 +391,17 @@ object MisskeySyntaxHighlighter { } private class Token( - val length: Int, - val color: Int = 0, - val italic: Boolean = false, - val comment: Boolean = false - ) + val length: Int, + val color: Int = 0, + val italic: Boolean = false, + val comment: Boolean = false + ) private class Env( - val source: String, - val start: Int, - val end: Int - ) { + val source: String, + val start: Int, + val end: Int + ) { // 出力先2 val spanList = SpanList() @@ -417,10 +421,10 @@ object MisskeySyntaxHighlighter { } if (token.italic) { spanList.addLast( - start, - end, - fontSpan(Typeface.defaultFromStyle(Typeface.ITALIC)) - ) + start, + end, + fontSpan(Typeface.defaultFromStyle(Typeface.ITALIC)) + ) } } } @@ -491,176 +495,176 @@ object MisskeySyntaxHighlighter { private val elements = arrayOf Token?>( - // マルチバイト文字をまとめて読み飛ばす - { - var s = pos - while (s < end && source[s] >= charH80) { - ++s - } - when { - s > pos -> Token(length = s - pos) - else -> null - } - }, + // マルチバイト文字をまとめて読み飛ばす + { + var s = pos + while (s < end && source[s] >= charH80) { + ++s + } + when { + s > pos -> Token(length = s - pos) + else -> null + } + }, - // 空白と改行をまとめて読み飛ばす - { - var s = pos - while (s < end && source[s] <= ' ') { - ++s - } - when { - s > pos -> Token(length = s - pos) - else -> null - } - }, + // 空白と改行をまとめて読み飛ばす + { + var s = pos + while (s < end && source[s] <= ' ') { + ++s + } + when { + s > pos -> Token(length = s - pos) + else -> null + } + }, - // comment - { - val match = remainMatcher(reLineComment) - when { - !match.find() -> null - else -> Token(length = match.end() - match.start(), comment = true) - } - }, + // comment + { + val match = remainMatcher(reLineComment) + when { + !match.find() -> null + else -> Token(length = match.end() - match.start(), comment = true) + } + }, - // block comment - { - val match = remainMatcher(reBlockComment) - when { - !match.find() -> null - else -> Token(length = match.end() - match.start(), comment = true) - } - }, + // block comment + { + val match = remainMatcher(reBlockComment) + when { + !match.find() -> null + else -> Token(length = match.end() - match.start(), comment = true) + } + }, - // string - { - val beginChar = source[pos] - if (!stringStart[beginChar.code]) return@arrayOf null - var i = pos + 1 - while (i < end) { - val char = source[i++] - if (char == beginChar) { - break // end - } else if (char == '\n' || i >= end) { - i = 0 // not string literal - break - } else if (char == '\\' && i < end) { - ++i // \" では閉じないようにする - } - } - when { - i <= pos -> null - else -> Token(length = i - pos, color = 0xe96900) - } - }, + // string + { + val beginChar = source[pos] + if (!stringStart[beginChar.code]) return@arrayOf null + var i = pos + 1 + while (i < end) { + val char = source[i++] + if (char == beginChar) { + break // end + } else if (char == '\n' || i >= end) { + i = 0 // not string literal + break + } else if (char == '\\' && i < end) { + ++i // \" では閉じないようにする + } + } + when { + i <= pos -> null + else -> Token(length = i - pos, color = 0xe96900) + } + }, - // regexp - { - if (source[pos] != '/') return@arrayOf null - val regexp = StringBuilder() - var i = pos + 1 - while (i < end) { - val char = source[i++] - if (char == '/') { - break - } else if (char == '\n' || i >= end) { - i = 0 // not closed - break - } else { - regexp.append(char) - if (char == '\\' && i < end) { - regexp.append(source[i++]) - } - } - } - when { - i == 0 -> null - regexp.isEmpty() -> null - regexp.first() == ' ' && regexp.last() == ' ' -> null - else -> Token(length = regexp.length + 2, color = 0xe9003f) - } - }, + // regexp + { + if (source[pos] != '/') return@arrayOf null + val regexp = StringBuilder() + var i = pos + 1 + while (i < end) { + val char = source[i++] + if (char == '/') { + break + } else if (char == '\n' || i >= end) { + i = 0 // not closed + break + } else { + regexp.append(char) + if (char == '\\' && i < end) { + regexp.append(source[i++]) + } + } + } + when { + i == 0 -> null + regexp.isEmpty() -> null + regexp.first() == ' ' && regexp.last() == ' ' -> null + else -> Token(length = regexp.length + 2, color = 0xe9003f) + } + }, - // label - { - // 直前に識別子があればNG - val prev = if (pos <= 0) null else source[pos - 1] - if (prev?.isLetterOrDigit() == true) return@arrayOf null + // label + { + // 直前に識別子があればNG + val prev = if (pos <= 0) null else source[pos - 1] + if (prev?.isLetterOrDigit() == true) return@arrayOf null - val match = remainMatcher(reLabel) - if (!match.find()) return@arrayOf null + val match = remainMatcher(reLabel) + if (!match.find()) return@arrayOf null - val matchEnd = match.end() - when { - // @user@host のように直後に@が続くのはNG - matchEnd < end && source[matchEnd] == '@' -> null - else -> Token(length = match.end() - pos, color = 0xe9003f) - } - }, + val matchEnd = match.end() + when { + // @user@host のように直後に@が続くのはNG + matchEnd < end && source[matchEnd] == '@' -> null + else -> Token(length = match.end() - pos, color = 0xe9003f) + } + }, - // number - { - val prev = if (pos <= 0) null else source[pos - 1] - if (prev?.isLetterOrDigit() == true) return@arrayOf null - val match = remainMatcher(reNumber) - when { - !match.find() -> null - else -> Token(length = match.end() - pos, color = 0xae81ff) - } - }, + // number + { + val prev = if (pos <= 0) null else source[pos - 1] + if (prev?.isLetterOrDigit() == true) return@arrayOf null + val match = remainMatcher(reNumber) + when { + !match.find() -> null + else -> Token(length = match.end() - pos, color = 0xae81ff) + } + }, - // method, property, keyword - { - // 直前の文字が識別子に使えるなら識別子の開始とはみなさない - val prev = if (pos <= 0) null else source[pos - 1] - if (prev?.isLetterOrDigit() == true || prev == '_') return@arrayOf null + // method, property, keyword + { + // 直前の文字が識別子に使えるなら識別子の開始とはみなさない + val prev = if (pos <= 0) null else source[pos - 1] + if (prev?.isLetterOrDigit() == true || prev == '_') return@arrayOf null - val match = remainMatcher(reKeyword) - if (!match.find()) return@arrayOf null - val kw = match.groupEx(1)!! - val bracket = match.groupEx(2) // may null + val match = remainMatcher(reKeyword) + if (!match.find()) return@arrayOf null + val kw = match.groupEx(1)!! + val bracket = match.groupEx(2) // may null - when { - // 英数字や_を含まないキーワードは無視する - // -moz-foo- や __ はキーワードだが、 - や -- はキーワードではない - !reContainsAlpha.matcher(kw).find() -> null + when { + // 英数字や_を含まないキーワードは無視する + // -moz-foo- や __ はキーワードだが、 - や -- はキーワードではない + !reContainsAlpha.matcher(kw).find() -> null - // メソッド呼び出しは対象が変数かプロパティかに関わらずメソッドの色になる - bracket?.isNotEmpty() == true -> - Token(length = kw.length, color = 0x8964c1, italic = true) + // メソッド呼び出しは対象が変数かプロパティかに関わらずメソッドの色になる + bracket?.isNotEmpty() == true -> + Token(length = kw.length, color = 0x8964c1, italic = true) - // 変数や定数ではなくプロパティならプロパティの色になる - prev == '.' -> Token(length = kw.length, color = 0xa71d5d) + // 変数や定数ではなくプロパティならプロパティの色になる + prev == '.' -> Token(length = kw.length, color = 0xa71d5d) - // 予約語ではない - // 強調表示しないが、識別子単位で読み飛ばす - !keywords.contains(kw) -> Token(length = kw.length) + // 予約語ではない + // 強調表示しないが、識別子単位で読み飛ばす + !keywords.contains(kw) -> Token(length = kw.length) - else -> when (kw) { + else -> when (kw) { - // 定数 - "true", "false", "null", "nil", "undefined", "NaN" -> - Token(length = kw.length, color = 0xae81ff) + // 定数 + "true", "false", "null", "nil", "undefined", "NaN" -> + Token(length = kw.length, color = 0xae81ff) - // その他の予約語 - else -> Token(length = kw.length, color = 0x2973b7) - } - } - }, + // その他の予約語 + else -> Token(length = kw.length, color = 0x2973b7) + } + } + }, - // symbol - { - val c = source[pos] - when { - symbolMap.get(c.code, false) -> - Token(length = 1, color = 0x42b983) - c == '-' -> - Token(length = 1, color = 0x42b983) - else -> null - } - } - ) + // symbol + { + val c = source[pos] + when { + symbolMap.get(c.code, false) -> + Token(length = 1, color = 0x42b983) + c == '-' -> + Token(length = 1, color = 0x42b983) + else -> null + } + } + ) fun parse(source: String) = Env(source, 0, source.length).parse() } @@ -673,8 +677,8 @@ object MisskeyMarkdownDecoder { // デコード結果にはメンションの配列を含む。TootStatusのパーサがこれを回収する。 class SpannableStringBuilderEx( - var mentions: ArrayList? = null - ) : SpannableStringBuilder() + var mentions: ArrayList? = null + ) : SpannableStringBuilder() // ブロック要素は始端と終端の空行を除去したい private val reStartEmptyLines = """\A(?:[  ]*?[\x0d\x0a]+)+""".toRegex() @@ -685,9 +689,9 @@ object MisskeyMarkdownDecoder { // 装飾つきテキストの出力時に使うデータの集まり internal class SpanOutputEnv( - val options: DecodeOptions, - val sb: SpannableStringBuilderEx - ) { + val options: DecodeOptions, + val sb: SpannableStringBuilderEx + ) { val context: Context = options.context ?: error("missing context") val font_bold = ActMain.timeline_font_bold @@ -738,10 +742,10 @@ object MisskeyMarkdownDecoder { for (range in list) { val word = HighlightWord.load(range.word) ?: continue spanList.addLast( - range.start, - range.end, - HighlightSpan(word.color_fg, word.color_bg) - ) + range.start, + range.end, + HighlightSpan(word.color_fg, word.color_bg) + ) if (word.sound_type != HighlightWord.SOUND_TYPE_NONE) { if (options.highlightSound == null) options.highlightSound = word @@ -776,10 +780,10 @@ object MisskeyMarkdownDecoder { val start = sb.length sb.append(href) spanList.addFirst( - start, - sb.length, - SvgEmojiSpan(context, "emj_1f5bc.svg",scale=1f), - ) + start, + sb.length, + SvgEmojiSpan(context, "emj_1f5bc.svg", scale = 1f), + ) } else -> appendText(shortenUrl(display_url)) @@ -788,11 +792,11 @@ object MisskeyMarkdownDecoder { // リンクを追加する fun appendLink( - text: String, - url: String, - allowShort: Boolean = false, - mention: TootMention? = null - ) { + text: String, + url: String, + allowShort: Boolean = false, + mention: TootMention? = null + ) { when { allowShort -> appendLinkText(text, url) else -> appendText(text) @@ -804,20 +808,20 @@ object MisskeyMarkdownDecoder { } else { // 通称と色を調べる getFullAcctOrNull( - rawAcct = Acct.parse(text.substring(1)), - url = url, - options.linkHelper, - options.mentionDefaultHostDomain, - ) + rawAcct = Acct.parse(text.substring(1)), + url = url, + options.linkHelper, + options.mentionDefaultHostDomain, + ) } val linkInfo = LinkInfo( - caption = text, - url = url, - ac = fullAcct?.let { AcctColor.load(fullAcct) }, - tag = options.linkTag, - mention = mention - ) + caption = text, + url = url, + ac = fullAcct?.let { AcctColor.load(fullAcct) }, + tag = options.linkTag, + mention = mention + ) // リンクの一部にハイライトがある場合、リンクをセットしてからハイライトをセットしないとクリック判定がおかしくなる。 spanList.addFirst(start, sb.length, MyClickableSpan(linkInfo)) } @@ -832,9 +836,9 @@ object MisskeyMarkdownDecoder { } fun appendMention( - username: String, - strHost: String? - ) { + username: String, + strHost: String? + ) { // ユーザが記述したacct val rawAcct = Acct.parse(username, strHost) @@ -868,11 +872,11 @@ object MisskeyMarkdownDecoder { // https://github.com/syuilo/misskey/pull/3603 - "github.com", "twitter.com" -> - "https://$strHost/$username" // no @ + "github.com", "twitter.com" -> + "https://$strHost/$username" // no @ - "gmail.com" -> - "mailto:$username@$strHost" + "gmail.com" -> + "mailto:$username@$strHost" else -> // MFMはメンションからユーザのURIを調べる正式な方法がない @@ -884,8 +888,8 @@ object MisskeyMarkdownDecoder { mention = mentions.find { m -> m.acct == shortAcct } if (mention == null) { val newMention = TootMention( - EntityId.DEFAULT, url, shortAcct.ascii, username - ) + EntityId.DEFAULT, url, shortAcct.ascii, username + ) mentions.add(newMention) mention = newMention } @@ -898,286 +902,286 @@ object MisskeyMarkdownDecoder { //////////////////////////////////////////////////////////////////////////// private fun mixColor( - @Suppress("SameParameterValue") col1: Int, - col2: Int - ): Int = Color.rgb( - (Color.red(col1) + Color.red(col2)) ushr 1, - (Color.green(col1) + Color.green(col2)) ushr 1, - (Color.blue(col1) + Color.blue(col2)) ushr 1 - ) + @Suppress("SameParameterValue") col1: Int, + col2: Int + ): Int = Color.rgb( + (Color.red(col1) + Color.red(col2)) ushr 1, + (Color.green(col1) + Color.green(col2)) ushr 1, + (Color.blue(col1) + Color.blue(col2)) ushr 1 + ) val quoteNestColors = intArrayOf( - mixColor(Color.GRAY, 0x0000ff), - mixColor(Color.GRAY, 0x0080ff), - mixColor(Color.GRAY, 0x00ff80), - mixColor(Color.GRAY, 0x00ff00), - mixColor(Color.GRAY, 0x80ff00), - mixColor(Color.GRAY, 0xff8000), - mixColor(Color.GRAY, 0xff0000), - mixColor(Color.GRAY, 0xff0080), - mixColor(Color.GRAY, 0x8000ff) - ) + mixColor(Color.GRAY, 0x0000ff), + mixColor(Color.GRAY, 0x0080ff), + mixColor(Color.GRAY, 0x00ff80), + mixColor(Color.GRAY, 0x00ff00), + mixColor(Color.GRAY, 0x80ff00), + mixColor(Color.GRAY, 0xff8000), + mixColor(Color.GRAY, 0xff0000), + mixColor(Color.GRAY, 0xff0080), + mixColor(Color.GRAY, 0x8000ff) + ) // ノード種別とレンダリング関数 internal enum class NodeType(val render: SpanOutputEnv.(Node) -> Unit) { TEXT({ - appendText(it.args[0], decodeEmoji = true) - }), + appendText(it.args[0], decodeEmoji = true) + }), EMOJI({ - val code = it.args[0] - if (code.isNotEmpty()) { - appendText(":$code:", decodeEmoji = true) - } - }), + val code = it.args[0] + if (code.isNotEmpty()) { + appendText(":$code:", decodeEmoji = true) + } + }), MENTION({ - appendMention(it.args[0], it.args[1].notEmpty()) - }), + appendMention(it.args[0], it.args[1].notEmpty()) + }), LATEX({ - fireRenderChildNodes(it) - }), + fireRenderChildNodes(it) + }), HASHTAG({ - val linkHelper = linkHelper - val tag = it.args[0] - if (tag.isNotEmpty() && linkHelper != null) { - appendLink( - "#$tag", - "https://${linkHelper.apiHost.ascii}/tags/" + tag.encodePercent() - ) - } - }), + val linkHelper = linkHelper + val tag = it.args[0] + if (tag.isNotEmpty() && linkHelper != null) { + appendLink( + "#$tag", + "https://${linkHelper.apiHost.ascii}/tags/" + tag.encodePercent() + ) + } + }), CODE_INLINE({ - val text = it.args[0] - val sp = MisskeySyntaxHighlighter.parse(text) - appendText(text) - spanList.addWithOffset(sp, start) - spanList.addLast(start, sb.length, BackgroundColorSpan(0x40808080)) - spanList.addLast(start, sb.length, fontSpan(Typeface.MONOSPACE)) - }), + val text = it.args[0] + val sp = MisskeySyntaxHighlighter.parse(text) + appendText(text) + spanList.addWithOffset(sp, start) + spanList.addLast(start, sb.length, BackgroundColorSpan(0x40808080)) + spanList.addLast(start, sb.length, fontSpan(Typeface.MONOSPACE)) + }), URL({ - val url = it.args[0] - if (url.isNotEmpty()) { - appendLink(url, url, allowShort = true) - } - }), + val url = it.args[0] + if (url.isNotEmpty()) { + appendLink(url, url, allowShort = true) + } + }), CODE_BLOCK({ - closePreviousBlock() + closePreviousBlock() - val text = trimBlock(it.args[0]) - val sp = MisskeySyntaxHighlighter.parse(text) - appendText(text) - spanList.addWithOffset(sp, start) - spanList.addLast(start, sb.length, BackgroundColorSpan(0x40808080)) - spanList.addLast(start, sb.length, RelativeSizeSpan(0.7f)) - spanList.addLast(start, sb.length, fontSpan(Typeface.MONOSPACE)) - closeBlock() - }), + val text = trimBlock(it.args[0]) + val sp = MisskeySyntaxHighlighter.parse(text) + appendText(text) + spanList.addWithOffset(sp, start) + spanList.addLast(start, sb.length, BackgroundColorSpan(0x40808080)) + spanList.addLast(start, sb.length, RelativeSizeSpan(0.7f)) + spanList.addLast(start, sb.length, fontSpan(Typeface.MONOSPACE)) + closeBlock() + }), QUOTE_INLINE({ - val text = trimBlock(it.args[0]) - appendText(text) - spanList.addLast( - start, - sb.length, - BackgroundColorSpan(0x20808080) - ) - spanList.addLast( - start, - sb.length, - fontSpan(Typeface.defaultFromStyle(Typeface.ITALIC)) - ) - }), + val text = trimBlock(it.args[0]) + appendText(text) + spanList.addLast( + start, + sb.length, + BackgroundColorSpan(0x20808080) + ) + spanList.addLast( + start, + sb.length, + fontSpan(Typeface.defaultFromStyle(Typeface.ITALIC)) + ) + }), SEARCH({ - closePreviousBlock() + closePreviousBlock() - val text = it.args[0] - val kw_start = sb.length // キーワードの開始位置 - appendText(text) - appendText(" ") - start = sb.length // 検索リンクの開始位置 + val text = it.args[0] + val kw_start = sb.length // キーワードの開始位置 + appendText(text) + appendText(" ") + start = sb.length // 検索リンクの開始位置 - appendLink( - context.getString(R.string.search), - "https://www.google.co.jp/search?q=${text.encodePercent()}" - ) - spanList.addLast(kw_start, sb.length, RelativeSizeSpan(1.2f)) + appendLink( + context.getString(R.string.search), + "https://www.google.co.jp/search?q=${text.encodePercent()}" + ) + spanList.addLast(kw_start, sb.length, RelativeSizeSpan(1.2f)) - closeBlock() - }), + closeBlock() + }), BIG({ - val start = this.start - fireRenderChildNodes(it) - spanList.addLast(start, sb.length, MisskeyBigSpan(font_bold)) - }), + val start = this.start + fireRenderChildNodes(it) + spanList.addLast(start, sb.length, MisskeyBigSpan(font_bold)) + }), BOLD({ - val start = this.start - fireRenderChildNodes(it) - spanList.addLast(start, sb.length, fontSpan(font_bold)) - }), + val start = this.start + fireRenderChildNodes(it) + spanList.addLast(start, sb.length, fontSpan(font_bold)) + }), STRIKE({ - val start = this.start - fireRenderChildNodes(it) - spanList.addLast(start, sb.length, StrikethroughSpan()) - }), + val start = this.start + fireRenderChildNodes(it) + spanList.addLast(start, sb.length, StrikethroughSpan()) + }), SMALL({ - val start = this.start - fireRenderChildNodes(it) - spanList.addLast(start, sb.length, RelativeSizeSpan(0.7f)) - }), + val start = this.start + fireRenderChildNodes(it) + spanList.addLast(start, sb.length, RelativeSizeSpan(0.7f)) + }), FUNCTION({ - val name = it.args.elementAtOrNull(0) - appendText("[") - appendText(name ?: "") - appendText(" ") - fireRenderChildNodes(it) - appendText("]") - }), + val name = it.args.elementAtOrNull(0) + appendText("[") + appendText(name ?: "") + appendText(" ") + fireRenderChildNodes(it) + appendText("]") + }), ITALIC({ - val start = this.start - fireRenderChildNodes(it) - spanList.addLast(start, sb.length, fontSpan(Typeface.defaultFromStyle(Typeface.ITALIC))) - }), + val start = this.start + fireRenderChildNodes(it) + spanList.addLast(start, sb.length, fontSpan(Typeface.defaultFromStyle(Typeface.ITALIC))) + }), MOTION({ - val start = this.start - fireRenderChildNodes(it) - spanList.addFirst( - start, - sb.length, - MisskeyMotionSpan(ActMain.timeline_font) - ) - }), + val start = this.start + fireRenderChildNodes(it) + spanList.addFirst( + start, + sb.length, + MisskeyMotionSpan(ActMain.timeline_font) + ) + }), LINK({ - val url = it.args[1] - // val silent = data?.get(2) - // silentはプレビュー表示を抑制するが、Subwayにはもともとないので関係なかった + val url = it.args[1] + // val silent = data?.get(2) + // silentはプレビュー表示を抑制するが、Subwayにはもともとないので関係なかった - if (url.isNotEmpty()) { - val start = this.start - fireRenderChildNodes(it) - val linkHelper = options.linkHelper - if (linkHelper != null) { - val linkInfo = LinkInfo( - url = url, - tag = options.linkTag, - ac = TootAccount.getAcctFromUrl(url)?.let { acct -> AcctColor.load(acct) }, - caption = sb.substring(start, sb.length) - ) - spanList.addFirst(start, sb.length, MyClickableSpan(linkInfo)) - } - } - }), + if (url.isNotEmpty()) { + val start = this.start + fireRenderChildNodes(it) + val linkHelper = options.linkHelper + if (linkHelper != null) { + val linkInfo = LinkInfo( + url = url, + tag = options.linkTag, + ac = TootAccount.getAcctFromUrl(url)?.let { acct -> AcctColor.load(acct) }, + caption = sb.substring(start, sb.length) + ) + spanList.addFirst(start, sb.length, MyClickableSpan(linkInfo)) + } + } + }), TITLE({ - closePreviousBlock() + closePreviousBlock() - val start = this.start - fireRenderChildNodes(it) // 改行を含まないことが分かっている - spanList.addLast( - start, - sb.length, - android.text.style.AlignmentSpan.Standard(android.text.Layout.Alignment.ALIGN_CENTER) - ) - spanList.addLast( - start, - sb.length, - BackgroundColorSpan(0x20808080) - ) - spanList.addLast(start, sb.length, RelativeSizeSpan(1.5f)) + val start = this.start + fireRenderChildNodes(it) // 改行を含まないことが分かっている + spanList.addLast( + start, + sb.length, + android.text.style.AlignmentSpan.Standard(android.text.Layout.Alignment.ALIGN_CENTER) + ) + spanList.addLast( + start, + sb.length, + BackgroundColorSpan(0x20808080) + ) + spanList.addLast(start, sb.length, RelativeSizeSpan(1.5f)) - closeBlock() - }), + closeBlock() + }), CENTER({ - closePreviousBlock() + closePreviousBlock() - val start = this.start - fireRenderChildNodes(it) - when { - it.quoteNest > 0 -> { - // 引用ネストの内部ではセンタリングさせると引用マーカーまで動いてしまうので - // センタリングが機能しないようにする - } + val start = this.start + fireRenderChildNodes(it) + when { + it.quoteNest > 0 -> { + // 引用ネストの内部ではセンタリングさせると引用マーカーまで動いてしまうので + // センタリングが機能しないようにする + } - else -> spanList.addLast( - start, - sb.length, - android.text.style.AlignmentSpan.Standard( - android.text.Layout.Alignment.ALIGN_CENTER - ) - ) - } + else -> spanList.addLast( + start, + sb.length, + android.text.style.AlignmentSpan.Standard( + android.text.Layout.Alignment.ALIGN_CENTER + ) + ) + } - closeBlock() - }), + closeBlock() + }), QUOTE_BLOCK({ - closePreviousBlock() + closePreviousBlock() - val start = this.start + val start = this.start - // 末尾にある空白のテキストノードを除去する - while (it.childNodes.isNotEmpty()) { - val last = it.childNodes.last() - if (last.type == TEXT && last.args[0].isBlank()) { - it.childNodes.removeLast() - } else { - break - } - } + // 末尾にある空白のテキストノードを除去する + while (it.childNodes.isNotEmpty()) { + val last = it.childNodes.last() + if (last.type == TEXT && last.args[0].isBlank()) { + it.childNodes.removeLast() + } else { + break + } + } - fireRenderChildNodes(it) + fireRenderChildNodes(it) - val bg_color = quoteNestColors[it.quoteNest % quoteNestColors.size] - // TextView の文字装飾では「ブロック要素の入れ子」を表現できない - // 内容の各行の始端に何か追加するというのがまずキツい - // しかし各行の頭に引用マークをつけないと引用のネストで意味が通じなくなってしまう - val tmp = sb.toString() - //log.d("QUOTE_BLOCK tmp=${tmp} start=$start end=${tmp.length}") - for (i in tmp.length - 1 downTo start) { - val prevChar = when (i) { - start -> '\n' - else -> tmp[i - 1] - } - //log.d("QUOTE_BLOCK prevChar=${ String.format("%x",prevChar.toInt())}") - if (prevChar == '\n') { - //log.d("QUOTE_BLOCK insert! i=$i") - sb.insert(i, "> ") - spanList.insert(i, 2) - spanList.addLast( - i, i + 1, - BackgroundColorSpan(bg_color) - ) - } - } + val bg_color = quoteNestColors[it.quoteNest % quoteNestColors.size] + // TextView の文字装飾では「ブロック要素の入れ子」を表現できない + // 内容の各行の始端に何か追加するというのがまずキツい + // しかし各行の頭に引用マークをつけないと引用のネストで意味が通じなくなってしまう + val tmp = sb.toString() + //log.d("QUOTE_BLOCK tmp=${tmp} start=$start end=${tmp.length}") + for (i in tmp.length - 1 downTo start) { + val prevChar = when (i) { + start -> '\n' + else -> tmp[i - 1] + } + //log.d("QUOTE_BLOCK prevChar=${ String.format("%x",prevChar.toInt())}") + if (prevChar == '\n') { + //log.d("QUOTE_BLOCK insert! i=$i") + sb.insert(i, "> ") + spanList.insert(i, 2) + spanList.addLast( + i, i + 1, + BackgroundColorSpan(bg_color) + ) + } + } - spanList.addLast( - start, - sb.length, - fontSpan(Typeface.defaultFromStyle(Typeface.ITALIC)) - ) + spanList.addLast( + start, + sb.length, + fontSpan(Typeface.defaultFromStyle(Typeface.ITALIC)) + ) - closeBlock() - }), + closeBlock() + }), ROOT({ - fireRenderChildNodes(it) - }), + fireRenderChildNodes(it) + }), ; @@ -1194,84 +1198,84 @@ object MisskeyMarkdownDecoder { BIG wraps hashSetOf( - EMOJI, HASHTAG, MENTION, FUNCTION, LATEX, - STRIKE, SMALL, ITALIC - ) + EMOJI, HASHTAG, MENTION, FUNCTION, LATEX, + STRIKE, SMALL, ITALIC + ) BOLD wraps hashSetOf( - EMOJI, HASHTAG, MENTION, FUNCTION, LATEX, URL, LINK, - STRIKE, SMALL, ITALIC - ) + EMOJI, HASHTAG, MENTION, FUNCTION, LATEX, URL, LINK, + STRIKE, SMALL, ITALIC + ) STRIKE wraps hashSetOf( - EMOJI, HASHTAG, MENTION, FUNCTION, LATEX, URL, LINK, - BIG, BOLD, SMALL, ITALIC - ) + EMOJI, HASHTAG, MENTION, FUNCTION, LATEX, URL, LINK, + BIG, BOLD, SMALL, ITALIC + ) SMALL wraps hashSetOf( - EMOJI, HASHTAG, MENTION, FUNCTION, LATEX, URL, LINK, - BOLD, STRIKE, ITALIC - ) + EMOJI, HASHTAG, MENTION, FUNCTION, LATEX, URL, LINK, + BOLD, STRIKE, ITALIC + ) ITALIC wraps hashSetOf( - EMOJI, HASHTAG, MENTION, FUNCTION, LATEX, URL, LINK, - BIG, BOLD, STRIKE, SMALL - ) + EMOJI, HASHTAG, MENTION, FUNCTION, LATEX, URL, LINK, + BIG, BOLD, STRIKE, SMALL + ) MOTION wraps hashSetOf( - EMOJI, HASHTAG, MENTION, FUNCTION, LATEX, URL, LINK, - BOLD, STRIKE, SMALL, ITALIC - ) + EMOJI, HASHTAG, MENTION, FUNCTION, LATEX, URL, LINK, + BOLD, STRIKE, SMALL, ITALIC + ) LINK wraps hashSetOf( - EMOJI, MOTION, FUNCTION, LATEX, - BIG, BOLD, STRIKE, SMALL, ITALIC - ) + EMOJI, MOTION, FUNCTION, LATEX, + BIG, BOLD, STRIKE, SMALL, ITALIC + ) TITLE wraps hashSetOf( - EMOJI, HASHTAG, MENTION, FUNCTION, LATEX, URL, LINK, - BIG, BOLD, STRIKE, SMALL, ITALIC, - MOTION, CODE_INLINE - ) + EMOJI, HASHTAG, MENTION, FUNCTION, LATEX, URL, LINK, + BIG, BOLD, STRIKE, SMALL, ITALIC, + MOTION, CODE_INLINE + ) CENTER wraps hashSetOf( - EMOJI, HASHTAG, MENTION, FUNCTION, LATEX, URL, LINK, - BIG, BOLD, STRIKE, SMALL, ITALIC, - MOTION, CODE_INLINE - ) + EMOJI, HASHTAG, MENTION, FUNCTION, LATEX, URL, LINK, + BIG, BOLD, STRIKE, SMALL, ITALIC, + MOTION, CODE_INLINE + ) FUNCTION wraps hashSetOf( - CODE_BLOCK, QUOTE_INLINE, SEARCH, - EMOJI, HASHTAG, MENTION, LATEX, URL, LINK, - BIG, BOLD, STRIKE, SMALL, ITALIC, - MOTION, CODE_INLINE, - TITLE, CENTER, QUOTE_BLOCK - ) + CODE_BLOCK, QUOTE_INLINE, SEARCH, + EMOJI, HASHTAG, MENTION, LATEX, URL, LINK, + BIG, BOLD, STRIKE, SMALL, ITALIC, + MOTION, CODE_INLINE, + TITLE, CENTER, QUOTE_BLOCK + ) LATEX wraps hashSetOf( - CODE_BLOCK, QUOTE_INLINE, SEARCH, - EMOJI, HASHTAG, MENTION, FUNCTION, URL, LINK, - BIG, BOLD, STRIKE, SMALL, ITALIC, - MOTION, CODE_INLINE, - TITLE, CENTER, QUOTE_BLOCK - ) + CODE_BLOCK, QUOTE_INLINE, SEARCH, + EMOJI, HASHTAG, MENTION, FUNCTION, URL, LINK, + BIG, BOLD, STRIKE, SMALL, ITALIC, + MOTION, CODE_INLINE, + TITLE, CENTER, QUOTE_BLOCK + ) // all except ROOT,TEXT val allSet = hashSetOf( - CODE_BLOCK, QUOTE_INLINE, SEARCH, - EMOJI, HASHTAG, MENTION, FUNCTION, LATEX, URL, LINK, - BIG, BOLD, STRIKE, SMALL, ITALIC, - MOTION, CODE_INLINE, - TITLE, CENTER, QUOTE_BLOCK - ) + CODE_BLOCK, QUOTE_INLINE, SEARCH, + EMOJI, HASHTAG, MENTION, FUNCTION, LATEX, URL, LINK, + BIG, BOLD, STRIKE, SMALL, ITALIC, + MOTION, CODE_INLINE, + TITLE, CENTER, QUOTE_BLOCK + ) QUOTE_BLOCK wraps allSet @@ -1285,15 +1289,15 @@ object MisskeyMarkdownDecoder { // マークダウン要素 internal class Node( - val type: NodeType, // ノード種別 - val args: Array = emptyArray(), // 引数 - parentNode: Node? - ) { + val type: NodeType, // ノード種別 + val args: Array = emptyArray(), // 引数 + parentNode: Node? + ) { val childNodes = LinkedList() internal val quoteNest: Int = (parentNode?.quoteNest ?: 0) + when (type) { - NodeType.QUOTE_BLOCK, NodeType.QUOTE_INLINE -> 1 + NodeType.QUOTE_BLOCK, NodeType.QUOTE_INLINE -> 1 else -> 0 } @@ -1301,25 +1305,25 @@ object MisskeyMarkdownDecoder { // マークダウン要素の出現位置 internal class NodeDetected( - val node: Node, - val start: Int, // テキスト中の開始位置 - val end: Int, // テキスト中の終了位置 - val textInside: String, // 内部範囲。親から継承する場合もあるし独自に作る場合もある - val startInside: Int, // 内部範囲の開始位置 - private val lengthInside: Int // 内部範囲の終了位置 - ) { + val node: Node, + val start: Int, // テキスト中の開始位置 + val end: Int, // テキスト中の終了位置 + val textInside: String, // 内部範囲。親から継承する場合もあるし独自に作る場合もある + val startInside: Int, // 内部範囲の開始位置 + private val lengthInside: Int // 内部範囲の終了位置 + ) { val endInside: Int get() = startInside + lengthInside } internal class NodeParseEnv( - val useFunction: Boolean, - private val parentNode: Node, - val text: String, - start: Int, - val end: Int - ) { + val useFunction: Boolean, + private val parentNode: Node, + val text: String, + start: Int, + val end: Int + ) { private val childNodes = parentNode.childNodes private val allowInside: HashSet = @@ -1348,7 +1352,7 @@ object MisskeyMarkdownDecoder { var i = lastEnd //スキャン中の位置 while (i < end) { // 注目位置の文字に関連するパーサー - val lastParsers = nodeParserMap[text[i].code] + val lastParsers = nodeParserMap[text[i].code] if (lastParsers == null) { ++i continue @@ -1366,13 +1370,13 @@ object MisskeyMarkdownDecoder { val n = d.node if (!allowInside.contains(d.node.type)) { log.w( - "not allowed : ${parentNode.type} => ${n.type} ${ - text.substring( - d.start, - d.end - ) - }" - ) + "not allowed : ${parentNode.type} => ${n.type} ${ + text.substring( + d.start, + d.end + ) + }" + ) null } else { d @@ -1391,49 +1395,49 @@ object MisskeyMarkdownDecoder { lastEnd = i NodeParseEnv( - useFunction, - detected.node, - detected.textInside, - detected.startInside, - detected.endInside - ).parseInside() + useFunction, + detected.node, + detected.textInside, + detected.startInside, + detected.endInside + ).parseInside() } closeText(end) } internal fun makeDetected( - type: NodeType, - args: Array, - start: Int, - end: Int, - textInside: String, - startInside: Int, - lengthInside: Int - ): NodeDetected { + type: NodeType, + args: Array, + start: Int, + end: Int, + textInside: String, + startInside: Int, + lengthInside: Int + ): NodeDetected { val node = Node(type, args, parentNode) if (DEBUG) log.d( - "NodeDetected: ${node.type} inside=${ - textInside.substring(startInside, startInside + lengthInside) - }" - ) + "NodeDetected: ${node.type} inside=${ + textInside.substring(startInside, startInside + lengthInside) + }" + ) return NodeDetected( - node, - start, - end, - textInside, - startInside, - lengthInside - ) + node, + start, + end, + textInside, + startInside, + lengthInside + ) } } // ノードのパースを行う関数をキャプチャパラメータつきで生成する private fun simpleParser( - pattern: Pattern, type: NodeType - ): NodeParseEnv.() -> NodeDetected? = { + pattern: Pattern, type: NodeType + ): NodeParseEnv.() -> NodeDetected? = { val matcher = remainMatcher(pattern) when { !matcher.find() -> null @@ -1441,11 +1445,11 @@ object MisskeyMarkdownDecoder { else -> { val textInside = matcher.groupEx(1)!! makeDetected( - type, - arrayOf(textInside), - matcher.start(), matcher.end(), - this.text, matcher.start(1), textInside.length - ) + type, + arrayOf(textInside), + matcher.start(), matcher.end(), + this.text, matcher.start(1), textInside.length + ) } } @@ -1468,11 +1472,11 @@ object MisskeyMarkdownDecoder { val name = matcher.groupEx(1)?.ellipsizeDot3(3) ?: "???" val textInside = matcher.groupEx(2)!! return makeDetected( - type, - arrayOf(name), - matcher.start(), matcher.end(), - this.text, matcher.start(2), textInside.length - ) + type, + arrayOf(name), + matcher.start(), matcher.end(), + this.text, matcher.start(2), textInside.length + ) } } val type = NodeType.TITLE @@ -1480,77 +1484,82 @@ object MisskeyMarkdownDecoder { if (matcher.find()) { val textInside = matcher.groupEx(1)!! return makeDetected( - type, - arrayOf(textInside), - matcher.start(), matcher.end(), - this.text, matcher.start(1), textInside.length - ) + type, + arrayOf(textInside), + matcher.start(), matcher.end(), + this.text, matcher.start(1), textInside.length + ) } return null } + @Suppress("SpellCheckingInspection") private val latexEscape = listOf( - "\\#" to "#", - "\\$" to "$", - "\\%" to "%", - "\\&" to "&", - "\\_" to "_", - "\\{" to "{", - "\\}" to "}", - "\\;" to "", - "\\!" to "", + "\\#" to "#", + "\\$" to "$", + "\\%" to "%", + "\\&" to "&", + "\\_" to "_", + "\\{" to "{", + "\\}" to "}", + "\\;" to "", + "\\!" to "", - "\\textbackslash" to "\\", - "\\backslash" to "\\", - "\\textasciitilde" to "~", - "\\textasciicircum" to "^", - "\\textbar" to "|", - "\\textless" to "<", - "\\textgreater" to ">", - ).sortedByDescending{ it.first.length} + "\\textbackslash" to "\\", + "\\backslash" to "\\", + "\\textasciitilde" to "~", + "\\textasciicircum" to "^", + "\\textbar" to "|", + "\\textless" to "<", + "\\textgreater" to ">", + ).sortedByDescending { it.first.length } - private fun partialEquals(src:String,start:Int,needle:String):Boolean{ - for( i in needle.indices){ - if( src[start+i] != needle[i]) return false - } - return true - } + private fun partialEquals(src: String, start: Int, needle: String): Boolean { + for (i in needle.indices) { + if (src[start + i] != needle[i]) return false + } + return true + } - private fun String.unescapeLatex():String{ - val sb = StringBuilder(length) - val end = length - var i = 0 - while(i NodeDetected?>>().apply { fun addParser( - firstChars: String, - vararg nodeParsers: NodeParseEnv.() -> NodeDetected? - ) { + firstChars: String, + vararg nodeParsers: NodeParseEnv.() -> NodeDetected? + ) { for (s in firstChars) { - put(s.code, nodeParsers) + put(s.code, nodeParsers) } } // Strike ~~...~~ addParser( - "~", simpleParser( - """\A~~(.+?)~~""".asciiPattern(), NodeType.STRIKE - ) - ) + "~", simpleParser( + """\A~~(.+?)~~""".asciiPattern(), NodeType.STRIKE + ) + ) // Quote "..." addParser( - "\"", simpleParser( - """\A"([^\x0d\x0a]+?)\n"[\x0d\x0a]*""".asciiPattern(), NodeType.QUOTE_INLINE - ) - ) + "\"", simpleParser( + """\A"([^\x0d\x0a]+?)\n"[\x0d\x0a]*""".asciiPattern(), NodeType.QUOTE_INLINE + ) + ) // Quote (行頭)>...(改行) // この正規表現の場合は \A ではなく ^ で各行の始端にマッチさせる @@ -1613,97 +1622,97 @@ object MisskeyMarkdownDecoder { .asciiPattern(Pattern.MULTILINE) addParser(">", { - if (pos > 0) { - val c = text[pos - 1] - if (c != '\r' && c != '\n') { - //直前が改行文字ではない - if (DEBUG) log.d("QUOTE: previous char is not line end. ${c} pos=$pos text=$text") - return@addParser null - } - } + if (pos > 0) { + val c = text[pos - 1] + if (c != '\r' && c != '\n') { + //直前が改行文字ではない + if (DEBUG) log.d("QUOTE: previous char is not line end. ${c} pos=$pos text=$text") + return@addParser null + } + } - var p = pos - val content = StringBuilder() - val matcher = remainMatcher(reQuoteBlock) - while (true) { - if (!matcher.find(p)) break - p = matcher.end() - if (content.isNotEmpty()) content.append('\n') - content.append(matcher.groupEx(1)) - // 改行の直後なので次回マッチの ^ は大丈夫なはず… - } - if (content.isNotEmpty()) content.append('\n') + var p = pos + val content = StringBuilder() + val matcher = remainMatcher(reQuoteBlock) + while (true) { + if (!matcher.find(p)) break + p = matcher.end() + if (content.isNotEmpty()) content.append('\n') + content.append(matcher.groupEx(1)) + // 改行の直後なので次回マッチの ^ は大丈夫なはず… + } + if (content.isNotEmpty()) content.append('\n') - if (p <= pos) { - // > のあとに全く何もない - if (DEBUG) log.d("QUOTE: not a quote") - return@addParser null - } - val textInside = content.toString() + if (p <= pos) { + // > のあとに全く何もない + if (DEBUG) log.d("QUOTE: not a quote") + return@addParser null + } + val textInside = content.toString() - makeDetected( - NodeType.QUOTE_BLOCK, - emptyArray(), - pos, p, - textInside, 0, textInside.length - ) - }) + makeDetected( + NodeType.QUOTE_BLOCK, + emptyArray(), + pos, p, + textInside, 0, textInside.length + ) + }) // 絵文字 :emoji: addParser( - ":", - simpleParser( - """\A:([a-zA-Z0-9+-_@]+):""".asciiPattern(), NodeType.EMOJI - ) - ) + ":", + simpleParser( + """\A:([a-zA-Z0-9+-_@]+):""".asciiPattern(), NodeType.EMOJI + ) + ) // モーション addParser( - "(", simpleParser( - """\A\Q(((\E(.+?)\Q)))\E""".asciiPattern(Pattern.DOTALL), NodeType.MOTION - ) - ) + "(", simpleParser( + """\A\Q(((\E(.+?)\Q)))\E""".asciiPattern(Pattern.DOTALL), NodeType.MOTION + ) + ) val reHtmlTag = """\A<([a-z]+)>(.+?)""".asciiPattern(Pattern.DOTALL) addParser("<", { - val matcher = remainMatcher(reHtmlTag) - when { - !matcher.find() -> null + val matcher = remainMatcher(reHtmlTag) + when { + !matcher.find() -> null - else -> { - val tagName = matcher.groupEx(1)!! - val textInside = matcher.groupEx(2)!! + else -> { + val tagName = matcher.groupEx(1)!! + val textInside = matcher.groupEx(2)!! - fun a(type: NodeType) = makeDetected( - type, - arrayOf(textInside), - matcher.start(), matcher.end(), - this.text, matcher.start(2), textInside.length - ) + fun a(type: NodeType) = makeDetected( + type, + arrayOf(textInside), + matcher.start(), matcher.end(), + this.text, matcher.start(2), textInside.length + ) - when (tagName) { - "motion" -> a(NodeType.MOTION) - "center" -> a(NodeType.CENTER) - "small" -> a(NodeType.SMALL) - "i" -> a(NodeType.ITALIC) - else -> null - } - } - } - }) + when (tagName) { + "motion" -> a(NodeType.MOTION) + "center" -> a(NodeType.CENTER) + "small" -> a(NodeType.SMALL) + "i" -> a(NodeType.ITALIC) + else -> null + } + } + } + }) // ***big*** **bold** addParser( - "*" - // 処理順序に意味があるので入れ替えないこと - // 記号列が長い順にパースを試す - , simpleParser( - """^\Q***\E(.+?)\Q***\E""".asciiPattern(), NodeType.BIG - ), simpleParser( - """^\Q**\E(.+?)\Q**\E""".asciiPattern(), NodeType.BOLD - ) - ) + "*" + // 処理順序に意味があるので入れ替えないこと + // 記号列が長い順にパースを試す + , simpleParser( + """^\Q***\E(.+?)\Q***\E""".asciiPattern(), NodeType.BIG + ), simpleParser( + """^\Q**\E(.+?)\Q**\E""".asciiPattern(), NodeType.BOLD + ) + ) val reAlnum = """[A-Za-z0-9]""".asciiPattern() @@ -1713,24 +1722,24 @@ object MisskeyMarkdownDecoder { addParser("h", { - // 直前の文字が英数字ならURLの開始とはみなさない - if (pos > 0 && MatcherCache.matcher(reAlnum, text, pos - 1, pos).find()) { - return@addParser null - } + // 直前の文字が英数字ならURLの開始とはみなさない + if (pos > 0 && MatcherCache.matcher(reAlnum, text, pos - 1, pos).find()) { + return@addParser null + } - val matcher = remainMatcher(reUrl) - if (!matcher.find()) { - return@addParser null - } + val matcher = remainMatcher(reUrl) + if (!matcher.find()) { + return@addParser null + } - val url = matcher.groupEx(1)!!.removeOrphanedBrackets(urlSafe = true) - makeDetected( - NodeType.URL, - arrayOf(url), - matcher.start(), matcher.start() + url.length, - "", 0, 0 - ) - }) + val url = matcher.groupEx(1)!!.removeOrphanedBrackets(urlSafe = true) + makeDetected( + NodeType.URL, + arrayOf(url), + matcher.start(), matcher.start() + url.length, + "", 0, 0 + ) + }) // 検索 val reSearchButton = """\A(検索|\[検索]|Search|\[Search])(\n|\z)""" @@ -1759,11 +1768,11 @@ object MisskeyMarkdownDecoder { keyword?.isEmpty() != false -> null else -> makeDetected( - NodeType.SEARCH, - arrayOf(keyword), - pos - (keyword.length + 1), matcher.end(), - this.text, pos - (keyword.length + 1), keyword.length - ) + NodeType.SEARCH, + arrayOf(keyword), + pos - (keyword.length + 1), matcher.end(), + this.text, pos - (keyword.length + 1), keyword.length + ) } } } @@ -1784,14 +1793,14 @@ object MisskeyMarkdownDecoder { else -> { val title = matcher.groupEx(1)!! makeDetected( - NodeType.LINK, - arrayOf( - title, matcher.groupEx(2)!! // url - , text[pos].toString() // silent なら "?" になる - ), - matcher.start(), matcher.end(), - this.text, matcher.start(1), title.length - ) + NodeType.LINK, + arrayOf( + title, matcher.groupEx(2)!! // url + , text[pos].toString() // silent なら "?" になる + ), + matcher.start(), matcher.end(), + this.text, matcher.start(1), title.length + ) } } } @@ -1811,29 +1820,29 @@ object MisskeyMarkdownDecoder { // メールアドレスの@の手前に使える文字なら真 val mailChars = SparseBooleanArray().apply { for (it in '0'..'9') { - put(it.code, true) + put(it.code, true) } for (it in 'A'..'Z') { - put(it.code, true) + put(it.code, true) } for (it in 'a'..'z') { - put(it.code, true) + put(it.code, true) } """${'$'}!#%&'`"*+-/=?^_{|}~""".forEach { put(it.code, true) } } addParser("@", { - val matcher = remainMatcher(TootAccount.reMisskeyMentionMFM) + val matcher = remainMatcher(TootAccount.reMisskeyMentionMFM) - when { - !matcher.find() -> null + when { + !matcher.find() -> null - else -> when { - // 直前の文字がメールアドレスの@の手前に使える文字ならメンションではない - pos > 0 && mailChars.get(text.codePointBefore(pos)) -> null + else -> when { + // 直前の文字がメールアドレスの@の手前に使える文字ならメンションではない + pos > 0 && mailChars.get(text.codePointBefore(pos)) -> null - else -> { + else -> { // log.d( // "mention detected: ${matcher.group(1)},${matcher.group(2)},${ // matcher.group( @@ -1841,19 +1850,19 @@ object MisskeyMarkdownDecoder { // ) // }" // ) - makeDetected( - NodeType.MENTION, - arrayOf( - matcher.groupEx(1)!!, - matcher.groupEx(2) ?: "" // username, host - ), - matcher.start(), matcher.end(), - "", 0, 0 - ) - } - } - } - }) + makeDetected( + NodeType.MENTION, + arrayOf( + matcher.groupEx(1)!!, + matcher.groupEx(2) ?: "" // username, host + ), + matcher.start(), matcher.end(), + "", 0, 0 + ) + } + } + } + }) // Hashtag val reHashtag = """\A#([^\s.,!?#:]+)""".asciiPattern() @@ -1861,55 +1870,55 @@ object MisskeyMarkdownDecoder { addParser("#", { - if (pos > 0 && MatcherCache.matcher(reAlnum, text, pos - 1, pos).find()) { - // 直前に英数字があるならタグにしない - return@addParser null - } + if (pos > 0 && MatcherCache.matcher(reAlnum, text, pos - 1, pos).find()) { + // 直前に英数字があるならタグにしない + return@addParser null + } - val matcher = remainMatcher(reHashtag) - if (!matcher.find()) { - // タグにマッチしない - return@addParser null - } + val matcher = remainMatcher(reHashtag) + if (!matcher.find()) { + // タグにマッチしない + return@addParser null + } - // 先頭の#を含まないタグテキスト - val tag = matcher.groupEx(1)!!.removeOrphanedBrackets() + // 先頭の#を含まないタグテキスト + val tag = matcher.groupEx(1)!!.removeOrphanedBrackets() - if (tag.isEmpty() || tag.length > 50 || reDigitsOnly.matcher(tag).find()) { - // 空文字列、50文字超過、数字だけのタグは不許可 - return@addParser null - } + if (tag.isEmpty() || tag.length > 50 || reDigitsOnly.matcher(tag).find()) { + // 空文字列、50文字超過、数字だけのタグは不許可 + return@addParser null + } - makeDetected( - NodeType.HASHTAG, - arrayOf(tag), - matcher.start(), matcher.start() + 1 + tag.length, - "", 0, 0 - ) - }) + makeDetected( + NodeType.HASHTAG, + arrayOf(tag), + matcher.start(), matcher.start() + 1 + tag.length, + "", 0, 0 + ) + }) // code (ブロック、インライン) addParser( - "`", simpleParser( - """\A```(?:.*)\n([\s\S]+?)\n```(?:\n|$)""".asciiPattern(), NodeType.CODE_BLOCK - /* - (A) - ```code``` は 閉じる部分の前後に改行がないのでダメ - (B) - ```lang - code - code - code - ``` - はlang部分は表示されない - (C) - STの表示上の都合で閉じる部分の後の改行が複数あっても全て除去する - */ - ), simpleParser( - // インラインコードは内部にとある文字を含むと認識されない。理由は顔文字と衝突するからだとか - """\A`([^`´\x0d\x0a]+)`""".asciiPattern(), NodeType.CODE_INLINE - ) - ) + "`", simpleParser( + """\A```(?:.*)\n([\s\S]+?)\n```(?:\n|$)""".asciiPattern(), NodeType.CODE_BLOCK + /* + (A) + ```code``` は 閉じる部分の前後に改行がないのでダメ + (B) + ```lang + code + code + code + ``` + はlang部分は表示されない + (C) + STの表示上の都合で閉じる部分の後の改行が複数あっても全て除去する + */ + ), simpleParser( + // インラインコードは内部にとある文字を含むと認識されない。理由は顔文字と衝突するからだとか + """\A`([^`´\x0d\x0a]+)`""".asciiPattern(), NodeType.CODE_INLINE + ) + ) } // 入力テキストからタグを抽出するために使う @@ -1943,8 +1952,10 @@ object MisskeyMarkdownDecoder { if (src != null) { val root = Node(NodeType.ROOT, emptyArray(), null) - NodeParseEnv(useFunction = (options.linkHelper?.misskeyVersion - ?: 12) >= 11, root, src, 0, src.length).parseInside() + NodeParseEnv( + useFunction = (options.linkHelper?.misskeyVersion + ?: 12) >= 11, root, src, 0, src.length + ).parseInside() env.fireRender(root).setSpan(env.sb) } diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/PostHelper.kt b/app/src/main/java/jp/juggler/subwaytooter/util/PostHelper.kt index 208dc393..615f5698 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/util/PostHelper.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/util/PostHelper.kt @@ -350,7 +350,7 @@ class PostHelper( if (visibility == checkVisibility && !checkFun(instance)) { val strVisibility = Styler.getVisibilityString(activity, account.isMisskey, checkVisibility) return@runApiTask TootApiResult( - getString(R.string.server_has_no_support_of_visibility,strVisibility) + getString(R.string.server_has_no_support_of_visibility, strVisibility) ) } } @@ -783,7 +783,7 @@ class PostHelper( val limit = 100 val s = src.substring(start, end) val acct_list = AcctSet.searchPrefix(s, limit) - log.d("search for %s, result=%d", s, acct_list.size) + log.d("search for ${s}, result=${acct_list.size}") if (acct_list.isEmpty()) { closeAcctPopup() } else { @@ -811,7 +811,7 @@ class PostHelper( val limit = 100 val s = src.substring(last_sharp + 1, end) val tag_list = TagSet.searchPrefix(s, limit) - log.d("search for %s, result=%d", s, tag_list.size) + log.d("search for ${s}, result=${tag_list.size}") if (tag_list.isEmpty()) { closeAcctPopup() } else { @@ -863,7 +863,7 @@ class PostHelper( val s = src.substring(last_colon + 1, end).lowercase().replace('-', '_') val matches = EmojiDecoder.searchShortCode(activity, s, remain) - log.d("checkEmoji: search for %s, result=%d", s, matches.size) + log.d("checkEmoji: search for ${s}, result=${matches.size}") code_list.addAll(matches) } diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/TaskList.kt b/app/src/main/java/jp/juggler/subwaytooter/util/TaskList.kt index 58bf316d..54e19634 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/util/TaskList.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/util/TaskList.kt @@ -8,79 +8,78 @@ import java.io.FileNotFoundException import java.util.* class TaskList { - - companion object { - - private val log = LogCategory("TaskList") - private const val FILE_TASK_LIST = "JOB_TASK_LIST" - } - - private lateinit var _list : LinkedList - - @Synchronized - private fun prepareList(context : Context) : LinkedList { - if(! ::_list.isInitialized) { - _list = LinkedList() - - try { - context.openFileInput(FILE_TASK_LIST).use { inputStream -> - val bao = ByteArrayOutputStream() - IOUtils.copy(inputStream, bao) - bao.toByteArray().decodeUTF8().decodeJsonArray().objectList().forEach { - _list.add(it) - } - } - } catch(ex : FileNotFoundException) { - log.e(ex, "prepareList: file not found.") - } catch(ex : Throwable) { - log.trace(ex, "TaskList: prepareArray failed.") - } - } - - return _list - } - - @Synchronized - private fun saveArray(context : Context) { - val list = prepareList(context) - try { - log.d("saveArray size=%s", list.size) - val data = JsonArray(list).toString().encodeUTF8() - context.openFileOutput(FILE_TASK_LIST, Context.MODE_PRIVATE) - .use { IOUtils.write(data, it) } - } catch(ex : Throwable) { - log.trace(ex) - log.e(ex, "TaskList: saveArray failed.size=%s", list.size) - } - - } - - @Synchronized - fun addLast(context : Context, removeOld : Boolean, taskData : JsonObject) { - val list = prepareList(context) - if(removeOld) { - val it = list.iterator() - while(it.hasNext()) { - val item = it.next() - if(taskData == item) it.remove() - } - } - list.addLast(taskData) - saveArray(context) - } - - @Suppress("unused") - @Synchronized - fun hasNext(context : Context) : Boolean { - return prepareList(context).isNotEmpty() - } - - @Synchronized - fun next(context : Context) : JsonObject? { - val list = prepareList(context) - val item = if(list.isEmpty()) null else list.removeFirst() - saveArray(context) - return item - } - + + companion object { + + private val log = LogCategory("TaskList") + private const val FILE_TASK_LIST = "JOB_TASK_LIST" + } + + private lateinit var _list: LinkedList + + @Synchronized + private fun prepareList(context: Context): LinkedList { + if (!::_list.isInitialized) { + _list = LinkedList() + + try { + context.openFileInput(FILE_TASK_LIST).use { inputStream -> + val bao = ByteArrayOutputStream() + IOUtils.copy(inputStream, bao) + bao.toByteArray().decodeUTF8().decodeJsonArray().objectList().forEach { + _list.add(it) + } + } + } catch (ex: FileNotFoundException) { + log.e(ex, "prepareList: file not found.") + } catch (ex: Throwable) { + log.trace(ex, "TaskList: prepareArray failed.") + } + } + + return _list + } + + @Synchronized + private fun saveArray(context: Context) { + val list = prepareList(context) + try { + log.d("saveArray size=${list.size}") + val data = JsonArray(list).toString().encodeUTF8() + context.openFileOutput(FILE_TASK_LIST, Context.MODE_PRIVATE) + .use { IOUtils.write(data, it) } + } catch (ex: Throwable) { + log.trace(ex, "TaskList: saveArray failed.size=${list.size}") + } + + } + + @Synchronized + fun addLast(context: Context, removeOld: Boolean, taskData: JsonObject) { + val list = prepareList(context) + if (removeOld) { + val it = list.iterator() + while (it.hasNext()) { + val item = it.next() + if (taskData == item) it.remove() + } + } + list.addLast(taskData) + saveArray(context) + } + + @Suppress("unused") + @Synchronized + fun hasNext(context: Context): Boolean { + return prepareList(context).isNotEmpty() + } + + @Synchronized + fun next(context: Context): JsonObject? { + val list = prepareList(context) + val item = if (list.isEmpty()) null else list.removeFirst() + saveArray(context) + return item + } + } diff --git a/app/src/main/java/jp/juggler/subwaytooter/view/MyListView.kt b/app/src/main/java/jp/juggler/subwaytooter/view/MyListView.kt index 63057146..89d7e4f0 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/view/MyListView.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/view/MyListView.kt @@ -11,42 +11,42 @@ import jp.juggler.subwaytooter.StatusButtonsPopup import jp.juggler.util.LogCategory class MyListView : ListView { - - companion object { - private val log = LogCategory("MyListView") - } - constructor(context : Context) : super(context) - constructor(context : Context, attrs : AttributeSet) : super(context, attrs) - constructor(context : Context, attrs : AttributeSet, defStyleAttr : Int) : super(context, attrs, defStyleAttr) - - @SuppressLint("ClickableViewAccessibility") - override fun onTouchEvent(ev : MotionEvent) : Boolean { - - // ポップアップを閉じた時にクリックでリストを触ったことになってしまう不具合の回避 - val now = SystemClock.elapsedRealtime() - if(now - StatusButtonsPopup.last_popup_close < 30L) { - val action = ev.action - if(action == MotionEvent.ACTION_DOWN) { - // ポップアップを閉じた直後はタッチダウンを無視する - return false - } - - val rv = super.onTouchEvent(ev) - log.d("onTouchEvent action=%s, rv=%s", action, rv) - return rv - } - - return super.onTouchEvent(ev) - } - - override fun layoutChildren() { - try { - super.layoutChildren() - } catch(ex : Throwable) { - log.trace(ex) - } - - } - + companion object { + private val log = LogCategory("MyListView") + } + + constructor(context: Context) : super(context) + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) + constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) + + @SuppressLint("ClickableViewAccessibility") + override fun onTouchEvent(ev: MotionEvent): Boolean { + + // ポップアップを閉じた時にクリックでリストを触ったことになってしまう不具合の回避 + val now = SystemClock.elapsedRealtime() + if (now - StatusButtonsPopup.last_popup_close < 30L) { + val action = ev.action + if (action == MotionEvent.ACTION_DOWN) { + // ポップアップを閉じた直後はタッチダウンを無視する + return false + } + + val rv = super.onTouchEvent(ev) + log.d("onTouchEvent action=${action}, rv=${rv}") + return rv + } + + return super.onTouchEvent(ev) + } + + override fun layoutChildren() { + try { + super.layoutChildren() + } catch (ex: Throwable) { + log.trace(ex) + } + + } + } diff --git a/app/src/main/java/jp/juggler/subwaytooter/view/MyNetworkImageView.kt b/app/src/main/java/jp/juggler/subwaytooter/view/MyNetworkImageView.kt index 9e2dbceb..d903b493 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/view/MyNetworkImageView.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/view/MyNetworkImageView.kt @@ -28,549 +28,549 @@ import jp.juggler.util.LogCategory import jp.juggler.util.clipRange class MyNetworkImageView : AppCompatImageView { - - companion object { - - internal val log = LogCategory("MyNetworkImageView") - } - - // ロード中などに表示するDrawableのリソースID - private var mDefaultImage : Drawable? = null - - // エラー時に表示するDrawableのリソースID - private var mErrorImage : Drawable? = null - - // 角丸の半径。元画像の短辺に対する割合を指定するらしい - internal var mCornerRadius = 0f - - // 表示したい画像のURL - private var mUrl : String? = null - private var mMayGif : Boolean = false - - // 非同期処理のキャンセル - private var mTarget : Target<*>? = null - - private val proc_load_image : Runnable = Runnable { loadImageIfNecessary() } - private val proc_focus_point : Runnable = Runnable { updateFocusPoint() } - - private var media_type_drawable : Drawable? = null - private var media_type_bottom = 0 - private var media_type_left = 0 - - constructor(context : Context) - : super(context) - - constructor(context : Context, attrs : AttributeSet) - : super(context, attrs) - - constructor(context : Context, attrs : AttributeSet, defStyleAttr : Int) - : super(context, attrs, defStyleAttr) - - fun setDefaultImage(defaultImage : Drawable?) { - mDefaultImage = defaultImage - loadImageIfNecessary() - } - - fun setErrorImage(errorImage : Drawable?) { - mErrorImage = errorImage - loadImageIfNecessary() - } - - fun setImageUrl( - pref : SharedPreferences, - r : Float, - url : String?, - gifUrlArg : String? = null - ) { - - mCornerRadius = r - - val gif_url = if(Pref.bpEnableGifAnimation(pref)) gifUrlArg else null - - if(gif_url?.isNotEmpty() == true) { - mUrl = gif_url - mMayGif = true - } else { - mUrl = url - mMayGif = false - } - loadImageIfNecessary() - } - - private fun getGlide() : RequestManager? { - try { - return Glide.with(context) - } catch(ex : IllegalArgumentException) { - if(ex.message?.contains("destroyed activity") == true) { - // ignore it - } else { - log.e(ex, "Glide.with() failed.") - } - } catch(ex : Throwable) { - log.e(ex, "Glide.with() failed.") - } - return null - } - - fun cancelLoading(defaultDrawable : Drawable? = null) { - - val d = drawable - if(d is Animatable) { - if(d.isRunning) { - //warning.d("cancelLoading: Animatable.stop()") - d.stop() - } - } - - setImageDrawable(defaultDrawable) - - val target = mTarget - if(target != null) { - try { - getGlide()?.clear(target) - } catch(ex : Throwable) { - log.e(ex, "Glide.clear() failed.") - } - - mTarget = null - } - - } - - // 必要なら非同期処理を開始する - private fun loadImageIfNecessary() { - try { - val url = mUrl - if(url?.isEmpty() != false) { - // if the URL to be loaded in this view is empty, - // cancel any old requests and clear the currently loaded image. - cancelLoading(mDefaultImage) - return - } - - // すでにリクエストが発行済みで、リクエストされたURLが同じなら何もしない - if((mTarget as? UrlTarget)?.urlLoading == url) return - - // if there is a pre-existing request, cancel it if it's fetching a different URL. - cancelLoading(mDefaultImage) - - // 非表示状態ならロードを延期する - if(! isShown) return - - var wrapWidth = false - var wrapHeight = false - val lp = layoutParams - if(lp != null) { - wrapWidth = lp.width == ViewGroup.LayoutParams.WRAP_CONTENT - wrapHeight = lp.height == ViewGroup.LayoutParams.WRAP_CONTENT - } - - // Calculate the max image width / height to use while ignoring WRAP_CONTENT dimens. - val desiredWidth = if(wrapWidth) Target.SIZE_ORIGINAL else width - val desiredHeight = if(wrapHeight) Target.SIZE_ORIGINAL else height - - if(desiredWidth != Target.SIZE_ORIGINAL && desiredWidth <= 0 - || desiredHeight != Target.SIZE_ORIGINAL && desiredHeight <= 0 - ) { - // desiredWidth,desiredHeight の指定がおかしいと非同期処理中にSimpleTargetが落ちる - // おそらくレイアウト後に再度呼び出される - return - } - - val glideHeaders = LazyHeaders.Builder() - .addHeader("Accept", "image/webp,image/*,*/*;q=0.8") - .build() - - val glideUrl = GlideUrl(url, glideHeaders) - - mTarget = if(mMayGif) { - getGlide() - ?.load(glideUrl) - ?.into(MyTargetGif(url)) - } else { - getGlide() - ?.load(glideUrl) - ?.into(MyTarget(url)) - } - } catch(ex : Throwable) { - log.trace(ex) - } - } - - private fun replaceGifDrawable(resource : GifDrawable) : Drawable { - // ディスクキャッシュから読んだ画像は角丸が正しく扱われない - // MyGifDrawable に差し替えて描画させる - try { - return MyGifDrawable(resource, mCornerRadius) - } catch(ex : Throwable) { - log.trace(ex) - } - return resource - } - - private fun replaceBitmapDrawable(resource : BitmapDrawable) : Drawable { - try { - val bitmap = resource.bitmap - if(bitmap != null) return replaceBitmapDrawable(bitmap) - } catch(ex : Throwable) { - log.trace(ex) - } - return resource - } - - private fun replaceBitmapDrawable(bitmap : Bitmap) : Drawable { - val d = RoundedBitmapDrawableFactory.create(resources, bitmap) - d.cornerRadius = mCornerRadius - return d - } - - private fun onLoadFailed(urlLoading : String) { - try { - // 別の画像を表示するよう指定が変化していたなら何もしない - if(urlLoading != mUrl) return - - // エラー表示用の画像リソースが指定されていたら使う - when(val drawable = mErrorImage) { - null -> { - // このタイミングでImageViewのDrawableを変更するとチラつきの元になるので何もしない - } - - else -> setImageDrawable(drawable) - } - } catch(ex : Throwable) { - log.trace(ex) - } - } - - private interface UrlTarget { - - val urlLoading : String - } - - // 静止画用のターゲット - private inner class MyTarget( - override val urlLoading : String - ) : ImageViewTarget(this@MyNetworkImageView), UrlTarget { - - // errorDrawable The error drawable to optionally show, or null. - override fun onLoadFailed(errorDrawable : Drawable?) { - onLoadFailed(urlLoading) - } - - override fun setResource(resource : Drawable?) { - try { - // 別の画像を表示するよう指定が変化していたなら何もしない - if(urlLoading != mUrl) return - - if(mCornerRadius > 0f) { - if(resource is BitmapDrawable) { - // BitmapDrawableは角丸処理が可能。 - setImageDrawable(replaceBitmapDrawable(resource.bitmap)) - return - } - // その他のDrawable - // たとえばInstanceTickerのアイコンにSVGが使われていたらPictureDrawableになる - log.w("cornerRadius=$mCornerRadius,drawable=$resource,url=$urlLoading") - } - - setImageDrawable(resource) - return - - } catch(ex : Throwable) { - log.trace(ex) - } - } - - } - - private inner class MyTargetGif( - override val urlLoading : String - ) : ImageViewTarget(this@MyNetworkImageView), UrlTarget { - - private var glide_drawable : Drawable? = null - - override fun onLoadFailed(errorDrawable : Drawable?) = onLoadFailed(urlLoading) - - override fun onResourceReady( - drawable : Drawable, - transition : Transition? - ) { - try { - // 別の画像を表示するよう指定が変化していたなら何もしない - if(urlLoading != mUrl) return - - afterResourceReady( - transition, - when { - mCornerRadius <= 0f -> { - // 角丸でないならそのまま使う - drawable - } - - // GidDrawableを置き換える - drawable is GifDrawable -> replaceGifDrawable(drawable) - - // Glide 4.xから、静止画はBitmapDrawableになった - drawable is BitmapDrawable -> replaceBitmapDrawable(drawable) - - else -> { - log.d("onResourceReady: drawable class=%s", drawable.javaClass) - drawable - } - } - ) - } catch(ex : Throwable) { - log.trace(ex) - } - } - - private fun afterResourceReady(transition : Transition?, drawable : Drawable) { - super.onResourceReady(drawable, transition) - - // if( ! drawable.isAnimated() ){ - // //XXX: Try to generalize this to other sizes/shapes. - // // This is a dirty hack that tries to make loading square thumbnails and then square full images less costly - // // by forcing both the smaller thumb and the larger version to have exactly the same intrinsic dimensions. - // // If a drawable is replaced in an ImageView by another drawable with different intrinsic dimensions, - // // the ImageView requests a layout. Scrolling rapidly while replacing thumbs with larger images triggers - // // lots of these calls and causes significant amounts of jank. - // float viewRatio = view.getWidth() / (float) view.getHeight(); - // float drawableRatio = drawable.getIntrinsicWidth() / (float) drawable.getIntrinsicHeight(); - // if( Math.abs( viewRatio - 1f ) <= SQUARE_RATIO_MARGIN - // && Math.abs( drawableRatio - 1f ) <= SQUARE_RATIO_MARGIN ){ - // drawable = new SquaringDrawable( drawable, view.getWidth() ); - // } - // } - - this.glide_drawable = drawable - if(drawable is GifDrawable) { - drawable.setLoopCount(GifDrawable.LOOP_FOREVER) - drawable.start() - } else if(drawable is MyGifDrawable) { - drawable.setLoopCount(GifDrawable.LOOP_FOREVER) - drawable.start() - } - } - - // super.onResourceReady から呼ばれる - override fun setResource(drawable : Drawable?) { - setImageDrawable(drawable) - } - - override fun onStart() { - val drawable = glide_drawable - if(drawable is Animatable && ! drawable.isRunning) { - log.d("MyTargetGif onStart glide_drawable=%s", drawable) - drawable.start() - } - } - - override fun onStop() { - val drawable = glide_drawable - if(drawable is Animatable && drawable.isRunning) { - log.d("MyTargetGif onStop glide_drawable=%s", drawable) - drawable.stop() - } - } - - override fun onDestroy() { - val drawable = glide_drawable - log.d("MyTargetGif onDestroy glide_drawable=%s", drawable) - super.onDestroy() - } - } - - override fun onSizeChanged(w : Int, h : Int, oldw : Int, oldh : Int) { - super.onSizeChanged(w, h, oldw, oldh) - post(proc_load_image) - post(proc_focus_point) - } - - override fun onLayout(changed : Boolean, left : Int, top : Int, right : Int, bottom : Int) { - super.onLayout(changed, left, top, right, bottom) - post(proc_load_image) - } - - override fun onDetachedFromWindow() { - cancelLoading(null) - super.onDetachedFromWindow() - } - - override fun onAttachedToWindow() { - super.onAttachedToWindow() - loadImageIfNecessary() - } - - override fun drawableStateChanged() { - super.drawableStateChanged() - invalidate() - } - - override fun onVisibilityChanged(changedView : View, visibility : Int) { - super.onVisibilityChanged(changedView, visibility) - loadImageIfNecessary() - } - - fun setMediaType(drawable_id : Int) { - if(drawable_id == 0) { - media_type_drawable = null - } else { - media_type_drawable = ContextCompat.getDrawable(context, drawable_id)?.mutate() - // DisplayMetrics dm = getResources().getDisplayMetrics(); - media_type_bottom = 0 - media_type_left = 0 - } - invalidate() - } - - override fun onDraw(canvas : Canvas) { - - // bitmapがrecycledされた場合に例外をキャッチする - try { - super.onDraw(canvas) - } catch(ex : Throwable) { - log.trace(ex) - } - - // media type の描画 - val media_type_drawable = this.media_type_drawable - if(media_type_drawable != null) { - val drawable_w = media_type_drawable.intrinsicWidth - val drawable_h = media_type_drawable.intrinsicHeight - // int view_w = getWidth(); - val view_h = height - media_type_drawable.setBounds( - 0, - view_h - drawable_h, - drawable_w, - view_h - ) - media_type_drawable.draw(canvas) - } - } - - ///////////////////////////////////////////////////////////////////// - - // プロフ表示の背景画像のレイアウト崩れの対策 - var measureProfileBg = false - - override fun onMeasure(widthMeasureSpec : Int, heightMeasureSpec : Int) { - if(measureProfileBg) { - // このモードではコンテンツを一切見ずにサイズを決める - val w_size = MeasureSpec.getSize(widthMeasureSpec) - val w_measured = when(MeasureSpec.getMode(widthMeasureSpec)) { - MeasureSpec.EXACTLY -> w_size - MeasureSpec.AT_MOST -> w_size - MeasureSpec.UNSPECIFIED -> 0 - else -> 0 - } - val h_size = MeasureSpec.getSize(heightMeasureSpec) - val h_measured = when(MeasureSpec.getMode(heightMeasureSpec)) { - MeasureSpec.EXACTLY -> h_size - MeasureSpec.AT_MOST -> h_size - MeasureSpec.UNSPECIFIED -> 0 - else -> 0 - } - setMeasuredDimension(w_measured, h_measured) - } else { - // 通常のImageViewは内容を見てサイズを決める - // たとえLayputParamがw,hともmatchParentでも内容を見てしまう - super.onMeasure(widthMeasureSpec, heightMeasureSpec) - } - } - - ///////////////////////////////////////////////////////////////////// - - private var focusX : Float = 0f - private var focusY : Float = 0f - - fun setFocusPoint(focusX : Float, focusY : Float) { - // フォーカスポイントは上がプラスで下がマイナス - // https://github.com/jonom/jquery-focuspoint#1-calculate-your-images-focus-point - // このタイミングで正規化してしまう - - this.focusX = clipRange(- 1f, 1f, focusX) - this.focusY = - clipRange(- 1f, 1f, focusY) - } - - override fun setImageBitmap(bm : Bitmap?) { - super.setImageBitmap(bm) - updateFocusPoint() - } - - override fun setImageDrawable(drawable : Drawable?) { - super.setImageDrawable(drawable) - updateFocusPoint() - } - - private fun updateFocusPoint() { - - // ビューのサイズが0より大きい - val view_w = width.toFloat() - val view_h = height.toFloat() - if(view_w <= 0f || view_h <= 0f) return - - // 画像のサイズが0より大きい - val drawable = this.drawable ?: return - val drawable_w = drawable.intrinsicWidth.toFloat() - val drawable_h = drawable.intrinsicHeight.toFloat() - if(drawable_w <= 0f || drawable_h <= 0f) return - - when(scaleType) { - ScaleType.CENTER_CROP, ScaleType.MATRIX -> { - val view_aspect = view_w / view_h - val drawable_aspect = drawable_w / drawable_h - - if(drawable_aspect >= view_aspect) { - // ビューより画像の方が横長 - val focus_x = this.focusX - if(focus_x == 0f) { - scaleType = ScaleType.CENTER_CROP - } else { - val matrix = Matrix() - val scale = view_h / drawable_h - val delta = focus_x * ((drawable_w * scale) - view_w) - log.d("updateFocusPoint x delta=$delta") - matrix.postTranslate(drawable_w / - 2f, drawable_h / - 2f) - matrix.postScale(scale, scale) - matrix.postTranslate((view_w - delta) / 2f, view_h / 2f) - scaleType = ScaleType.MATRIX - imageMatrix = matrix - } - } else { - // ビューより画像の方が縦長 - val focus_y = this.focusY - if(focus_y == 0f) { - scaleType = ScaleType.CENTER_CROP - } else { - val matrix = Matrix() - val scale = view_w / drawable_w - val delta = focus_y * ((drawable_h * scale) - view_h) - matrix.postTranslate(drawable_w / - 2f, drawable_h / - 2f) - matrix.postScale(scale, scale) - matrix.postTranslate(view_w / 2f, (view_h - delta) / 2f) - scaleType = ScaleType.MATRIX - imageMatrix = matrix - } - } - } - - else -> { - // not supported. - } - } - } - - fun setScaleTypeForMedia() { - when(scaleType) { - ScaleType.CENTER_CROP, ScaleType.MATRIX -> { - // nothing to do - } - - else -> { - scaleType = ScaleType.CENTER_CROP - } - } - } - + + companion object { + + internal val log = LogCategory("MyNetworkImageView") + } + + // ロード中などに表示するDrawableのリソースID + private var mDefaultImage: Drawable? = null + + // エラー時に表示するDrawableのリソースID + private var mErrorImage: Drawable? = null + + // 角丸の半径。元画像の短辺に対する割合を指定するらしい + internal var mCornerRadius = 0f + + // 表示したい画像のURL + private var mUrl: String? = null + private var mMayGif: Boolean = false + + // 非同期処理のキャンセル + private var mTarget: Target<*>? = null + + private val proc_load_image: Runnable = Runnable { loadImageIfNecessary() } + private val proc_focus_point: Runnable = Runnable { updateFocusPoint() } + + private var media_type_drawable: Drawable? = null + private var media_type_bottom = 0 + private var media_type_left = 0 + + constructor(context: Context) + : super(context) + + constructor(context: Context, attrs: AttributeSet) + : super(context, attrs) + + constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) + : super(context, attrs, defStyleAttr) + + fun setDefaultImage(defaultImage: Drawable?) { + mDefaultImage = defaultImage + loadImageIfNecessary() + } + + fun setErrorImage(errorImage: Drawable?) { + mErrorImage = errorImage + loadImageIfNecessary() + } + + fun setImageUrl( + pref: SharedPreferences, + r: Float, + url: String?, + gifUrlArg: String? = null + ) { + + mCornerRadius = r + + val gif_url = if (Pref.bpEnableGifAnimation(pref)) gifUrlArg else null + + if (gif_url?.isNotEmpty() == true) { + mUrl = gif_url + mMayGif = true + } else { + mUrl = url + mMayGif = false + } + loadImageIfNecessary() + } + + private fun getGlide(): RequestManager? { + try { + return Glide.with(context) + } catch (ex: IllegalArgumentException) { + if (ex.message?.contains("destroyed activity") == true) { + // ignore it + } else { + log.e(ex, "Glide.with() failed.") + } + } catch (ex: Throwable) { + log.e(ex, "Glide.with() failed.") + } + return null + } + + fun cancelLoading(defaultDrawable: Drawable? = null) { + + val d = drawable + if (d is Animatable) { + if (d.isRunning) { + //warning.d("cancelLoading: Animatable.stop()") + d.stop() + } + } + + setImageDrawable(defaultDrawable) + + val target = mTarget + if (target != null) { + try { + getGlide()?.clear(target) + } catch (ex: Throwable) { + log.e(ex, "Glide.clear() failed.") + } + + mTarget = null + } + + } + + // 必要なら非同期処理を開始する + private fun loadImageIfNecessary() { + try { + val url = mUrl + if (url?.isEmpty() != false) { + // if the URL to be loaded in this view is empty, + // cancel any old requests and clear the currently loaded image. + cancelLoading(mDefaultImage) + return + } + + // すでにリクエストが発行済みで、リクエストされたURLが同じなら何もしない + if ((mTarget as? UrlTarget)?.urlLoading == url) return + + // if there is a pre-existing request, cancel it if it's fetching a different URL. + cancelLoading(mDefaultImage) + + // 非表示状態ならロードを延期する + if (!isShown) return + + var wrapWidth = false + var wrapHeight = false + val lp = layoutParams + if (lp != null) { + wrapWidth = lp.width == ViewGroup.LayoutParams.WRAP_CONTENT + wrapHeight = lp.height == ViewGroup.LayoutParams.WRAP_CONTENT + } + + // Calculate the max image width / height to use while ignoring WRAP_CONTENT dimens. + val desiredWidth = if (wrapWidth) Target.SIZE_ORIGINAL else width + val desiredHeight = if (wrapHeight) Target.SIZE_ORIGINAL else height + + if (desiredWidth != Target.SIZE_ORIGINAL && desiredWidth <= 0 + || desiredHeight != Target.SIZE_ORIGINAL && desiredHeight <= 0 + ) { + // desiredWidth,desiredHeight の指定がおかしいと非同期処理中にSimpleTargetが落ちる + // おそらくレイアウト後に再度呼び出される + return + } + + val glideHeaders = LazyHeaders.Builder() + .addHeader("Accept", "image/webp,image/*,*/*;q=0.8") + .build() + + val glideUrl = GlideUrl(url, glideHeaders) + + mTarget = if (mMayGif) { + getGlide() + ?.load(glideUrl) + ?.into(MyTargetGif(url)) + } else { + getGlide() + ?.load(glideUrl) + ?.into(MyTarget(url)) + } + } catch (ex: Throwable) { + log.trace(ex) + } + } + + private fun replaceGifDrawable(resource: GifDrawable): Drawable { + // ディスクキャッシュから読んだ画像は角丸が正しく扱われない + // MyGifDrawable に差し替えて描画させる + try { + return MyGifDrawable(resource, mCornerRadius) + } catch (ex: Throwable) { + log.trace(ex) + } + return resource + } + + private fun replaceBitmapDrawable(resource: BitmapDrawable): Drawable { + try { + val bitmap = resource.bitmap + if (bitmap != null) return replaceBitmapDrawable(bitmap) + } catch (ex: Throwable) { + log.trace(ex) + } + return resource + } + + private fun replaceBitmapDrawable(bitmap: Bitmap): Drawable { + val d = RoundedBitmapDrawableFactory.create(resources, bitmap) + d.cornerRadius = mCornerRadius + return d + } + + private fun onLoadFailed(urlLoading: String) { + try { + // 別の画像を表示するよう指定が変化していたなら何もしない + if (urlLoading != mUrl) return + + // エラー表示用の画像リソースが指定されていたら使う + when (val drawable = mErrorImage) { + null -> { + // このタイミングでImageViewのDrawableを変更するとチラつきの元になるので何もしない + } + + else -> setImageDrawable(drawable) + } + } catch (ex: Throwable) { + log.trace(ex) + } + } + + private interface UrlTarget { + + val urlLoading: String + } + + // 静止画用のターゲット + private inner class MyTarget( + override val urlLoading: String + ) : ImageViewTarget(this@MyNetworkImageView), UrlTarget { + + // errorDrawable The error drawable to optionally show, or null. + override fun onLoadFailed(errorDrawable: Drawable?) { + onLoadFailed(urlLoading) + } + + override fun setResource(resource: Drawable?) { + try { + // 別の画像を表示するよう指定が変化していたなら何もしない + if (urlLoading != mUrl) return + + if (mCornerRadius > 0f) { + if (resource is BitmapDrawable) { + // BitmapDrawableは角丸処理が可能。 + setImageDrawable(replaceBitmapDrawable(resource.bitmap)) + return + } + // その他のDrawable + // たとえばInstanceTickerのアイコンにSVGが使われていたらPictureDrawableになる + log.w("cornerRadius=$mCornerRadius,drawable=$resource,url=$urlLoading") + } + + setImageDrawable(resource) + return + + } catch (ex: Throwable) { + log.trace(ex) + } + } + + } + + private inner class MyTargetGif( + override val urlLoading: String + ) : ImageViewTarget(this@MyNetworkImageView), UrlTarget { + + private var glide_drawable: Drawable? = null + + override fun onLoadFailed(errorDrawable: Drawable?) = onLoadFailed(urlLoading) + + override fun onResourceReady( + drawable: Drawable, + transition: Transition? + ) { + try { + // 別の画像を表示するよう指定が変化していたなら何もしない + if (urlLoading != mUrl) return + + afterResourceReady( + transition, + when { + mCornerRadius <= 0f -> { + // 角丸でないならそのまま使う + drawable + } + + // GidDrawableを置き換える + drawable is GifDrawable -> replaceGifDrawable(drawable) + + // Glide 4.xから、静止画はBitmapDrawableになった + drawable is BitmapDrawable -> replaceBitmapDrawable(drawable) + + else -> { + log.d("onResourceReady: drawable class=${drawable.javaClass.simpleName}") + drawable + } + } + ) + } catch (ex: Throwable) { + log.trace(ex) + } + } + + private fun afterResourceReady(transition: Transition?, drawable: Drawable) { + super.onResourceReady(drawable, transition) + + // if( ! drawable.isAnimated() ){ + // //XXX: Try to generalize this to other sizes/shapes. + // // This is a dirty hack that tries to make loading square thumbnails and then square full images less costly + // // by forcing both the smaller thumb and the larger version to have exactly the same intrinsic dimensions. + // // If a drawable is replaced in an ImageView by another drawable with different intrinsic dimensions, + // // the ImageView requests a layout. Scrolling rapidly while replacing thumbs with larger images triggers + // // lots of these calls and causes significant amounts of junk. + // float viewRatio = view.getWidth() / (float) view.getHeight(); + // float drawableRatio = drawable.getIntrinsicWidth() / (float) drawable.getIntrinsicHeight(); + // if( Math.abs( viewRatio - 1f ) <= SQUARE_RATIO_MARGIN + // && Math.abs( drawableRatio - 1f ) <= SQUARE_RATIO_MARGIN ){ + // drawable = new SquaringDrawable( drawable, view.getWidth() ); + // } + // } + + this.glide_drawable = drawable + if (drawable is GifDrawable) { + drawable.setLoopCount(GifDrawable.LOOP_FOREVER) + drawable.start() + } else if (drawable is MyGifDrawable) { + drawable.setLoopCount(GifDrawable.LOOP_FOREVER) + drawable.start() + } + } + + // super.onResourceReady から呼ばれる + override fun setResource(drawable: Drawable?) { + setImageDrawable(drawable) + } + + override fun onStart() { + val drawable = glide_drawable + if (drawable is Animatable && !drawable.isRunning) { + log.d("MyTargetGif onStart glide_drawable=${drawable}") + drawable.start() + } + } + + override fun onStop() { + val drawable = glide_drawable + if (drawable is Animatable && drawable.isRunning) { + log.d("MyTargetGif onStop glide_drawable=${drawable}") + drawable.stop() + } + } + + override fun onDestroy() { + val drawable = glide_drawable + log.d("MyTargetGif onDestroy glide_drawable=${drawable}") + super.onDestroy() + } + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + post(proc_load_image) + post(proc_focus_point) + } + + override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { + super.onLayout(changed, left, top, right, bottom) + post(proc_load_image) + } + + override fun onDetachedFromWindow() { + cancelLoading(null) + super.onDetachedFromWindow() + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + loadImageIfNecessary() + } + + override fun drawableStateChanged() { + super.drawableStateChanged() + invalidate() + } + + override fun onVisibilityChanged(changedView: View, visibility: Int) { + super.onVisibilityChanged(changedView, visibility) + loadImageIfNecessary() + } + + fun setMediaType(drawable_id: Int) { + if (drawable_id == 0) { + media_type_drawable = null + } else { + media_type_drawable = ContextCompat.getDrawable(context, drawable_id)?.mutate() + // DisplayMetrics dm = getResources().getDisplayMetrics(); + media_type_bottom = 0 + media_type_left = 0 + } + invalidate() + } + + override fun onDraw(canvas: Canvas) { + + // bitmapがrecycledされた場合に例外をキャッチする + try { + super.onDraw(canvas) + } catch (ex: Throwable) { + log.trace(ex) + } + + // media type の描画 + val media_type_drawable = this.media_type_drawable + if (media_type_drawable != null) { + val drawable_w = media_type_drawable.intrinsicWidth + val drawable_h = media_type_drawable.intrinsicHeight + // int view_w = getWidth(); + val view_h = height + media_type_drawable.setBounds( + 0, + view_h - drawable_h, + drawable_w, + view_h + ) + media_type_drawable.draw(canvas) + } + } + + ///////////////////////////////////////////////////////////////////// + + // プロフ表示の背景画像のレイアウト崩れの対策 + var measureProfileBg = false + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + if (measureProfileBg) { + // このモードではコンテンツを一切見ずにサイズを決める + val w_size = MeasureSpec.getSize(widthMeasureSpec) + val w_measured = when (MeasureSpec.getMode(widthMeasureSpec)) { + MeasureSpec.EXACTLY -> w_size + MeasureSpec.AT_MOST -> w_size + MeasureSpec.UNSPECIFIED -> 0 + else -> 0 + } + val h_size = MeasureSpec.getSize(heightMeasureSpec) + val h_measured = when (MeasureSpec.getMode(heightMeasureSpec)) { + MeasureSpec.EXACTLY -> h_size + MeasureSpec.AT_MOST -> h_size + MeasureSpec.UNSPECIFIED -> 0 + else -> 0 + } + setMeasuredDimension(w_measured, h_measured) + } else { + // 通常のImageViewは内容を見てサイズを決める + // たとえLayoutParamがw,hともmatchParentでも内容を見てしまう + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + } + } + + ///////////////////////////////////////////////////////////////////// + + private var focusX: Float = 0f + private var focusY: Float = 0f + + fun setFocusPoint(focusX: Float, focusY: Float) { + // フォーカスポイントは上がプラスで下がマイナス + // https://github.com/jonom/jquery-focuspoint#1-calculate-your-images-focus-point + // このタイミングで正規化してしまう + + this.focusX = clipRange(-1f, 1f, focusX) + this.focusY = -clipRange(-1f, 1f, focusY) + } + + override fun setImageBitmap(bm: Bitmap?) { + super.setImageBitmap(bm) + updateFocusPoint() + } + + override fun setImageDrawable(drawable: Drawable?) { + super.setImageDrawable(drawable) + updateFocusPoint() + } + + private fun updateFocusPoint() { + + // ビューのサイズが0より大きい + val view_w = width.toFloat() + val view_h = height.toFloat() + if (view_w <= 0f || view_h <= 0f) return + + // 画像のサイズが0より大きい + val drawable = this.drawable ?: return + val drawable_w = drawable.intrinsicWidth.toFloat() + val drawable_h = drawable.intrinsicHeight.toFloat() + if (drawable_w <= 0f || drawable_h <= 0f) return + + when (scaleType) { + ScaleType.CENTER_CROP, ScaleType.MATRIX -> { + val view_aspect = view_w / view_h + val drawable_aspect = drawable_w / drawable_h + + if (drawable_aspect >= view_aspect) { + // ビューより画像の方が横長 + val focus_x = this.focusX + if (focus_x == 0f) { + scaleType = ScaleType.CENTER_CROP + } else { + val matrix = Matrix() + val scale = view_h / drawable_h + val delta = focus_x * ((drawable_w * scale) - view_w) + log.d("updateFocusPoint x delta=$delta") + matrix.postTranslate(drawable_w / -2f, drawable_h / -2f) + matrix.postScale(scale, scale) + matrix.postTranslate((view_w - delta) / 2f, view_h / 2f) + scaleType = ScaleType.MATRIX + imageMatrix = matrix + } + } else { + // ビューより画像の方が縦長 + val focus_y = this.focusY + if (focus_y == 0f) { + scaleType = ScaleType.CENTER_CROP + } else { + val matrix = Matrix() + val scale = view_w / drawable_w + val delta = focus_y * ((drawable_h * scale) - view_h) + matrix.postTranslate(drawable_w / -2f, drawable_h / -2f) + matrix.postScale(scale, scale) + matrix.postTranslate(view_w / 2f, (view_h - delta) / 2f) + scaleType = ScaleType.MATRIX + imageMatrix = matrix + } + } + } + + else -> { + // not supported. + } + } + } + + fun setScaleTypeForMedia() { + when (scaleType) { + ScaleType.CENTER_CROP, ScaleType.MATRIX -> { + // nothing to do + } + + else -> { + scaleType = ScaleType.CENTER_CROP + } + } + } + } diff --git a/app/src/main/java/jp/juggler/subwaytooter/view/PinchBitmapView.kt b/app/src/main/java/jp/juggler/subwaytooter/view/PinchBitmapView.kt index 41966ba3..9245a62f 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/view/PinchBitmapView.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/view/PinchBitmapView.kt @@ -17,490 +17,491 @@ import kotlin.math.abs import kotlin.math.max import kotlin.math.sqrt -class PinchBitmapView(context : Context, attrs : AttributeSet?, defStyle : Int) : - View(context, attrs, defStyle) { - - companion object { - - internal val log = LogCategory("PinchImageView") - - // 数値を範囲内にクリップする - private fun clip(min : Float, max : Float, v : Float) : Float { - return if(v < min) min else if(v > max) max else v - } - - // ビューの幅と画像の描画サイズを元に描画位置をクリップする - private fun clipTranslate( - view_w : Float // ビューの幅 - , bitmap_w : Float // 画像の幅 - , current_scale : Float // 画像の拡大率 - , trans_x : Float // タッチ操作による表示位置 - ) : Float { - - // 余白(拡大率が小さい場合はプラス、拡大率が大きい場合はマイナス) - val padding = view_w - bitmap_w * current_scale - - // 余白が>=0なら画像を中心に表示する。 <0なら操作された位置をクリップする。 - return if(padding >= 0f) padding / 2f else clip(padding, 0f, trans_x) - } - } - - private var callback : Callback? = null - - private var bitmap : Bitmap? = null - private var bitmap_w : Float = 0.toFloat() - private var bitmap_h : Float = 0.toFloat() - private var bitmap_aspect : Float = 0.toFloat() - - // 画像を表示する位置と拡大率 - private var current_trans_x : Float = 0.toFloat() - private var current_trans_y : Float = 0.toFloat() - private var current_scale : Float = 0.toFloat() - - // 画像表示に使う構造体 - private val drawMatrix = Matrix() - internal val paint = Paint() - - // タッチ操作中に指を動かした - private var bDrag : Boolean = false - - // タッチ操作中に指の数を変えた - private var bPointerCountChanged : Boolean = false - - // ページめくりに必要なスワイプ強度 - private var swipe_velocity = 0f - private var swipe_velocity2 = 0f - - // 指を動かしたと判断する距離 - private var drag_length = 0f - - private var time_touch_start = 0L - - // フリック操作の検出に使う - private var velocityTracker : VelocityTracker? = null - - private var click_time = 0L - private var click_count = 0 - - // 移動後の指の位置 - internal val pos = PointerAvg() - - // 移動開始時の指の位置 - private val start_pos = PointerAvg() - - // 移動開始時の画像の位置 - private var start_image_trans_x : Float = 0.toFloat() - private var start_image_trans_y : Float = 0.toFloat() - private var start_image_scale : Float = 0.toFloat() - - private var scale_min : Float = 0.toFloat() - private var scale_max : Float = 0.toFloat() - - private var view_w : Float = 0.toFloat() - private var view_h : Float = 0.toFloat() - private var view_aspect : Float = 0.toFloat() - - private val tracking_matrix = Matrix() - private val tracking_matrix_inv = Matrix() - private val avg_on_image1 = FloatArray(2) - private val avg_on_image2 = FloatArray(2) - - constructor(context : Context) : this(context, null) { - init(context) - } - - constructor(context : Context, attrs : AttributeSet?) : this(context, attrs, 0) { - init(context) - } - - init { - init(context) - } - - internal fun init(context : Context) { - - // 定数をdpからpxに変換 - val density = context.resources.displayMetrics.density - swipe_velocity = 1000f * density - swipe_velocity2 = 250f * density - drag_length = 4f * density // 誤反応しがちなのでやや厳しめ - } - - // ページめくり操作のコールバック - interface Callback { - - fun onSwipe(deltaX : Int, deltaY : Int) - - fun onMove(bitmap_w : Float, bitmap_h : Float, tx : Float, ty : Float, scale : Float) - } - - fun setCallback(callback : Callback?) { - this.callback = callback - } - - fun setBitmap(b : Bitmap?) { - - bitmap?.recycle() - - this.bitmap = b - - initializeScale() - } - - override fun onDraw(canvas : Canvas) { - super.onDraw(canvas) - - val bitmap = this.bitmap - if(bitmap != null && ! bitmap.isRecycled) { - - drawMatrix.reset() - drawMatrix.postScale(current_scale, current_scale) - drawMatrix.postTranslate(current_trans_x, current_trans_y) - - paint.isFilterBitmap = current_scale < 4f - canvas.drawBitmap(bitmap, drawMatrix, paint) - } - } - - override fun onSizeChanged(w : Int, h : Int, oldw : Int, oldh : Int) { - super.onSizeChanged(w, h, oldw, oldh) - - view_w = max(1f, w.toFloat()) - view_h = max(1f, h.toFloat()) - view_aspect = view_w / view_h - - initializeScale() - } - - override fun performClick() : Boolean { - super.performClick() - - initializeScale() - - return true - } - - private var defaultScale : Float = 1f - - // 表示位置の初期化 - // 呼ばれるのは、ビットマップを変更した時、ビューのサイズが変わった時、画像をクリックした時 - private fun initializeScale() { - val bitmap = this.bitmap - if(bitmap != null && ! bitmap.isRecycled && view_w >= 1f) { - - bitmap_w = max(1f, bitmap.width.toFloat()) - bitmap_h = max(1f, bitmap.height.toFloat()) - bitmap_aspect = bitmap_w / bitmap_h - - if(view_aspect > bitmap_aspect) { - scale_min = view_h / bitmap_h / 2f - scale_max = view_w / bitmap_w * 8f - } else { - scale_min = view_w / bitmap_w / 2f - scale_max = view_h / bitmap_h * 8f - } - if(scale_max < scale_min) scale_max = scale_min * 16f - - defaultScale = if(view_aspect > bitmap_aspect) { - view_h / bitmap_h - } else { - view_w / bitmap_w - } - - val draw_w = bitmap_w * defaultScale - val draw_h = bitmap_h * defaultScale - - current_scale = defaultScale - current_trans_x = (view_w - draw_w) / 2f - current_trans_y = (view_h - draw_h) / 2f - - callback?.onMove(bitmap_w, bitmap_h, current_trans_x, current_trans_y, current_scale) - } else { - defaultScale = 1f - scale_min = 1f - scale_max = 1f - - current_scale = defaultScale - current_trans_y = 0f - current_trans_x = 0f - - callback?.onMove(0f, 0f, current_trans_x, current_trans_y, current_scale) - } - - // 画像がnullに変化した時も再描画が必要 - invalidate() - } - - @SuppressLint("ClickableViewAccessibility") - override fun onTouchEvent(ev : MotionEvent) : Boolean { - - val bitmap = this.bitmap - if(bitmap == null - || bitmap.isRecycled - || view_w < 1f) - return false - - val action = ev.action - - if(action == MotionEvent.ACTION_DOWN) { - time_touch_start = SystemClock.elapsedRealtime() - - velocityTracker?.clear() - velocityTracker = VelocityTracker.obtain() - velocityTracker?.addMovement(ev) - - bPointerCountChanged = false - bDrag = bPointerCountChanged - trackStart(ev) - return true - } - - velocityTracker?.addMovement(ev) - - when(action) { - MotionEvent.ACTION_POINTER_DOWN, MotionEvent.ACTION_POINTER_UP -> { - // タッチ操作中に指の数を変えた - bPointerCountChanged = true - bDrag = bPointerCountChanged - trackStart(ev) - } - - MotionEvent.ACTION_MOVE -> trackNext(ev) - - MotionEvent.ACTION_UP -> { - trackNext(ev) - - checkClickOrPaging() - - velocityTracker?.recycle() - velocityTracker = null - } - } - return true - } - - private fun checkClickOrPaging() { - - if(! bDrag) { - // 指を動かしていないなら - - val now = SystemClock.elapsedRealtime() - - if(now - time_touch_start >= 1000L) { - // ロングタップはタップカウントをリセットする - log.d("click count reset by long tap") - click_count = 0 - return - } - - val delta = now - click_time - click_time = now - - if(delta > 334L) { - // 前回のタップからの時刻が長いとタップカウントをリセットする - log.d("click count reset by long interval") - click_count = 0 - } - - ++ click_count - - log.d("click %d %d", click_count, delta) - - if(click_count >= 2) { - // ダブルタップでクリック操作 - click_count = 0 - performClick() - } - - return - } - - click_count = 0 - - val velocityTracker = this.velocityTracker - if(! bPointerCountChanged && velocityTracker != null) { - - // 指の数を変えていないならページめくり操作かもしれない - - // 「画像を動かした」かどうかのチェック - val image_moved = max( - abs(current_trans_x - start_image_trans_x), - abs(current_trans_y - start_image_trans_y) - ) - if(image_moved >= drag_length) { - log.d("image moved. not flick action. $image_moved") - return - } - - velocityTracker.computeCurrentVelocity(1000) - val vx = velocityTracker.xVelocity - val vy = velocityTracker.yVelocity - val avx = abs(vx) - val avy = abs(vy) - val velocity = sqrt(vx * vx + vy * vy) - val aspect = try { - avx / avy - } catch(ex : Throwable) { - Float.MAX_VALUE - } - - when { - aspect >= 0.9f -> { - // 指を動かした方向が左右だった - - val vMin = when { - current_scale * bitmap_w <= view_w -> swipe_velocity2 - else -> swipe_velocity - } - - if(velocity < vMin) { - log.d("velocity $velocity not enough to pagingX") - return - } - - log.d("pagingX! m=$image_moved a=$aspect v=$velocity") - runOnMainLooper { callback?.onSwipe(if(vx >= 0f) - 1 else 1, 0) } - } - - aspect <= 0.333f -> { - // 指を動かした方向が上下だった - - val vMin = when { - current_scale * bitmap_h <= view_h -> swipe_velocity2 - else -> swipe_velocity - } - - if(velocity < vMin) { - log.d("velocity $velocity not enough to pagingY") - return - } - - log.d("pagingY! m=$image_moved a=$aspect v=$velocity") - runOnMainLooper { callback?.onSwipe(0, if(vy >= 0f) - 1 else 1) } - } - - else -> log.d("flick is not horizontal/vertical. aspect=$aspect") - } - } - } - - // マルチタッチの中心位置の計算 - internal class PointerAvg { - - // タッチ位置の数 - var count : Int = 0 - - // タッチ位置の平均 - val avg = FloatArray(2) - - // 中心と、中心から最も離れたタッチ位置の間の距離 - var max_radius : Float = 0.toFloat() - - fun update(ev : MotionEvent) { - - count = ev.pointerCount - if(count <= 1) { - avg[0] = ev.x - avg[1] = ev.y - max_radius = 0f - - } else { - avg[0] = 0f - avg[1] = 0f - for(i in 0 until count) { - avg[0] += ev.getX(i) - avg[1] += ev.getY(i) - } - avg[0] /= count.toFloat() - avg[1] /= count.toFloat() - max_radius = 0f - for(i in 0 until count) { - val dx = ev.getX(i) - avg[0] - val dy = ev.getY(i) - avg[1] - val radius = dx * dx + dy * dy - if(radius > max_radius) max_radius = radius - } - max_radius = sqrt(max_radius.toDouble()).toFloat() - if(max_radius < 1f) max_radius = 1f - } - } - } - - private fun trackStart(ev : MotionEvent) { - - // 追跡開始時の指の位置 - start_pos.update(ev) - - // 追跡開始時の画像の位置 - start_image_trans_x = current_trans_x - start_image_trans_y = current_trans_y - start_image_scale = current_scale - - } - - // 画面上の指の位置から画像中の指の位置を調べる - private fun getCoordinateOnImage(dst : FloatArray, src : FloatArray) { - tracking_matrix.reset() - tracking_matrix.postScale(current_scale, current_scale) - tracking_matrix.postTranslate(current_trans_x, current_trans_y) - tracking_matrix.invert(tracking_matrix_inv) - tracking_matrix_inv.mapPoints(dst, src) - } - - private fun trackNext(ev : MotionEvent) { - pos.update(ev) - - if(pos.count != start_pos.count) { - // タッチ操作中に指の数が変わった - log.d("nextTracking: pointer count changed") - bPointerCountChanged = true - bDrag = bPointerCountChanged - trackStart(ev) - return - } - - // ズーム操作 - if(pos.count > 1) { - - // タッチ位置にある絵柄の座標を調べる - getCoordinateOnImage(avg_on_image1, pos.avg) - - // ズーム率を変更する - current_scale = clip( - scale_min, - scale_max, - start_image_scale * pos.max_radius / start_pos.max_radius - ) - - // 再び調べる - getCoordinateOnImage(avg_on_image2, pos.avg) - - // ズーム変更の前後で位置がズレた分だけ移動させると、タッチ位置にある絵柄がズレない - start_image_trans_x += current_scale * (avg_on_image2[0] - avg_on_image1[0]) - start_image_trans_y += current_scale * (avg_on_image2[1] - avg_on_image1[1]) - - } - - // 平行移動 - run { - // start時から指を動かした量 - val move_x = pos.avg[0] - start_pos.avg[0] - val move_y = pos.avg[1] - start_pos.avg[1] - - // 「指を動かした」と判断したらフラグを立てる - if(abs(move_x) >= drag_length || abs(move_y) >= drag_length) { - bDrag = true - } - - // 画像の表示位置を更新 - current_trans_x = - clipTranslate(view_w, bitmap_w, current_scale, start_image_trans_x + move_x) - current_trans_y = - clipTranslate(view_h, bitmap_h, current_scale, start_image_trans_y + move_y) - } - - callback?.onMove(bitmap_w, bitmap_h, current_trans_x, current_trans_y, current_scale) - invalidate() - } - +class PinchBitmapView(context: Context, attrs: AttributeSet?, defStyle: Int) : + View(context, attrs, defStyle) { + + companion object { + + internal val log = LogCategory("PinchImageView") + + // 数値を範囲内にクリップする + private fun clip(min: Float, max: Float, v: Float): Float { + return if (v < min) min else if (v > max) max else v + } + + // ビューの幅と画像の描画サイズを元に描画位置をクリップする + private fun clipTranslate( + view_w: Float // ビューの幅 + , bitmap_w: Float // 画像の幅 + , current_scale: Float // 画像の拡大率 + , trans_x: Float // タッチ操作による表示位置 + ): Float { + + // 余白(拡大率が小さい場合はプラス、拡大率が大きい場合はマイナス) + val padding = view_w - bitmap_w * current_scale + + // 余白が>=0なら画像を中心に表示する。 <0なら操作された位置をクリップする。 + return if (padding >= 0f) padding / 2f else clip(padding, 0f, trans_x) + } + } + + private var callback: Callback? = null + + private var bitmap: Bitmap? = null + private var bitmap_w: Float = 0.toFloat() + private var bitmap_h: Float = 0.toFloat() + private var bitmap_aspect: Float = 0.toFloat() + + // 画像を表示する位置と拡大率 + private var current_trans_x: Float = 0.toFloat() + private var current_trans_y: Float = 0.toFloat() + private var current_scale: Float = 0.toFloat() + + // 画像表示に使う構造体 + private val drawMatrix = Matrix() + internal val paint = Paint() + + // タッチ操作中に指を動かした + private var bDrag: Boolean = false + + // タッチ操作中に指の数を変えた + private var bPointerCountChanged: Boolean = false + + // ページめくりに必要なスワイプ強度 + private var swipe_velocity = 0f + private var swipe_velocity2 = 0f + + // 指を動かしたと判断する距離 + private var drag_length = 0f + + private var time_touch_start = 0L + + // フリック操作の検出に使う + private var velocityTracker: VelocityTracker? = null + + private var click_time = 0L + private var click_count = 0 + + // 移動後の指の位置 + internal val pos = PointerAvg() + + // 移動開始時の指の位置 + private val start_pos = PointerAvg() + + // 移動開始時の画像の位置 + private var start_image_trans_x: Float = 0.toFloat() + private var start_image_trans_y: Float = 0.toFloat() + private var start_image_scale: Float = 0.toFloat() + + private var scale_min: Float = 0.toFloat() + private var scale_max: Float = 0.toFloat() + + private var view_w: Float = 0.toFloat() + private var view_h: Float = 0.toFloat() + private var view_aspect: Float = 0.toFloat() + + private val tracking_matrix = Matrix() + private val tracking_matrix_inv = Matrix() + private val avg_on_image1 = FloatArray(2) + private val avg_on_image2 = FloatArray(2) + + constructor(context: Context) : this(context, null) { + init(context) + } + + constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) { + init(context) + } + + init { + init(context) + } + + internal fun init(context: Context) { + + // 定数をdpからpxに変換 + val density = context.resources.displayMetrics.density + swipe_velocity = 1000f * density + swipe_velocity2 = 250f * density + drag_length = 4f * density // 誤反応しがちなのでやや厳しめ + } + + // ページめくり操作のコールバック + interface Callback { + + fun onSwipe(deltaX: Int, deltaY: Int) + + fun onMove(bitmap_w: Float, bitmap_h: Float, tx: Float, ty: Float, scale: Float) + } + + fun setCallback(callback: Callback?) { + this.callback = callback + } + + fun setBitmap(b: Bitmap?) { + + bitmap?.recycle() + + this.bitmap = b + + initializeScale() + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + + val bitmap = this.bitmap + if (bitmap != null && !bitmap.isRecycled) { + + drawMatrix.reset() + drawMatrix.postScale(current_scale, current_scale) + drawMatrix.postTranslate(current_trans_x, current_trans_y) + + paint.isFilterBitmap = current_scale < 4f + canvas.drawBitmap(bitmap, drawMatrix, paint) + } + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + + view_w = max(1f, w.toFloat()) + view_h = max(1f, h.toFloat()) + view_aspect = view_w / view_h + + initializeScale() + } + + override fun performClick(): Boolean { + super.performClick() + + initializeScale() + + return true + } + + private var defaultScale: Float = 1f + + // 表示位置の初期化 + // 呼ばれるのは、ビットマップを変更した時、ビューのサイズが変わった時、画像をクリックした時 + private fun initializeScale() { + val bitmap = this.bitmap + if (bitmap != null && !bitmap.isRecycled && view_w >= 1f) { + + bitmap_w = max(1f, bitmap.width.toFloat()) + bitmap_h = max(1f, bitmap.height.toFloat()) + bitmap_aspect = bitmap_w / bitmap_h + + if (view_aspect > bitmap_aspect) { + scale_min = view_h / bitmap_h / 2f + scale_max = view_w / bitmap_w * 8f + } else { + scale_min = view_w / bitmap_w / 2f + scale_max = view_h / bitmap_h * 8f + } + if (scale_max < scale_min) scale_max = scale_min * 16f + + defaultScale = if (view_aspect > bitmap_aspect) { + view_h / bitmap_h + } else { + view_w / bitmap_w + } + + val draw_w = bitmap_w * defaultScale + val draw_h = bitmap_h * defaultScale + + current_scale = defaultScale + current_trans_x = (view_w - draw_w) / 2f + current_trans_y = (view_h - draw_h) / 2f + + callback?.onMove(bitmap_w, bitmap_h, current_trans_x, current_trans_y, current_scale) + } else { + defaultScale = 1f + scale_min = 1f + scale_max = 1f + + current_scale = defaultScale + current_trans_y = 0f + current_trans_x = 0f + + callback?.onMove(0f, 0f, current_trans_x, current_trans_y, current_scale) + } + + // 画像がnullに変化した時も再描画が必要 + invalidate() + } + + @SuppressLint("ClickableViewAccessibility") + override fun onTouchEvent(ev: MotionEvent): Boolean { + + val bitmap = this.bitmap + if (bitmap == null + || bitmap.isRecycled + || view_w < 1f + ) + return false + + val action = ev.action + + if (action == MotionEvent.ACTION_DOWN) { + time_touch_start = SystemClock.elapsedRealtime() + + velocityTracker?.clear() + velocityTracker = VelocityTracker.obtain() + velocityTracker?.addMovement(ev) + + bPointerCountChanged = false + bDrag = bPointerCountChanged + trackStart(ev) + return true + } + + velocityTracker?.addMovement(ev) + + when (action) { + MotionEvent.ACTION_POINTER_DOWN, MotionEvent.ACTION_POINTER_UP -> { + // タッチ操作中に指の数を変えた + bPointerCountChanged = true + bDrag = bPointerCountChanged + trackStart(ev) + } + + MotionEvent.ACTION_MOVE -> trackNext(ev) + + MotionEvent.ACTION_UP -> { + trackNext(ev) + + checkClickOrPaging() + + velocityTracker?.recycle() + velocityTracker = null + } + } + return true + } + + private fun checkClickOrPaging() { + + if (!bDrag) { + // 指を動かしていないなら + + val now = SystemClock.elapsedRealtime() + + if (now - time_touch_start >= 1000L) { + // ロングタップはタップカウントをリセットする + log.d("click count reset by long tap") + click_count = 0 + return + } + + val delta = now - click_time + click_time = now + + if (delta > 334L) { + // 前回のタップからの時刻が長いとタップカウントをリセットする + log.d("click count reset by long interval") + click_count = 0 + } + + ++click_count + + log.d("click ${click_count} ${delta}") + + if (click_count >= 2) { + // ダブルタップでクリック操作 + click_count = 0 + performClick() + } + + return + } + + click_count = 0 + + val velocityTracker = this.velocityTracker + if (!bPointerCountChanged && velocityTracker != null) { + + // 指の数を変えていないならページめくり操作かもしれない + + // 「画像を動かした」かどうかのチェック + val image_moved = max( + abs(current_trans_x - start_image_trans_x), + abs(current_trans_y - start_image_trans_y) + ) + if (image_moved >= drag_length) { + log.d("image moved. not flick action. $image_moved") + return + } + + velocityTracker.computeCurrentVelocity(1000) + val vx = velocityTracker.xVelocity + val vy = velocityTracker.yVelocity + val avx = abs(vx) + val avy = abs(vy) + val velocity = sqrt(vx * vx + vy * vy) + val aspect = try { + avx / avy + } catch (ex: Throwable) { + Float.MAX_VALUE + } + + when { + aspect >= 0.9f -> { + // 指を動かした方向が左右だった + + val vMin = when { + current_scale * bitmap_w <= view_w -> swipe_velocity2 + else -> swipe_velocity + } + + if (velocity < vMin) { + log.d("velocity $velocity not enough to pagingX") + return + } + + log.d("pagingX! m=$image_moved a=$aspect v=$velocity") + runOnMainLooper { callback?.onSwipe(if (vx >= 0f) -1 else 1, 0) } + } + + aspect <= 0.333f -> { + // 指を動かした方向が上下だった + + val vMin = when { + current_scale * bitmap_h <= view_h -> swipe_velocity2 + else -> swipe_velocity + } + + if (velocity < vMin) { + log.d("velocity $velocity not enough to pagingY") + return + } + + log.d("pagingY! m=$image_moved a=$aspect v=$velocity") + runOnMainLooper { callback?.onSwipe(0, if (vy >= 0f) -1 else 1) } + } + + else -> log.d("flick is not horizontal/vertical. aspect=$aspect") + } + } + } + + // マルチタッチの中心位置の計算 + internal class PointerAvg { + + // タッチ位置の数 + var count: Int = 0 + + // タッチ位置の平均 + val avg = FloatArray(2) + + // 中心と、中心から最も離れたタッチ位置の間の距離 + var max_radius: Float = 0.toFloat() + + fun update(ev: MotionEvent) { + + count = ev.pointerCount + if (count <= 1) { + avg[0] = ev.x + avg[1] = ev.y + max_radius = 0f + + } else { + avg[0] = 0f + avg[1] = 0f + for (i in 0 until count) { + avg[0] += ev.getX(i) + avg[1] += ev.getY(i) + } + avg[0] /= count.toFloat() + avg[1] /= count.toFloat() + max_radius = 0f + for (i in 0 until count) { + val dx = ev.getX(i) - avg[0] + val dy = ev.getY(i) - avg[1] + val radius = dx * dx + dy * dy + if (radius > max_radius) max_radius = radius + } + max_radius = sqrt(max_radius.toDouble()).toFloat() + if (max_radius < 1f) max_radius = 1f + } + } + } + + private fun trackStart(ev: MotionEvent) { + + // 追跡開始時の指の位置 + start_pos.update(ev) + + // 追跡開始時の画像の位置 + start_image_trans_x = current_trans_x + start_image_trans_y = current_trans_y + start_image_scale = current_scale + + } + + // 画面上の指の位置から画像中の指の位置を調べる + private fun getCoordinateOnImage(dst: FloatArray, src: FloatArray) { + tracking_matrix.reset() + tracking_matrix.postScale(current_scale, current_scale) + tracking_matrix.postTranslate(current_trans_x, current_trans_y) + tracking_matrix.invert(tracking_matrix_inv) + tracking_matrix_inv.mapPoints(dst, src) + } + + private fun trackNext(ev: MotionEvent) { + pos.update(ev) + + if (pos.count != start_pos.count) { + // タッチ操作中に指の数が変わった + log.d("nextTracking: pointer count changed") + bPointerCountChanged = true + bDrag = bPointerCountChanged + trackStart(ev) + return + } + + // ズーム操作 + if (pos.count > 1) { + + // タッチ位置にある絵柄の座標を調べる + getCoordinateOnImage(avg_on_image1, pos.avg) + + // ズーム率を変更する + current_scale = clip( + scale_min, + scale_max, + start_image_scale * pos.max_radius / start_pos.max_radius + ) + + // 再び調べる + getCoordinateOnImage(avg_on_image2, pos.avg) + + // ズーム変更の前後で位置がズレた分だけ移動させると、タッチ位置にある絵柄がズレない + start_image_trans_x += current_scale * (avg_on_image2[0] - avg_on_image1[0]) + start_image_trans_y += current_scale * (avg_on_image2[1] - avg_on_image1[1]) + + } + + // 平行移動 + run { + // start時から指を動かした量 + val move_x = pos.avg[0] - start_pos.avg[0] + val move_y = pos.avg[1] - start_pos.avg[1] + + // 「指を動かした」と判断したらフラグを立てる + if (abs(move_x) >= drag_length || abs(move_y) >= drag_length) { + bDrag = true + } + + // 画像の表示位置を更新 + current_trans_x = + clipTranslate(view_w, bitmap_w, current_scale, start_image_trans_x + move_x) + current_trans_y = + clipTranslate(view_h, bitmap_h, current_scale, start_image_trans_y + move_y) + } + + callback?.onMove(bitmap_w, bitmap_h, current_trans_x, current_trans_y, current_scale) + invalidate() + } + } diff --git a/app/src/main/java/jp/juggler/util/BitmapUtils.kt b/app/src/main/java/jp/juggler/util/BitmapUtils.kt index 20325e31..0eec2a89 100644 --- a/app/src/main/java/jp/juggler/util/BitmapUtils.kt +++ b/app/src/main/java/jp/juggler/util/BitmapUtils.kt @@ -248,11 +248,7 @@ fun createResizedBitmap( val paint = Paint() paint.isFilterBitmap = true canvas.drawBitmap(sourceBitmap, matrix, paint) - log.d( - "createResizedBitmap: resized to %sx%s", - dstSizeInt.x, - dstSizeInt.y - ) + log.d("createResizedBitmap: resized to ${dstSizeInt.x}x${dstSizeInt.y}") val tmp = dst dst = null tmp diff --git a/app/src/main/java/jp/juggler/util/StorageUtils.kt b/app/src/main/java/jp/juggler/util/StorageUtils.kt index b98c28ff..cd3cd6a5 100644 --- a/app/src/main/java/jp/juggler/util/StorageUtils.kt +++ b/app/src/main/java/jp/juggler/util/StorageUtils.kt @@ -167,66 +167,66 @@ import java.util.* private const val MIME_TYPE_APPLICATION_OCTET_STREAM = "application/octet-stream" -private val mimeTypeExMap : HashMap by lazy { - val map = HashMap() - map["BDM"] = "application/vnd.syncml.dm+wbxml" - map["DAT"] = "" - map["TID"] = "" - map["js"] = "text/javascript" - map["sh"] = "application/x-sh" - map["lua"] = "text/x-lua" - map +private val mimeTypeExMap: HashMap by lazy { + val map = HashMap() + map["BDM"] = "application/vnd.syncml.dm+wbxml" + map["DAT"] = "" + map["TID"] = "" + map["js"] = "text/javascript" + map["sh"] = "application/x-sh" + map["lua"] = "text/x-lua" + map } @Suppress("unused") -fun getMimeType(log : LogCategory?, src : String) : String { - var ext = MimeTypeMap.getFileExtensionFromUrl(src) - if(ext != null && ext.isNotEmpty()) { - ext = ext.lowercase() - - // - var mime_type : String? = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext) - if(mime_type?.isNotEmpty() == true) return mime_type - - // - mime_type = mimeTypeExMap[ext] - if(mime_type?.isNotEmpty() == true) return mime_type - - // 戻り値が空文字列の場合とnullの場合があり、空文字列の場合は既知なのでログ出力しない - - if(mime_type == null && log != null) { - log.w("getMimeType(): unknown file extension '%s'", ext) - } - } - return MIME_TYPE_APPLICATION_OCTET_STREAM +fun getMimeType(log: LogCategory?, src: String): String { + var ext = MimeTypeMap.getFileExtensionFromUrl(src) + if (ext != null && ext.isNotEmpty()) { + ext = ext.lowercase() + + // + var mime_type: String? = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext) + if (mime_type?.isNotEmpty() == true) return mime_type + + // + mime_type = mimeTypeExMap[ext] + if (mime_type?.isNotEmpty() == true) return mime_type + + // 戻り値が空文字列の場合とnullの場合があり、空文字列の場合は既知なのでログ出力しない + + if (mime_type == null && log != null) { + log.w("getMimeType(): unknown file extension '${ext}'") + } + } + return MIME_TYPE_APPLICATION_OCTET_STREAM } -fun getDocumentName(contentResolver : ContentResolver, uri : Uri) : String { - val errorName = "no_name" - return contentResolver.query(uri, null, null, null, null, null) - ?.use { cursor -> - return if(! cursor.moveToFirst()) { - errorName - } else { - cursor.getStringOrNull(OpenableColumns.DISPLAY_NAME) ?: errorName - } - } - ?: errorName +fun getDocumentName(contentResolver: ContentResolver, uri: Uri): String { + val errorName = "no_name" + return contentResolver.query(uri, null, null, null, null, null) + ?.use { cursor -> + return if (!cursor.moveToFirst()) { + errorName + } else { + cursor.getStringOrNull(OpenableColumns.DISPLAY_NAME) ?: errorName + } + } + ?: errorName } -fun getStreamSize(bClose : Boolean, inStream : InputStream) : Long { - try { - var size = 0L - while(true) { - val r = IOUtils.skip(inStream, 16384) - if(r <= 0) break - size += r - } - return size - } finally { - @Suppress("DEPRECATION") - if(bClose) IOUtils.closeQuietly(inStream) - } +fun getStreamSize(bClose: Boolean, inStream: InputStream): Long { + try { + var size = 0L + while (true) { + val r = IOUtils.skip(inStream, 16384) + if (r <= 0) break + size += r + } + return size + } finally { + @Suppress("DEPRECATION") + if (bClose) IOUtils.closeQuietly(inStream) + } } //fun File.loadByteArray() : ByteArray { @@ -242,84 +242,84 @@ fun getStreamSize(bClose : Boolean, inStream : InputStream) : Long { // } //} -fun Context.loadRawResource(resId : Int) : ByteArray { - resources.openRawResource(resId).use { inStream -> - val bao = ByteArrayOutputStream(inStream.available()) - IOUtils.copy(inStream, bao) - return bao.toByteArray() - } +fun Context.loadRawResource(resId: Int): ByteArray { + resources.openRawResource(resId).use { inStream -> + val bao = ByteArrayOutputStream(inStream.available()) + IOUtils.copy(inStream, bao) + return bao.toByteArray() + } } -fun intentOpenDocument(mimeType : String) : Intent { - val intent = Intent(Intent.ACTION_OPEN_DOCUMENT) - intent.addCategory(Intent.CATEGORY_OPENABLE) - intent.type = mimeType // "image/*" - return intent +fun intentOpenDocument(mimeType: String): Intent { + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT) + intent.addCategory(Intent.CATEGORY_OPENABLE) + intent.type = mimeType // "image/*" + return intent } fun intentGetContent( - allowMultiple : Boolean, - caption : String, - mimeTypes : Array -) : Intent { - val intent = Intent(Intent.ACTION_GET_CONTENT) - intent.addCategory(Intent.CATEGORY_OPENABLE) - - if(allowMultiple) { - // EXTRA_ALLOW_MULTIPLE は API 18 (4.3)以降。ACTION_GET_CONTENT でも ACTION_OPEN_DOCUMENT でも指定できる - intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) - } - - // EXTRA_MIME_TYPES は API 19以降。ACTION_GET_CONTENT でも ACTION_OPEN_DOCUMENT でも指定できる - intent.putExtra("android.intent.extra.MIME_TYPES", mimeTypes) - - intent.type = when { - mimeTypes.size == 1 -> mimeTypes[0] - - // On Android 6.0 and above using "video/* image/" or "image/ video/*" type doesn't work - // it only recognizes the first filter you specify. - Build.VERSION.SDK_INT >= 23 -> "*/*" - - else -> mimeTypes.joinToString(" ") - } - - return Intent.createChooser(intent, caption) + allowMultiple: Boolean, + caption: String, + mimeTypes: Array +): Intent { + val intent = Intent(Intent.ACTION_GET_CONTENT) + intent.addCategory(Intent.CATEGORY_OPENABLE) + + if (allowMultiple) { + // EXTRA_ALLOW_MULTIPLE は API 18 (4.3)以降。ACTION_GET_CONTENT でも ACTION_OPEN_DOCUMENT でも指定できる + intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) + } + + // EXTRA_MIME_TYPES は API 19以降。ACTION_GET_CONTENT でも ACTION_OPEN_DOCUMENT でも指定できる + intent.putExtra("android.intent.extra.MIME_TYPES", mimeTypes) + + intent.type = when { + mimeTypes.size == 1 -> mimeTypes[0] + + // On Android 6.0 and above using "video/* image/" or "image/ video/*" type doesn't work + // it only recognizes the first filter you specify. + Build.VERSION.SDK_INT >= 23 -> "*/*" + + else -> mimeTypes.joinToString(" ") + } + + return Intent.createChooser(intent, caption) } data class GetContentResultEntry( - val uri : Uri, - val mimeType : String? = null, - var time : Long? = null + val uri: Uri, + val mimeType: String? = null, + var time: Long? = null ) // returns list of pair of uri and mime-type. -fun Intent.handleGetContentResult(contentResolver : ContentResolver) : ArrayList { - val urlList = ArrayList() - // 単一選択 - this.data?.let { - urlList.add(GetContentResultEntry(it, this.type)) - } - // 複数選択 - val cd = this.clipData - if(cd != null) { - for(i in 0 until cd.itemCount) { - cd.getItemAt(i)?.uri?.let { uri -> - if(null == urlList.find { it.uri == uri }) { - urlList.add(GetContentResultEntry(uri)) - } - } - } - } - urlList.forEach { - try { - contentResolver.takePersistableUriPermission( - it.uri, - Intent.FLAG_GRANT_READ_URI_PERMISSION - ) - } catch(_ : Throwable) { - } - } - return urlList +fun Intent.handleGetContentResult(contentResolver: ContentResolver): ArrayList { + val urlList = ArrayList() + // 単一選択 + this.data?.let { + urlList.add(GetContentResultEntry(it, this.type)) + } + // 複数選択 + val cd = this.clipData + if (cd != null) { + for (i in 0 until cd.itemCount) { + cd.getItemAt(i)?.uri?.let { uri -> + if (null == urlList.find { it.uri == uri }) { + urlList.add(GetContentResultEntry(uri)) + } + } + } + } + urlList.forEach { + try { + contentResolver.takePersistableUriPermission( + it.uri, + Intent.FLAG_GRANT_READ_URI_PERMISSION + ) + } catch (_: Throwable) { + } + } + return urlList }