diff --git a/.idea/dictionaries/tateisu.xml b/.idea/dictionaries/tateisu.xml index 5e33879d..cbf6ca42 100644 --- a/.idea/dictionaries/tateisu.xml +++ b/.idea/dictionaries/tateisu.xml @@ -84,6 +84,7 @@ mastodonsearch mimumedon misskey + misskeyclientproto miyon mpeg mpga @@ -126,6 +127,7 @@ styler subwaytooter swipy + systemui taisaku tateisu tbody diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActMain.kt b/app/src/main/java/jp/juggler/subwaytooter/ActMain.kt index 03385e19..7fcd0e40 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActMain.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ActMain.kt @@ -54,276 +54,262 @@ import kotlin.math.max import kotlin.math.min class ActMain : AsyncActivity(), Column.Callback, View.OnClickListener, - ViewPager.OnPageChangeListener, DrawerLayout.DrawerListener { - - class PhoneEnv { - - internal lateinit var pager : MyViewPager - internal lateinit var pager_adapter : ColumnPagerAdapter - } - - class TabletEnv { - - internal lateinit var tablet_pager : RecyclerView - internal lateinit var tablet_pager_adapter : TabletColumnPagerAdapter - internal lateinit var tablet_layout_manager : LinearLayoutManager - internal lateinit var tablet_snap_helper : GravitySnapHelper - } - - companion object { - - val log = LogCategory("ActMain") - - // リザルト - const val RESULT_APP_DATA_IMPORT = Activity.RESULT_FIRST_USER - - // リクエスト - const val REQUEST_CODE_COLUMN_LIST = 1 - const val REQUEST_CODE_ACCOUNT_SETTING = 2 - const val REQUEST_APP_ABOUT = 3 - const val REQUEST_CODE_NICKNAME = 4 - const val REQUEST_CODE_POST = 5 - const val REQUEST_CODE_COLUMN_COLOR = 6 - const val REQUEST_CODE_APP_SETTING = 7 - const val REQUEST_CODE_TEXT = 8 - const val REQUEST_CODE_LANGUAGE_FILTER = 9 - - const val COLUMN_WIDTH_MIN_DP = 300 - - const val STATE_CURRENT_PAGE = "current_page" - - // 外部からインテントを受信した後、アカウント選択中に画面回転したらアカウント選択からやり直す - internal var sent_intent2 : Intent? = null - - // アプリ設定のキャッシュ - var boostButtonSize = 1 - var replyIconSize = 1 - var headerIconSize = 1 - var stripIconSize = 1 - var screenBottomPadding = 0 - var timeline_font : Typeface = Typeface.DEFAULT - var timeline_font_bold : Typeface = Typeface.DEFAULT_BOLD - - private fun Float.clipFontSize() : Float = - if(isNaN()) this else max(1f, this) - } - - // アプリ設定のキャッシュ - var density = 0f - var acct_pad_lr = 0 - var timeline_font_size_sp = Float.NaN - var acct_font_size_sp = Float.NaN - var notification_tl_font_size_sp = Float.NaN - var header_text_size_sp = Float.NaN - var timeline_spacing : Float? = null - var avatarIconSize : Int = 0 - var notificationTlIconSize : Int = 0 - - // onResume() .. onPause() の間なら真 - private var isResumed = false - - // onStart() .. onStop() の間なら真 - private var isStart_ = false - - // onActivityResultで設定されてonResumeで消化される - // 状態保存の必要なし - private var posted_acct : Acct? = null // acctAscii - private var posted_status_id : EntityId? = null - private var posted_reply_id : EntityId? = null - private var posted_redraft_id : EntityId? = null - - // 画面上のUI操作で生成されて - // onPause,onPageDestroy 等のタイミングで閉じられる - // 状態保存の必要なし - internal var listItemPopup : StatusButtonsPopup? = null - - private var phoneEnv : PhoneEnv? = null - private var tabletEnv : TabletEnv? = null - - private var nScreenColumn : Int = 0 - private var nColumnWidth : Int = 0 // dividerの幅を含む - - private var nAutoCwCellWidth = 0 - private var nAutoCwLines = 0 - - private var dlgPrivacyPolicy : WeakReference? = null - - private var quickTootVisibility : TootVisibility = TootVisibility.AccountSetting - - ////////////////////////////////////////////////////////////////// - // 変更しない変数(lateinit) - - private lateinit var llQuickTootBar : LinearLayout - private lateinit var etQuickToot : MyEditText - private lateinit var btnQuickToot : ImageButton - private lateinit var btnQuickTootMenu : ImageButton - private lateinit var llEmpty : View - private lateinit var llColumnStrip : ColumnStripLinearLayout - private lateinit var svColumnStrip : HorizontalScrollView - private lateinit var btnMenu : ImageButton - private lateinit var btnToot : ImageButton - private lateinit var vFooterDivider1 : View - private lateinit var vFooterDivider2 : View - - lateinit var drawer : MyDrawerLayout - - lateinit var post_helper : PostHelper - - lateinit var pref : SharedPreferences - lateinit var handler : Handler - lateinit var app_state : AppState - - ////////////////////////////////////////////////////////////////// - // 変更しない変数 - - val follow_complete_callback : ()->Unit = { - showToast(false, R.string.follow_succeeded) - } - - val unfollow_complete_callback : ()->Unit = { - showToast(false, R.string.unfollow_succeeded) - } - val cancel_follow_request_complete_callback : ()->Unit = { - showToast(false, R.string.follow_request_cancelled) - } - - val favourite_complete_callback : ()->Unit = { - showToast(false, R.string.favourite_succeeded) - } - val unfavourite_complete_callback : ()->Unit = { - showToast(false, R.string.unfavourite_succeeded) - } - - val bookmark_complete_callback : ()->Unit = { - showToast(false, R.string.bookmark_succeeded) - } - val unbookmark_complete_callback : ()->Unit = { - showToast(false, R.string.unbookmark_succeeded) - } - - val boost_complete_callback : ()->Unit = { - showToast(false, R.string.boost_succeeded) - } - - val unboost_complete_callback : ()->Unit = { - showToast(false, R.string.unboost_succeeded) - } - - val reaction_complete_callback : ()->Unit = { - showToast(false, R.string.reaction_succeeded) - } - - // 相対時刻の表記を定期的に更新する - private val proc_updateRelativeTime = object : Runnable { - override fun run() { - handler.removeCallbacks(this) - if(! isStart_) return - if(Pref.bpRelativeTimestamp(pref)) { - for(c in app_state.column_list) { - c.fireRelativeTime() - } - handler.postDelayed(this, 10000L) - } - } - } - - private val link_click_listener : (View, MyClickableSpan) -> Unit = { viewClicked, span -> - - val linkInfo = span.linkInfo - var view = viewClicked - var column : Column? = null - var whoRef : TootAccountRef? = null - - while(true) { - val tag = view.tag - if(tag is ItemViewHolder) { - column = tag.column - whoRef = tag.getAccount() - - break - } else if(tag is ViewHolderItem) { - column = tag.ivh.column - whoRef = tag.ivh.getAccount() - break - } else if(tag is ColumnViewHolder) { - column = tag.column - whoRef = null - break - } else if(tag is ViewHolderHeaderBase) { - column = tag.column - whoRef = tag.getAccount() - break - } else if(tag is TabletColumnViewHolder) { - column = tag.columnViewHolder.column - break - } else { - val parent = view.parent - if(parent is View) { - view = parent - } else { - break - } - } - } - val pos = nextPosition(column) - val access_info = column?.access_info - - var tag_list : ArrayList? = null - - try { - val cs = (viewClicked as TextView).text - if(cs is Spannable) { - for(s in cs.getSpans(0, cs.length, MyClickableSpan::class.java)) { - val li = s.linkInfo - val pair = li.url.findHashtagFromUrl() - if(pair != null) { - if(tag_list == null) tag_list = ArrayList() - tag_list.add(if(li.text.startsWith('#')) li.text else "#${pair.first}") - } - } - } - } catch(ex : Throwable) { - log.trace(ex) - } + ViewPager.OnPageChangeListener, DrawerLayout.DrawerListener { - openCustomTab( - this@ActMain, - pos, + class PhoneEnv { + + internal lateinit var pager: MyViewPager + internal lateinit var pager_adapter: ColumnPagerAdapter + } + + class TabletEnv { + + internal lateinit var tablet_pager: RecyclerView + internal lateinit var tablet_pager_adapter: TabletColumnPagerAdapter + internal lateinit var tablet_layout_manager: LinearLayoutManager + internal lateinit var tablet_snap_helper: GravitySnapHelper + } + + companion object { + + val log = LogCategory("ActMain") + + // リザルト + const val RESULT_APP_DATA_IMPORT = Activity.RESULT_FIRST_USER + + // リクエスト + const val REQUEST_CODE_COLUMN_LIST = 1 + const val REQUEST_CODE_ACCOUNT_SETTING = 2 + const val REQUEST_APP_ABOUT = 3 + const val REQUEST_CODE_NICKNAME = 4 + const val REQUEST_CODE_POST = 5 + const val REQUEST_CODE_COLUMN_COLOR = 6 + const val REQUEST_CODE_APP_SETTING = 7 + const val REQUEST_CODE_TEXT = 8 + const val REQUEST_CODE_LANGUAGE_FILTER = 9 + + const val COLUMN_WIDTH_MIN_DP = 300 + + const val STATE_CURRENT_PAGE = "current_page" + + // 外部からインテントを受信した後、アカウント選択中に画面回転したらアカウント選択からやり直す + internal var sent_intent2: Intent? = null + + // アプリ設定のキャッシュ + var boostButtonSize = 1 + var replyIconSize = 1 + var headerIconSize = 1 + var stripIconSize = 1 + var screenBottomPadding = 0 + var timeline_font: Typeface = Typeface.DEFAULT + var timeline_font_bold: Typeface = Typeface.DEFAULT_BOLD + + private fun Float.clipFontSize(): Float = + if (isNaN()) this else max(1f, this) + } + + // アプリ設定のキャッシュ + var density = 0f + var acct_pad_lr = 0 + var timeline_font_size_sp = Float.NaN + var acct_font_size_sp = Float.NaN + var notification_tl_font_size_sp = Float.NaN + var header_text_size_sp = Float.NaN + var timeline_spacing: Float? = null + var avatarIconSize: Int = 0 + var notificationTlIconSize: Int = 0 + + // onResume() .. onPause() の間なら真 + private var isResumed = false + + // onStart() .. onStop() の間なら真 + private var isStart_ = false + + // onActivityResultで設定されてonResumeで消化される + // 状態保存の必要なし + private var posted_acct: Acct? = null // acctAscii + private var posted_status_id: EntityId? = null + private var posted_reply_id: EntityId? = null + private var posted_redraft_id: EntityId? = null + + // 画面上のUI操作で生成されて + // onPause,onPageDestroy 等のタイミングで閉じられる + // 状態保存の必要なし + internal var listItemPopup: StatusButtonsPopup? = null + + private var phoneEnv: PhoneEnv? = null + private var tabletEnv: TabletEnv? = null + + private var nScreenColumn: Int = 0 + private var nColumnWidth: Int = 0 // dividerの幅を含む + + private var nAutoCwCellWidth = 0 + private var nAutoCwLines = 0 + + private var dlgPrivacyPolicy: WeakReference? = null + + private var quickTootVisibility: TootVisibility = TootVisibility.AccountSetting + + ////////////////////////////////////////////////////////////////// + // 変更しない変数(lateinit) + + private lateinit var llQuickTootBar: LinearLayout + private lateinit var etQuickToot: MyEditText + private lateinit var btnQuickToot: ImageButton + private lateinit var btnQuickTootMenu: ImageButton + private lateinit var llEmpty: View + private lateinit var llColumnStrip: ColumnStripLinearLayout + private lateinit var svColumnStrip: HorizontalScrollView + private lateinit var btnMenu: ImageButton + private lateinit var btnToot: ImageButton + private lateinit var vFooterDivider1: View + private lateinit var vFooterDivider2: View + + lateinit var drawer: MyDrawerLayout + + lateinit var post_helper: PostHelper + + lateinit var pref: SharedPreferences + lateinit var handler: Handler + lateinit var app_state: AppState + + ////////////////////////////////////////////////////////////////// + // 読み取り専用のプロパティ + + val follow_complete_callback: () -> Unit = { + showToast(false, R.string.follow_succeeded) + } + + val unfollow_complete_callback: () -> Unit = { + showToast(false, R.string.unfollow_succeeded) + } + val cancel_follow_request_complete_callback: () -> Unit = { + showToast(false, R.string.follow_request_cancelled) + } + + val favourite_complete_callback: () -> Unit = { + showToast(false, R.string.favourite_succeeded) + } + val unfavourite_complete_callback: () -> Unit = { + showToast(false, R.string.unfavourite_succeeded) + } + + val bookmark_complete_callback: () -> Unit = { + showToast(false, R.string.bookmark_succeeded) + } + val unbookmark_complete_callback: () -> Unit = { + showToast(false, R.string.unbookmark_succeeded) + } + + val boost_complete_callback: () -> Unit = { + showToast(false, R.string.boost_succeeded) + } + + val unboost_complete_callback: () -> Unit = { + showToast(false, R.string.unboost_succeeded) + } + + val reaction_complete_callback: () -> Unit = { + showToast(false, R.string.reaction_succeeded) + } + + // 相対時刻の表記を定期的に更新する + private val proc_updateRelativeTime = object : Runnable { + override fun run() { + handler.removeCallbacks(this) + if (!isStart_) return + if (Pref.bpRelativeTimestamp(pref)) { + for (c in app_state.column_list) { + c.fireRelativeTime() + } + handler.postDelayed(this, 10000L) + } + } + } + + private val link_click_listener: (View, MyClickableSpan) -> Unit = { viewClicked, span -> + + // ビュー階層を下から辿って文脈を取得する + var column: Column? = null + var whoRef: TootAccountRef? = null + var view = viewClicked + loop@ while (true) { + when (val tag = view.tag) { + is ItemViewHolder -> { + column = tag.column + whoRef = tag.getAccount() + break@loop + } + is ViewHolderItem -> { + column = tag.ivh.column + whoRef = tag.ivh.getAccount() + break@loop + } + is ColumnViewHolder -> { + column = tag.column + whoRef = null + break@loop + } + is ViewHolderHeaderBase -> { + column = tag.column + whoRef = tag.getAccount() + break@loop + } + is TabletColumnViewHolder -> { + column = tag.columnViewHolder.column + break@loop + } + else -> when (val parent = view.parent) { + is View -> view = parent + else -> break@loop + } + } + } + + val hashtagList = ArrayList().apply { + try { + val cs = viewClicked.cast()?.text + if (cs is Spannable) { + for (s in cs.getSpans(0, cs.length, MyClickableSpan::class.java)) { + val li = s.linkInfo + val pair = li.url.findHashtagFromUrl() + if (pair != null) add(if (li.text.startsWith('#')) li.text else "#${pair.first}") + } + } + } catch (ex: Throwable) { + log.trace(ex) + } + } + + val linkInfo = span.linkInfo + + openCustomTab( + this, + nextPosition(column), linkInfo.url, - accessInfo = access_info, - tagList = tag_list, + accessInfo = column?.access_info, + tagList = hashtagList.notEmpty(), whoRef = whoRef, linkInfo = linkInfo ) - } - - private fun showQuickTootVisibility() { - btnQuickTootMenu.imageResource = - when(val resId = Styler.getVisibilityIconId(false, quickTootVisibility)) { - R.drawable.ic_question -> R.drawable.ic_description - else -> resId - } - } - - private fun performQuickTootMenu() { - dlgQuickTootMenu.toggle() - } - - private val dlgQuickTootMenu = DlgQuickTootMenu(this, object : DlgQuickTootMenu.Callback { - - override var visibility : TootVisibility + } + + + private val dlgQuickTootMenu = DlgQuickTootMenu(this, object : DlgQuickTootMenu.Callback { + + override var visibility: TootVisibility get() = quickTootVisibility set(value) { - if(value != quickTootVisibility) { + if (value != quickTootVisibility) { quickTootVisibility = value pref.edit().put(Pref.spQuickTootVisibility, value.id.toString()).apply() showQuickTootVisibility() } } - - override fun onMacro(text : String) { + + override fun onMacro(text: String) { val editable = etQuickToot.text - if(editable?.isNotEmpty() == true) { + if (editable?.isNotEmpty() == true) { val start = etQuickToot.selectionStart val end = etQuickToot.selectionEnd editable.replace(start, end, text) @@ -336,62 +322,59 @@ class ActMain : AsyncActivity(), Column.Callback, View.OnClickListener, } } }) - - val viewPool = RecyclerView.RecycledViewPool() - - ////////////////////////////////////////////////////////////////// - // 読み取り専用のプロパティ - - override val isActivityStart : Boolean - get() = isStart_ - - // スマホモードなら現在のカラムを、タブレットモードなら-1Lを返す - // (カラム一覧画面のデフォルト選択位置に使われる) - val currentColumn : Int - get() = phoneTab( + + val viewPool = RecyclerView.RecycledViewPool() + + override val isActivityStart: Boolean + get() = isStart_ + + // スマホモードなら現在のカラムを、タブレットモードなら-1Lを返す + // (カラム一覧画面のデフォルト選択位置に使われる) + val currentColumn: Int + get() = phoneTab( { it.pager.currentItem }, - { - 1 } + { -1 } ) - - // 新しいカラムをどこに挿入するか - // 現在のページの次の位置か、終端 - val defaultInsertPosition : Int - get() = phoneTab( + + // 新しいカラムをどこに挿入するか + // 現在のページの次の位置か、終端 + val defaultInsertPosition: Int + get() = phoneTab( { it.pager.currentItem + 1 }, { Integer.MAX_VALUE } ) - - private val TabletEnv.visibleColumnsIndices : IntRange - get() { - var vs = tablet_layout_manager.findFirstVisibleItemPosition() - var ve = tablet_layout_manager.findLastVisibleItemPosition() - if(vs == RecyclerView.NO_POSITION || ve == RecyclerView.NO_POSITION) { - return IntRange(- 1, - 2) // empty and less than zero - } - - val child = tablet_layout_manager.findViewByPosition(vs) - val slide_ratio = - clipRange(0f, 1f, abs((child?.left ?: 0) / nColumnWidth.toFloat())) - if(slide_ratio >= 0.95f) { - ++ vs - ++ ve - } - return IntRange(vs, min(ve, vs + nScreenColumn - 1)) - } - - private val TabletEnv.visibleColumns : List - get() = visibleColumnsIndices - .mapNotNull { - try { - app_state.column_list[it] - } catch(ex : Throwable) { - null - } - } - - // デフォルトの投稿先アカウントを探す。アカウント選択が必要な状況ならnull - val currentPostTarget : SavedAccount? - get() = phoneTab( + + private val TabletEnv.visibleColumnsIndices: IntRange + get() { + var vs = tablet_layout_manager.findFirstVisibleItemPosition() + var ve = tablet_layout_manager.findLastVisibleItemPosition() + if (vs == RecyclerView.NO_POSITION || ve == RecyclerView.NO_POSITION) { + return IntRange(-1, -2) // empty and less than zero + } + + val child = tablet_layout_manager.findViewByPosition(vs) + val slide_ratio = + clipRange(0f, 1f, abs((child?.left ?: 0) / nColumnWidth.toFloat())) + if (slide_ratio >= 0.95f) { + ++vs + ++ve + } + return IntRange(vs, min(ve, vs + nScreenColumn - 1)) + } + + private val TabletEnv.visibleColumns: List + get() = visibleColumnsIndices + .mapNotNull { + try { + app_state.column_list[it] + } catch (ex: Throwable) { + null + } + } + + // デフォルトの投稿先アカウントを探す。アカウント選択が必要な状況ならnull + val currentPostTarget: SavedAccount? + get() = phoneTab( { env -> val c = env.pager_adapter.getColumn(env.pager.currentItem) return when { @@ -400,124 +383,124 @@ class ActMain : AsyncActivity(), Column.Callback, View.OnClickListener, } }, { env -> - + val db_id = Pref.lpTabletTootDefaultAccount(App1.pref) - if(db_id != - 1L) { + if (db_id != -1L) { val a = SavedAccount.loadAccount(this@ActMain, db_id) - if(a != null && ! a.isPseudo) return a + if (a != null && !a.isPseudo) return a } - + val accounts = ArrayList() - for(c in env.visibleColumns) { + for (c in env.visibleColumns) { try { val a = c.access_info // 画面内に疑似アカウントがあれば常にアカウント選択が必要 - if(a.isPseudo) { + if (a.isPseudo) { accounts.clear() break } // 既出でなければ追加する - if(null == accounts.find { it == a }) accounts.add(a) - } catch(ex : Throwable) { - + if (null == accounts.find { it == a }) accounts.add(a) + } catch (ex: Throwable) { + } } - - return when(accounts.size) { + + return when (accounts.size) { // 候補が1つだけならアカウント選択は不要 1 -> accounts.first() // 候補が2つ以上ならアカウント選択は必要 else -> null } }) - - // 簡易投稿入力のテキスト - val quickTootText : String - get() = etQuickToot.text.toString() - - ////////////////////////////////////////////////////////////////// - // アクティビティイベント - - override fun onCreate(savedInstanceState : Bundle?) { - log.d("onCreate") - super.onCreate(savedInstanceState) - requestWindowFeature(Window.FEATURE_NO_TITLE) - App1.setActivityTheme(this, noActionBar = true) - - handler = App1.getAppState(this).handler - app_state = App1.getAppState(this) - pref = App1.pref - - density = app_state.density - acct_pad_lr = (0.5f + 4f * density).toInt() - - timeline_font_size_sp = Pref.fpTimelineFontSize(pref).clipFontSize() - acct_font_size_sp = Pref.fpAcctFontSize(pref).clipFontSize() - notification_tl_font_size_sp = Pref.fpNotificationTlFontSize(pref).clipFontSize() - header_text_size_sp = Pref.fpHeaderTextSize(pref).clipFontSize() - - val fv = Pref.spTimelineSpacing(pref).toFloatOrNull() - timeline_spacing = if(fv != null && fv.isFinite() && fv != 0f) fv else null - - initUI() - - updateColumnStrip() - - if(app_state.column_list.isNotEmpty()) { - - // 前回最後に表示していたカラムの位置にスクロールする - val column_pos = Pref.ipLastColumnPos(pref) - log.d("ipLastColumnPos load $column_pos") - if(column_pos in 0 until app_state.column_list.size) { - scrollToColumn(column_pos, false) - } - - // 表示位置に合わせたイベントを発行 - phoneTab( + + // 簡易投稿入力のテキスト + val quickTootText: String + get() = etQuickToot.text.toString() + + ////////////////////////////////////////////////////////////////// + // アクティビティイベント + + override fun onCreate(savedInstanceState: Bundle?) { + log.d("onCreate") + super.onCreate(savedInstanceState) + requestWindowFeature(Window.FEATURE_NO_TITLE) + App1.setActivityTheme(this, noActionBar = true) + + handler = App1.getAppState(this).handler + app_state = App1.getAppState(this) + pref = App1.pref + + density = app_state.density + acct_pad_lr = (0.5f + 4f * density).toInt() + + timeline_font_size_sp = Pref.fpTimelineFontSize(pref).clipFontSize() + acct_font_size_sp = Pref.fpAcctFontSize(pref).clipFontSize() + notification_tl_font_size_sp = Pref.fpNotificationTlFontSize(pref).clipFontSize() + header_text_size_sp = Pref.fpHeaderTextSize(pref).clipFontSize() + + val fv = Pref.spTimelineSpacing(pref).toFloatOrNull() + timeline_spacing = if (fv != null && fv.isFinite() && fv != 0f) fv else null + + initUI() + + updateColumnStrip() + + if (app_state.column_list.isNotEmpty()) { + + // 前回最後に表示していたカラムの位置にスクロールする + val column_pos = Pref.ipLastColumnPos(pref) + log.d("ipLastColumnPos load $column_pos") + if (column_pos in 0 until app_state.column_list.size) { + scrollToColumn(column_pos, false) + } + + // 表示位置に合わせたイベントを発行 + phoneTab( { env -> onPageSelected(env.pager.currentItem) }, { env -> resizeColumnWidth(env) } ) - } - - PollingWorker.queueUpdateNotification(this) - - if(savedInstanceState != null) { - sent_intent2?.let { handleSentIntent(it) } - } - - checkPrivacyPolicy() - } - - override fun onDestroy() { - log.d("onDestroy") - super.onDestroy() - post_helper.onDestroy() - - // このアクティビティに関連する ColumnViewHolder への参照を全カラムから除去する - for(c in app_state.column_list) { - c.removeColumnViewHolderByActivity(this) - } - } - - override fun onNewIntent(intent : Intent?) { - super.onNewIntent(intent) - log.w("onNewIntent: isResumed = isResumed") - } - - override fun onConfigurationChanged(newConfig : Configuration) { - log.d("onConfigurationChanged") - super.onConfigurationChanged(newConfig) - if(newConfig.screenHeightDp > 0 || newConfig.screenHeightDp > 0) { - tabOnly { env -> resizeColumnWidth(env) } - } - } - - override fun onSaveInstanceState(outState : Bundle) { - super.onSaveInstanceState(outState) - - log.d("onSaveInstanceState") - - phoneTab( + } + + PollingWorker.queueUpdateNotification(this) + + if (savedInstanceState != null) { + sent_intent2?.let { handleSentIntent(it) } + } + + checkPrivacyPolicy() + } + + override fun onDestroy() { + log.d("onDestroy") + super.onDestroy() + post_helper.onDestroy() + + // このアクティビティに関連する ColumnViewHolder への参照を全カラムから除去する + for (c in app_state.column_list) { + c.removeColumnViewHolderByActivity(this) + } + } + + override fun onNewIntent(intent: Intent?) { + super.onNewIntent(intent) + log.w("onNewIntent: isResumed = isResumed") + } + + override fun onConfigurationChanged(newConfig: Configuration) { + log.d("onConfigurationChanged") + super.onConfigurationChanged(newConfig) + if (newConfig.screenHeightDp > 0 || newConfig.screenHeightDp > 0) { + tabOnly { env -> resizeColumnWidth(env) } + } + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + + log.d("onSaveInstanceState") + + phoneTab( { env -> outState.putInt(STATE_CURRENT_PAGE, env.pager.currentItem) }, { env -> env.tablet_layout_manager.findLastVisibleItemPosition() @@ -525,468 +508,478 @@ class ActMain : AsyncActivity(), Column.Callback, View.OnClickListener, ?.let { outState.putInt(STATE_CURRENT_PAGE, it) } } ) - - for(column in app_state.column_list) { - column.saveScrollPosition() - } - - } - - override fun onRestoreInstanceState(savedInstanceState : Bundle) { - log.d("onRestoreInstanceState") - super.onRestoreInstanceState(savedInstanceState) - val pos = savedInstanceState.getInt(STATE_CURRENT_PAGE) - if(pos > 0 && pos < app_state.column_list.size) { - phoneTab( + + for (column in app_state.column_list) { + column.saveScrollPosition() + } + + } + + override fun onRestoreInstanceState(savedInstanceState: Bundle) { + log.d("onRestoreInstanceState") + super.onRestoreInstanceState(savedInstanceState) + val pos = savedInstanceState.getInt(STATE_CURRENT_PAGE) + if (pos > 0 && pos < app_state.column_list.size) { + phoneTab( { env -> env.pager.currentItem = pos }, { env -> env.tablet_layout_manager .smoothScrollToPosition(env.tablet_pager, null, pos) } ) - } - } - - override fun onStart() { - val tsTotal = SystemClock.elapsedRealtime() - super.onStart() - - isStart_ = true - log.d("onStart") - - var ts = SystemClock.elapsedRealtime() - var te : Long - - // カラーカスタマイズを読み直す - ListDivider.color = Pref.ipListDividerColor(pref) - TabletColumnDivider.color = Pref.ipListDividerColor(pref) - ItemViewHolder.toot_color_unlisted = Pref.ipTootColorUnlisted(pref) - ItemViewHolder.toot_color_follower = Pref.ipTootColorFollower(pref) - ItemViewHolder.toot_color_direct_user = Pref.ipTootColorDirectUser(pref) - ItemViewHolder.toot_color_direct_me = Pref.ipTootColorDirectMe(pref) - MyClickableSpan.showLinkUnderline = Pref.bpShowLinkUnderline(pref) - MyClickableSpan.defaultLinkColor = Pref.ipLinkColor(pref).notZero() - ?: getAttributeColor(R.attr.colorLink) - - CustomShare.reloadCache(this, pref) - - te = SystemClock.elapsedRealtime() - if(te - ts >= 100L) log.w("onStart: ${te - ts}ms : reload color") - ts = SystemClock.elapsedRealtime() - - var tz = TimeZone.getDefault() - try { - val tz_id = Pref.spTimeZone(pref) - if(tz_id.isNotEmpty()) { - tz = TimeZone.getTimeZone(tz_id) - } - } catch(ex : Throwable) { - log.e(ex, "getTimeZone failed.") - } - TootStatus.date_format.timeZone = tz - - te = SystemClock.elapsedRealtime() - if(te - ts >= 100L) log.w("onStart: ${te - ts}ms : reload timezone") - ts = SystemClock.elapsedRealtime() - - // バグいアカウントデータを消す - try { - SavedAccount.sweepBuggieData() - } catch(ex : Throwable) { - log.trace(ex) - } - - te = SystemClock.elapsedRealtime() - if(te - ts >= 100L) log.w("onStart: ${te - ts}ms : sweepBuggieData") - ts = SystemClock.elapsedRealtime() - - // アカウント設定から戻ってきたら、カラムを消す必要があるかもしれない - val new_order = ArrayList() - - for(i in 0 until app_state.column_list.size) { - val column = app_state.column_list[i] - - if(! column.access_info.isNA) { - // 存在確認 - SavedAccount.loadAccount(this@ActMain, column.access_info.db_id) - ?: continue - } - new_order.add(i) - } - - if(new_order.size != app_state.column_list.size) { - setOrder(new_order) - } - - te = SystemClock.elapsedRealtime() - if(te - ts >= 100L) log.w("onStart: ${te - ts}ms : column order") - ts = SystemClock.elapsedRealtime() - - // 背景画像を表示しない設定が変更された時にカラムの背景を設定しなおす - for(column in app_state.column_list) { - column.viewHolder?.lastAnnouncementShown = 0L - column.fireColumnColor() - } - - te = SystemClock.elapsedRealtime() - if(te - ts >= 100L) log.w("onStart: ${te - ts}ms :fireColumnColor") - ts = SystemClock.elapsedRealtime() - - // 各カラムのアカウント設定を読み直す - reloadAccountSetting() - - te = SystemClock.elapsedRealtime() - if(te - ts >= 100L) log.w("onStart: ${te - ts}ms :reloadAccountSetting") - ts = SystemClock.elapsedRealtime() - - // 投稿直後ならカラムの再取得を行う - refreshAfterPost() - - te = SystemClock.elapsedRealtime() - if(te - ts >= 100L) log.w("onStart: ${te - ts}ms :refreshAfterPost") - ts = SystemClock.elapsedRealtime() - - // 画面復帰時に再取得やストリーミング開始を行う - for(column in app_state.column_list) { - column.onStart(this) - } - - te = SystemClock.elapsedRealtime() - if(te - ts >= 100L) log.w("onStart: ${te - ts}ms :column.onStart") - ts = SystemClock.elapsedRealtime() - - // カラムの表示範囲インジケータを更新 - updateColumnStripSelection(- 1, - 1f) - - te = SystemClock.elapsedRealtime() - if(te - ts >= 100L) log.w("onStart: ${te - ts}ms :updateColumnStripSelection") - ts = SystemClock.elapsedRealtime() - - - for(c in app_state.column_list) { - c.fireShowContent(reason = "ActMain onStart", reset = true) - } - - te = SystemClock.elapsedRealtime() - if(te - ts >= 100L) log.w("onStart: ${te - ts}ms :fireShowContent") - - // 相対時刻表示 - proc_updateRelativeTime.run() - - - te = SystemClock.elapsedRealtime() - if(te - tsTotal >= 100L) log.w("onStart: ${te - tsTotal}ms : total") - - app_state.enableSpeech() - } - - override fun onStop() { - - log.d("onStop") - - isStart_ = false - - handler.removeCallbacks(proc_updateRelativeTime) - - post_helper.closeAcctPopup() - - closeListItemPopup() - - app_state.stream_reader.stopAll() - - for(column in app_state.column_list) { - column.saveScrollPosition() - } - app_state.saveColumnList(bEnableSpeech = false) - - super.onStop() - - } - - override fun onResume() { - log.d("onResume") - isResumed = true - - super.onResume() - /* - super.onResume() から呼ばれる isTopOfTask() が android.os.RemoteException 例外をたまに出すが、放置することにした。 - - java.lang.RuntimeException: - at android.app.ActivityThread.performResumeActivity (ActivityThread.java:4430) - at android.app.ActivityThread.handleResumeActivity (ActivityThread.java:4470) - at android.app.servertransaction.TransactionExecutor.performLifecycleSequence (TransactionExecutor.java:183) - at android.app.servertransaction.TransactionExecutor.cycleToPath (TransactionExecutor.java:165) - at android.app.servertransaction.TransactionExecutor.executeLifecycleState (TransactionExecutor.java:142) - at android.app.servertransaction.TransactionExecutor.execute (TransactionExecutor.java:70) - at android.app.ActivityThread$H.handleMessage (ActivityThread.java:2199) - at android.os.Handler.dispatchMessage (Handler.java:112) - at android.os.Looper.loop (Looper.java:216) - at android.app.ActivityThread.main (ActivityThread.java:7625) - at java.lang.reflect.Method.invoke (Native Method) - at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:524) - at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:987) - Caused by: java.lang.IllegalArgumentException: - at android.os.Parcel.createException (Parcel.java:1957) - at android.os.Parcel.readException (Parcel.java:1921) - at android.os.Parcel.readException (Parcel.java:1871) - at android.app.IActivityManager$Stub$Proxy.isTopOfTask (IActivityManager.java:7912) - at android.app.Activity.isTopOfTask (Activity.java:6724) - at android.app.Activity.onResume (Activity.java:1425) - at androidx.fragment.app.FragmentActivity.onResume (FragmentActivity.java:456) - at jp.juggler.subwaytooter.ActMain.onResume (ActMain.kt:685) - at android.app.Instrumentation.callActivityOnResume (Instrumentation.java:1456) - at android.app.Activity.performResume (Activity.java:7614) - at android.app.ActivityThread.performResumeActivity (ActivityThread.java:4412) - Caused by: android.os.RemoteException: - at com.android.server.am.ActivityManagerService.isTopOfTask (ActivityManagerService.java:16128) - at android.app.IActivityManager$Stub.onTransact (IActivityManager.java:2376) - at com.android.server.am.ActivityManagerService.onTransact (ActivityManagerService.java:3648) - at com.android.server.am.HwActivityManagerService.onTransact (HwActivityManagerService.java:609) - at android.os.Binder.execTransact (Binder.java:739) - */ - - MyClickableSpan.link_callback = WeakReference(link_click_listener) - - if(Pref.bpDontScreenOff(pref)) { - window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) - } else { - window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) - } - - // 外部から受け取ったUriの処理 - val uri = ActCallback.last_uri.getAndSet(null) - if(uri != null) { - handleIntentUri(uri) - } - - // 外部から受け取ったUriの処理 - val intent = ActCallback.sent_intent.getAndSet(null) - if(intent != null) { - handleSentIntent(intent) - } - - } - - override fun onPause() { - log.d("onPause") - isResumed = false - - // 最後に表示していたカラムの位置 - val last_pos = phoneTab( + } + } + + override fun onStart() { + val tsTotal = SystemClock.elapsedRealtime() + super.onStart() + + isStart_ = true + log.d("onStart") + + var ts = SystemClock.elapsedRealtime() + var te: Long + + // カラーカスタマイズを読み直す + ListDivider.color = Pref.ipListDividerColor(pref) + TabletColumnDivider.color = Pref.ipListDividerColor(pref) + ItemViewHolder.toot_color_unlisted = Pref.ipTootColorUnlisted(pref) + ItemViewHolder.toot_color_follower = Pref.ipTootColorFollower(pref) + ItemViewHolder.toot_color_direct_user = Pref.ipTootColorDirectUser(pref) + ItemViewHolder.toot_color_direct_me = Pref.ipTootColorDirectMe(pref) + MyClickableSpan.showLinkUnderline = Pref.bpShowLinkUnderline(pref) + MyClickableSpan.defaultLinkColor = Pref.ipLinkColor(pref).notZero() + ?: getAttributeColor(R.attr.colorLink) + + CustomShare.reloadCache(this, pref) + + te = SystemClock.elapsedRealtime() + if (te - ts >= 100L) log.w("onStart: ${te - ts}ms : reload color") + ts = SystemClock.elapsedRealtime() + + var tz = TimeZone.getDefault() + try { + val tz_id = Pref.spTimeZone(pref) + if (tz_id.isNotEmpty()) { + tz = TimeZone.getTimeZone(tz_id) + } + } catch (ex: Throwable) { + log.e(ex, "getTimeZone failed.") + } + TootStatus.date_format.timeZone = tz + + te = SystemClock.elapsedRealtime() + if (te - ts >= 100L) log.w("onStart: ${te - ts}ms : reload timezone") + ts = SystemClock.elapsedRealtime() + + // バグいアカウントデータを消す + try { + SavedAccount.sweepBuggieData() + } catch (ex: Throwable) { + log.trace(ex) + } + + te = SystemClock.elapsedRealtime() + if (te - ts >= 100L) log.w("onStart: ${te - ts}ms : sweepBuggieData") + ts = SystemClock.elapsedRealtime() + + // アカウント設定から戻ってきたら、カラムを消す必要があるかもしれない + val new_order = ArrayList() + + for (i in 0 until app_state.column_list.size) { + val column = app_state.column_list[i] + + if (!column.access_info.isNA) { + // 存在確認 + SavedAccount.loadAccount(this@ActMain, column.access_info.db_id) + ?: continue + } + new_order.add(i) + } + + if (new_order.size != app_state.column_list.size) { + setOrder(new_order) + } + + te = SystemClock.elapsedRealtime() + if (te - ts >= 100L) log.w("onStart: ${te - ts}ms : column order") + ts = SystemClock.elapsedRealtime() + + // 背景画像を表示しない設定が変更された時にカラムの背景を設定しなおす + for (column in app_state.column_list) { + column.viewHolder?.lastAnnouncementShown = 0L + column.fireColumnColor() + } + + te = SystemClock.elapsedRealtime() + if (te - ts >= 100L) log.w("onStart: ${te - ts}ms :fireColumnColor") + ts = SystemClock.elapsedRealtime() + + // 各カラムのアカウント設定を読み直す + reloadAccountSetting() + + te = SystemClock.elapsedRealtime() + if (te - ts >= 100L) log.w("onStart: ${te - ts}ms :reloadAccountSetting") + ts = SystemClock.elapsedRealtime() + + // 投稿直後ならカラムの再取得を行う + refreshAfterPost() + + te = SystemClock.elapsedRealtime() + if (te - ts >= 100L) log.w("onStart: ${te - ts}ms :refreshAfterPost") + ts = SystemClock.elapsedRealtime() + + // 画面復帰時に再取得やストリーミング開始を行う + for (column in app_state.column_list) { + column.onStart(this) + } + + te = SystemClock.elapsedRealtime() + if (te - ts >= 100L) log.w("onStart: ${te - ts}ms :column.onStart") + ts = SystemClock.elapsedRealtime() + + // カラムの表示範囲インジケータを更新 + updateColumnStripSelection(-1, -1f) + + te = SystemClock.elapsedRealtime() + if (te - ts >= 100L) log.w("onStart: ${te - ts}ms :updateColumnStripSelection") + ts = SystemClock.elapsedRealtime() + + + for (c in app_state.column_list) { + c.fireShowContent(reason = "ActMain onStart", reset = true) + } + + te = SystemClock.elapsedRealtime() + if (te - ts >= 100L) log.w("onStart: ${te - ts}ms :fireShowContent") + + // 相対時刻表示 + proc_updateRelativeTime.run() + + + te = SystemClock.elapsedRealtime() + if (te - tsTotal >= 100L) log.w("onStart: ${te - tsTotal}ms : total") + + app_state.enableSpeech() + } + + override fun onStop() { + + log.d("onStop") + + isStart_ = false + + handler.removeCallbacks(proc_updateRelativeTime) + + post_helper.closeAcctPopup() + + closeListItemPopup() + + app_state.stream_reader.stopAll() + + for (column in app_state.column_list) { + column.saveScrollPosition() + } + app_state.saveColumnList(bEnableSpeech = false) + + super.onStop() + + } + + override fun onResume() { + log.d("onResume") + isResumed = true + + super.onResume() + /* + super.onResume() から呼ばれる isTopOfTask() が android.os.RemoteException 例外をたまに出すが、放置することにした。 + + java.lang.RuntimeException: + at android.app.ActivityThread.performResumeActivity (ActivityThread.java:4430) + at android.app.ActivityThread.handleResumeActivity (ActivityThread.java:4470) + at android.app.servertransaction.TransactionExecutor.performLifecycleSequence (TransactionExecutor.java:183) + at android.app.servertransaction.TransactionExecutor.cycleToPath (TransactionExecutor.java:165) + at android.app.servertransaction.TransactionExecutor.executeLifecycleState (TransactionExecutor.java:142) + at android.app.servertransaction.TransactionExecutor.execute (TransactionExecutor.java:70) + at android.app.ActivityThread$H.handleMessage (ActivityThread.java:2199) + at android.os.Handler.dispatchMessage (Handler.java:112) + at android.os.Looper.loop (Looper.java:216) + at android.app.ActivityThread.main (ActivityThread.java:7625) + at java.lang.reflect.Method.invoke (Native Method) + at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:524) + at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:987) + Caused by: java.lang.IllegalArgumentException: + at android.os.Parcel.createException (Parcel.java:1957) + at android.os.Parcel.readException (Parcel.java:1921) + at android.os.Parcel.readException (Parcel.java:1871) + at android.app.IActivityManager$Stub$Proxy.isTopOfTask (IActivityManager.java:7912) + at android.app.Activity.isTopOfTask (Activity.java:6724) + at android.app.Activity.onResume (Activity.java:1425) + at androidx.fragment.app.FragmentActivity.onResume (FragmentActivity.java:456) + at jp.juggler.subwaytooter.ActMain.onResume (ActMain.kt:685) + at android.app.Instrumentation.callActivityOnResume (Instrumentation.java:1456) + at android.app.Activity.performResume (Activity.java:7614) + at android.app.ActivityThread.performResumeActivity (ActivityThread.java:4412) + Caused by: android.os.RemoteException: + at com.android.server.am.ActivityManagerService.isTopOfTask (ActivityManagerService.java:16128) + at android.app.IActivityManager$Stub.onTransact (IActivityManager.java:2376) + at com.android.server.am.ActivityManagerService.onTransact (ActivityManagerService.java:3648) + at com.android.server.am.HwActivityManagerService.onTransact (HwActivityManagerService.java:609) + at android.os.Binder.execTransact (Binder.java:739) + */ + + MyClickableSpan.link_callback = WeakReference(link_click_listener) + + if (Pref.bpDontScreenOff(pref)) { + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } else { + window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } + + // 外部から受け取ったUriの処理 + val uri = ActCallback.last_uri.getAndSet(null) + if (uri != null) { + handleIntentUri(uri) + } + + // 外部から受け取ったUriの処理 + val intent = ActCallback.sent_intent.getAndSet(null) + if (intent != null) { + handleSentIntent(intent) + } + + } + + override fun onPause() { + log.d("onPause") + isResumed = false + + // 最後に表示していたカラムの位置 + val last_pos = phoneTab( { env -> env.pager.currentItem }, { env -> env.visibleColumnsIndices.first }) - log.d("ipLastColumnPos save $last_pos") - pref.edit().put(Pref.ipLastColumnPos, last_pos).apply() - - for(column in app_state.column_list) { - column.saveScrollPosition() - } - - app_state.saveColumnList(bEnableSpeech = false) - - super.onPause() - } - - ////////////////////////////////////////////////////////////////// - // UIイベント - - override fun onPageScrollStateChanged(state : Int) { - } - - override fun onPageScrolled( - position : Int, - positionOffset : Float, - positionOffsetPixels : Int + log.d("ipLastColumnPos save $last_pos") + pref.edit().put(Pref.ipLastColumnPos, last_pos).apply() + + for (column in app_state.column_list) { + column.saveScrollPosition() + } + + app_state.saveColumnList(bEnableSpeech = false) + + super.onPause() + } + + ////////////////////////////////////////////////////////////////// + // UIイベント + + override fun onPageScrollStateChanged(state: Int) { + } + + override fun onPageScrolled( + position: Int, + positionOffset: Float, + positionOffsetPixels: Int ) { - updateColumnStripSelection(position, positionOffset) - } - - override fun onPageSelected(position : Int) { - handler.post { - if(position >= 0 && position < app_state.column_list.size) { - val column = app_state.column_list[position] - if(! column.bFirstInitialized) { - column.startLoading() - } - scrollColumnStrip(position) - post_helper.setInstance( + updateColumnStripSelection(position, positionOffset) + } + + override fun onPageSelected(position: Int) { + handler.post { + if (position >= 0 && position < app_state.column_list.size) { + val column = app_state.column_list[position] + if (!column.bFirstInitialized) { + column.startLoading() + } + scrollColumnStrip(position) + post_helper.setInstance( when { column.access_info.isNA -> null else -> column.access_info } ) - } - } - } - - override fun onClick(v : View) { - when(v.id) { - R.id.btnMenu -> if(! drawer.isDrawerOpen(GravityCompat.START)) { + } + } + } + + override fun onClick(v: View) { + when (v.id) { + R.id.btnMenu -> if (!drawer.isDrawerOpen(GravityCompat.START)) { drawer.openDrawer(GravityCompat.START) } - - R.id.btnToot -> Action_Account.openPost(this@ActMain) - + + R.id.btnToot -> Action_Account.openPost(this) R.id.btnQuickToot -> performQuickPost(null) - R.id.btnQuickTootMenu -> performQuickTootMenu() - } - } - + } + } + //////////////////////////////////////////////////////////////////// - - // スマホモードとタブレットモードでコードを切り替える - private inline fun phoneTab( - codePhone : (PhoneEnv) -> R, - codeTablet : (TabletEnv) -> R - ) : R { - - val pe = phoneEnv - if(pe != null) return codePhone(pe) - - val te = tabletEnv - if(te != null) return codeTablet(te) - - throw RuntimeException("missing phoneEnv or tabletEnv") + + // スマホモードとタブレットモードでコードを切り替える + private inline fun phoneTab( + codePhone: (PhoneEnv) -> R, + codeTablet: (TabletEnv) -> R + ): R { + + val pe = phoneEnv + if (pe != null) return codePhone(pe) + + val te = tabletEnv + if (te != null) return codeTablet(te) + + throw RuntimeException("missing phoneEnv or tabletEnv") + } + + // スマホモードならラムダを実行する。タブレットモードならnullを返す + private inline fun phoneOnly(code: (PhoneEnv) -> R): R? { + val pe = phoneEnv + return if (pe != null) code(pe) else null + } + + // タブレットモードならラムダを実行する。スマホモードならnullを返す + @Suppress("unused") + private inline fun tabOnly(code: (TabletEnv) -> R): R? { + val te = tabletEnv + return if (te != null) code(te) else null + } + + // 新しいカラムをどこに挿入するか + // カラムの次の位置か、現在のページの次の位置か、終端 + fun nextPosition(column: Column?): Int { + if (column != null) { + val pos = app_state.column_list.indexOf(column) + if (pos != -1) return pos + 1 + } + return defaultInsertPosition + } + + private fun showQuickTootVisibility() { + btnQuickTootMenu.imageResource = + when (val resId = Styler.getVisibilityIconId(false, quickTootVisibility)) { + R.drawable.ic_question -> R.drawable.ic_description + else -> resId + } } - - // スマホモードならラムダを実行する。タブレットモードならnullを返す - private inline fun phoneOnly(code : (PhoneEnv) -> R) : R? { - val pe = phoneEnv - return if(pe != null) code(pe) else null + + private fun performQuickTootMenu() { + dlgQuickTootMenu.toggle() } - - // タブレットモードならラムダを実行する。スマホモードならnullを返す - @Suppress("unused") - private inline fun tabOnly(code : (TabletEnv) -> R) : R? { - val te = tabletEnv - return if(te != null) code(te) else null - } - - // 新しいカラムをどこに挿入するか - // カラムの次の位置か、現在のページの次の位置か、終端 - fun nextPosition(column : Column?) : Int { - if(column != null) { - val pos = app_state.column_list.indexOf(column) - if(pos != - 1) return pos + 1 - } - return defaultInsertPosition - } - + private fun refreshAfterPost() { - val posted_acct = this.posted_acct - val posted_status_id = this.posted_status_id - - if(posted_acct != null && posted_status_id == null) { - // 予約投稿なら予約投稿リストをリロードする - for(column in app_state.column_list) { - if(column.type == ColumnType.SCHEDULED_STATUS - && column.access_info.acct == posted_acct - ) { - column.startLoading() - } - } - - } else if(posted_acct != null && posted_status_id != null) { - - val posted_redraft_id = this.posted_redraft_id - if(posted_redraft_id != null) { - val host = posted_acct.host - if(host != null) { - for(column in app_state.column_list) { - column.onStatusRemoved(host, posted_redraft_id) - } - } - this.posted_redraft_id = null - } - - val refresh_after_toot = Pref.ipRefreshAfterToot(pref) - if(refresh_after_toot != Pref.RAT_DONT_REFRESH) { - for(column in app_state.column_list) { - if(column.access_info.acct != posted_acct) continue - column.startRefreshForPost( + val posted_acct = this.posted_acct + val posted_status_id = this.posted_status_id + + if (posted_acct != null && posted_status_id == null) { + // 予約投稿なら予約投稿リストをリロードする + for (column in app_state.column_list) { + if (column.type == ColumnType.SCHEDULED_STATUS + && column.access_info.acct == posted_acct + ) { + column.startLoading() + } + } + + } else if (posted_acct != null && posted_status_id != null) { + + val posted_redraft_id = this.posted_redraft_id + if (posted_redraft_id != null) { + val host = posted_acct.host + if (host != null) { + for (column in app_state.column_list) { + column.onStatusRemoved(host, posted_redraft_id) + } + } + this.posted_redraft_id = null + } + + val refresh_after_toot = Pref.ipRefreshAfterToot(pref) + if (refresh_after_toot != Pref.RAT_DONT_REFRESH) { + for (column in app_state.column_list) { + if (column.access_info.acct != posted_acct) continue + column.startRefreshForPost( refresh_after_toot, posted_status_id, posted_reply_id ) - } - } - } - this.posted_acct = null - this.posted_status_id = null - } - - private fun handleSentIntent(intent : Intent) { - sent_intent2 = intent - - // Galaxy S8+ で STのSSを取った後に出るポップアップからそのまま共有でSTを選ぶと何も起きない問題への対策 - handler.post { - AccountPicker.pick( + } + } + } + this.posted_acct = null + this.posted_status_id = null + } + + private fun handleSentIntent(intent: Intent) { + sent_intent2 = intent + + // Galaxy S8+ で STのSSを取った後に出るポップアップからそのまま共有でSTを選ぶと何も起きない問題への対策 + handler.post { + AccountPicker.pick( this, bAllowPseudo = false, bAuto = true, message = getString(R.string.account_picker_toot), dismiss_callback = { sent_intent2 = null } ) { ai -> - sent_intent2 = null - ActPost.open(this@ActMain, REQUEST_CODE_POST, ai.db_id, sent_intent = intent) - } - } - } - - fun closeListItemPopup() { - try { - listItemPopup?.dismiss() - } catch(ignored : Throwable) { - } - listItemPopup = null - } - - private fun performQuickPost(account : SavedAccount?) { - if(account == null) { - val a = if(tabletEnv != null && ! Pref.bpQuickTootOmitAccountSelection(pref)) { - // タブレットモードでオプションが無効なら - // 簡易投稿は常にアカウント選択する - null - } else { - currentPostTarget - } - - if(a != null && ! a.isPseudo) { - performQuickPost(a) - } else { - // アカウントを選択してやり直し - AccountPicker.pick( + sent_intent2 = null + ActPost.open(this@ActMain, REQUEST_CODE_POST, ai.db_id, sent_intent = intent) + } + } + } + + fun closeListItemPopup() { + try { + listItemPopup?.dismiss() + } catch (ignored: Throwable) { + } + listItemPopup = null + } + + private fun performQuickPost(account: SavedAccount?) { + if (account == null) { + val a = if (tabletEnv != null && !Pref.bpQuickTootOmitAccountSelection(pref)) { + // タブレットモードでオプションが無効なら + // 簡易投稿は常にアカウント選択する + null + } else { + currentPostTarget + } + + if (a != null && !a.isPseudo) { + performQuickPost(a) + } else { + // アカウントを選択してやり直し + AccountPicker.pick( this, bAllowPseudo = false, bAuto = true, message = getString(R.string.account_picker_toot) ) { ai -> performQuickPost(ai) } - } - return - } - - post_helper.content = etQuickToot.text.toString().trim { it <= ' ' } - post_helper.spoiler_text = null - - post_helper.visibility = when(quickTootVisibility) { + } + return + } + + post_helper.content = etQuickToot.text.toString().trim { it <= ' ' } + post_helper.spoiler_text = null + + post_helper.visibility = when (quickTootVisibility) { TootVisibility.AccountSetting -> account.visibility - else -> quickTootVisibility - } - - post_helper.bNSFW = false - post_helper.in_reply_to_id = null - post_helper.attachment_list = null - post_helper.emojiMapCustom = - App1.custom_emoji_lister.getMap(account) - - - etQuickToot.hideKeyboard() - - post_helper.post(account, callback = object : PostHelper.PostCompleteCallback { + else -> quickTootVisibility + } + + post_helper.bNSFW = false + post_helper.in_reply_to_id = null + post_helper.attachment_list = null + post_helper.emojiMapCustom = + App1.custom_emoji_lister.getMap(account) + + + etQuickToot.hideKeyboard() + + post_helper.post(account, callback = object : PostHelper.PostCompleteCallback { override fun onPostComplete( - target_account : SavedAccount, - status : TootStatus + target_account: SavedAccount, + status: TootStatus ) { etQuickToot.setText("") posted_acct = target_account.acct @@ -995,45 +988,45 @@ class ActMain : AsyncActivity(), Column.Callback, View.OnClickListener, posted_redraft_id = null refreshAfterPost() } - - override fun onScheduledPostComplete(target_account : SavedAccount) { + + override fun onScheduledPostComplete(target_account: SavedAccount) { } }) - } - - private fun isOrderChanged(new_order : ArrayList) : Boolean { - if(new_order.size != app_state.column_list.size) return true - var i = 0 - val ie = new_order.size - while(i < ie) { - if(new_order[i] != i) return true - ++ i - } - return false - } - - override fun onActivityResult(requestCode : Int, resultCode : Int, data : Intent?) { - log.d("onActivityResult req=$requestCode res=$resultCode data=$data") - - if(resultCode == Activity.RESULT_OK) { - when(requestCode) { - REQUEST_CODE_COLUMN_LIST -> if(data != null) { + } + + private fun isOrderChanged(new_order: ArrayList): Boolean { + if (new_order.size != app_state.column_list.size) return true + var i = 0 + val ie = new_order.size + while (i < ie) { + if (new_order[i] != i) return true + ++i + } + return false + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + log.d("onActivityResult req=$requestCode res=$resultCode data=$data") + + if (resultCode == Activity.RESULT_OK) { + when (requestCode) { + REQUEST_CODE_COLUMN_LIST -> if (data != null) { val order = data.getIntegerArrayListExtra(ActColumnList.EXTRA_ORDER) - if(order != null && isOrderChanged(order)) { + if (order != null && isOrderChanged(order)) { setOrder(order) } - - if(app_state.column_list.isNotEmpty()) { - val select = data.getIntExtra(ActColumnList.EXTRA_SELECTION, - 1) - if(0 <= select && select < app_state.column_list.size) { + + if (app_state.column_list.isNotEmpty()) { + val select = data.getIntExtra(ActColumnList.EXTRA_SELECTION, -1) + if (0 <= select && select < app_state.column_list.size) { scrollToColumn(select) } } } - - REQUEST_APP_ABOUT -> if(data != null) { + + REQUEST_APP_ABOUT -> if (data != null) { val search = data.getStringExtra(ActAbout.EXTRA_SEARCH) - if(search?.isNotEmpty() == true) { + if (search?.isNotEmpty() == true) { Action_Account.timeline( this@ActMain, defaultInsertPosition, @@ -1043,22 +1036,22 @@ class ActMain : AsyncActivity(), Column.Callback, View.OnClickListener, } return } - + REQUEST_CODE_NICKNAME -> { - + updateColumnStrip() - - for(column in app_state.column_list) { + + for (column in app_state.column_list) { column.fireShowColumnHeader() } - + } - - REQUEST_CODE_POST -> if(data != null) { + + REQUEST_CODE_POST -> if (data != null) { etQuickToot.setText("") posted_acct = data.getStringExtra(ActPost.EXTRA_POSTED_ACCT)?.let { Acct.parse(it) } - if(data.extras?.containsKey(ActPost.EXTRA_POSTED_STATUS_ID) == true) { + if (data.extras?.containsKey(ActPost.EXTRA_POSTED_STATUS_ID) == true) { posted_status_id = EntityId.from(data, ActPost.EXTRA_POSTED_STATUS_ID) posted_reply_id = EntityId.from(data, ActPost.EXTRA_POSTED_REPLY_ID) posted_redraft_id = EntityId.from(data, ActPost.EXTRA_POSTED_REDRAFT_ID) @@ -1066,11 +1059,11 @@ class ActMain : AsyncActivity(), Column.Callback, View.OnClickListener, posted_status_id = null } } - - REQUEST_CODE_COLUMN_COLOR -> if(data != null) { + + REQUEST_CODE_COLUMN_COLOR -> if (data != null) { app_state.saveColumnList() val idx = data.getIntExtra(ActColumnCustomize.EXTRA_COLUMN_INDEX, 0) - if(idx in app_state.column_list.indices) { + if (idx in app_state.column_list.indices) { app_state.column_list[idx].fireColumnColor() app_state.column_list[idx].fireShowContent( reason = "ActMain column color changed", @@ -1079,130 +1072,100 @@ class ActMain : AsyncActivity(), Column.Callback, View.OnClickListener, } updateColumnStrip() } - - REQUEST_CODE_LANGUAGE_FILTER -> if(data != null) { + + REQUEST_CODE_LANGUAGE_FILTER -> if (data != null) { app_state.saveColumnList() val idx = data.getIntExtra(ActLanguageFilter.EXTRA_COLUMN_INDEX, 0) - if(idx in app_state.column_list.indices) { + if (idx in app_state.column_list.indices) { app_state.column_list[idx].onLanguageFilterChanged() } } - } - } - - when(requestCode) { - + } + } + + when (requestCode) { + REQUEST_CODE_ACCOUNT_SETTING -> { updateColumnStrip() - - for(column in app_state.column_list) { - column.fireShowColumnHeader() - } - - if(resultCode == Activity.RESULT_OK && data != null) { - openBrowser(data.data) - } else if(resultCode == ActAccountSetting.RESULT_INPUT_ACCESS_TOKEN && data != null) { - val db_id = data.getLongExtra(ActAccountSetting.EXTRA_DB_ID, - 1L) - checkAccessToken2(db_id) + + app_state.column_list.forEach { it.fireShowColumnHeader() } + + when (resultCode) { + RESULT_OK -> data?.data?.let { openBrowser(it) } + + ActAccountSetting.RESULT_INPUT_ACCESS_TOKEN -> + data?.getLongExtra(ActAccountSetting.EXTRA_DB_ID, -1L) + ?.takeIf { it != -1L }?.let { checkAccessToken2(it) } } } - + REQUEST_CODE_APP_SETTING -> { Column.reloadDefaultColor(this, pref) showFooterColor() updateColumnStrip() - - if(resultCode == RESULT_APP_DATA_IMPORT) { - importAppData(data?.data) - } - } - - REQUEST_CODE_TEXT -> when(resultCode) { - ActText.RESULT_SEARCH_MSP -> { - val text = data?.getStringExtra(Intent.EXTRA_TEXT) ?: "" - addColumn( - false, - defaultInsertPosition, - SavedAccount.na, - ColumnType.SEARCH_MSP, - text - ) - } - - ActText.RESULT_SEARCH_TS -> { - val text = data?.getStringExtra(Intent.EXTRA_TEXT) ?: "" - addColumn( - false, - defaultInsertPosition, - SavedAccount.na, - ColumnType.SEARCH_TS, - text - ) - } - ActText.RESULT_SEARCH_NOTESTOCK -> { - val text = data?.getStringExtra(Intent.EXTRA_TEXT) ?: "" - addColumn( - false, - defaultInsertPosition, - SavedAccount.na, - ColumnType.SEARCH_NOTESTOCK, - text - ) + if (resultCode == RESULT_APP_DATA_IMPORT) { + data?.data?.let { importAppData(it) } } } - } - - super.onActivityResult(requestCode, resultCode, data) - } - - override fun onBackPressed() { - - // メニューが開いていたら閉じる - if(drawer.isDrawerOpen(GravityCompat.START)) { - drawer.closeDrawer(GravityCompat.START) - return - } - - // カラムが0個ならアプリを終了する - if(app_state.column_list.isEmpty()) { - this@ActMain.finish() - return - } - - // カラム設定が開いているならカラム設定を閉じる - if(closeColumnSetting()) { - return - } - - fun getClosableColumnList() : List { - val visibleColumnList = ArrayList() - phoneTab({ env -> + + REQUEST_CODE_TEXT -> when (resultCode) { + ActText.RESULT_SEARCH_MSP -> searchFromActivityResult(data, ColumnType.SEARCH_MSP) + ActText.RESULT_SEARCH_TS -> searchFromActivityResult(data, ColumnType.SEARCH_TS) + ActText.RESULT_SEARCH_NOTESTOCK -> searchFromActivityResult(data, ColumnType.SEARCH_NOTESTOCK) + } + } + + super.onActivityResult(requestCode, resultCode, data) + } + + override fun onBackPressed() { + + // メニューが開いていたら閉じる + if (drawer.isDrawerOpen(GravityCompat.START)) { + drawer.closeDrawer(GravityCompat.START) + return + } + + // カラムが0個ならアプリを終了する + if (app_state.column_list.isEmpty()) { + this@ActMain.finish() + return + } + + // カラム設定が開いているならカラム設定を閉じる + if (closeColumnSetting()) { + return + } + + fun getClosableColumnList(): List { + val visibleColumnList = ArrayList() + phoneTab({ env -> try { visibleColumnList.add(app_state.column_list[env.pager.currentItem]) - } catch(ex : Throwable) { + } catch (ex: Throwable) { } }, { env -> visibleColumnList.addAll(env.visibleColumns) }) - - return visibleColumnList.filter { ! it.dont_close } - - } - - // カラムが1個以上ある場合は設定に合わせて挙動を変える - when(Pref.ipBackButtonAction(pref)) { - + + return visibleColumnList.filter { !it.dont_close } + + } + + // カラムが1個以上ある場合は設定に合わせて挙動を変える + when (Pref.ipBackButtonAction(pref)) { + Pref.BACK_EXIT_APP -> this@ActMain.finish() - + Pref.BACK_OPEN_COLUMN_LIST -> Action_App.columnList(this@ActMain) - + Pref.BACK_CLOSE_COLUMN -> { - + val closeableColumnList = getClosableColumnList() - when(closeableColumnList.size) { + when (closeableColumnList.size) { 0 -> { - if(Pref.bpExitAppWhenCloseProtectedColumn(pref) + if (Pref.bpExitAppWhenCloseProtectedColumn(pref) && Pref.bpDontConfirmBeforeCloseColumn(pref) ) { this@ActMain.finish() @@ -1210,11 +1173,11 @@ class ActMain : AsyncActivity(), Column.Callback, View.OnClickListener, showToast(false, R.string.missing_closeable_column) } } - + 1 -> { closeColumn(closeableColumnList.first()) } - + else -> { showToast( false, @@ -1223,158 +1186,158 @@ class ActMain : AsyncActivity(), Column.Callback, View.OnClickListener, } } } - - // ActAppSetting.BACK_ASK_ALWAYS - else -> { - - val closeableColumnList = getClosableColumnList() - - val dialog = ActionsDialog() - - - if(closeableColumnList.size == 1) { - val column = closeableColumnList.first() - dialog.addAction(getString(R.string.close_column)) { - closeColumn(column, bConfirmed = true) - } - } - - dialog.addAction(getString(R.string.open_column_list)) { Action_App.columnList(this@ActMain) } - dialog.addAction(getString(R.string.app_exit)) { this@ActMain.finish() } - dialog.show(this, null) - } - } - } - - internal fun initUI() { - setContentView(R.layout.act_main) - App1.initEdgeToEdge(this) - - quickTootVisibility = - TootVisibility.parseSavedVisibility(Pref.spQuickTootVisibility(pref)) - ?: quickTootVisibility - - - Column.reloadDefaultColor(this, pref) - - var sv = Pref.spTimelineFont(pref) - if(sv.isNotEmpty()) { - try { - timeline_font = Typeface.createFromFile(sv) - } catch(ex : Throwable) { - log.trace(ex) - } - } - - sv = Pref.spTimelineFontBold(pref) - if(sv.isNotEmpty()) { - try { - timeline_font_bold = Typeface.createFromFile(sv) - } catch(ex : Throwable) { - log.trace(ex) - } - - } else { - try { - timeline_font_bold = Typeface.create(timeline_font, Typeface.BOLD) - } catch(ex : Throwable) { - log.trace(ex) - } - } - - fun parseIconSize(stringPref : StringPref, minDp : Float = 1f) : Int { - var icon_size_dp = stringPref.defVal.toFloat() - try { - sv = stringPref(pref) - val fv = if(sv.isEmpty()) Float.NaN else sv.toFloat() - if(fv.isFinite() && fv >= minDp) { - icon_size_dp = fv - } - } catch(ex : Throwable) { - log.trace(ex) - } - return (0.5f + icon_size_dp * density).toInt() - } - - avatarIconSize = parseIconSize(Pref.spAvatarIconSize) - notificationTlIconSize = parseIconSize(Pref.spNotificationTlIconSize) - boostButtonSize = parseIconSize(Pref.spBoostButtonSize) - replyIconSize = parseIconSize(Pref.spReplyIconSize) - headerIconSize = parseIconSize(Pref.spHeaderIconSize) - stripIconSize = parseIconSize(Pref.spStripIconSize) - screenBottomPadding = parseIconSize(Pref.spScreenBottomPadding, minDp = 0f) - - run { - var round_ratio = 33f - try { - if(Pref.bpDontRound(pref)) { - round_ratio = 0f - } else { - sv = Pref.spRoundRatio(pref) - if(sv.isNotEmpty()) { - val fv = sv.toFloat() - if(fv.isFinite()) { - round_ratio = fv - } - } - } - } catch(ex : Throwable) { - log.trace(ex) - } - Styler.round_ratio = clipRange(0f, 1f, round_ratio / 100f) * 0.5f - } - - run { - var boost_alpha = 0.8f - try { - val f = (Pref.spBoostAlpha.toInt(pref).toFloat() + 0.5f) / 100f - boost_alpha = when { - f >= 1f -> 1f - f < 0f -> 0.66f - else -> f - } - } catch(ex : Throwable) { - log.trace(ex) - } - Styler.boost_alpha = boost_alpha - } - - llEmpty = findViewById(R.id.llEmpty) - - drawer = findViewById(R.id.drawer_layout) - drawer.addDrawerListener(this) - - drawer.setExclusionSize(stripIconSize) - - - - - - SideMenuAdapter(this, handler, findViewById(R.id.nav_view), drawer) - - btnMenu = findViewById(R.id.btnMenu) - btnToot = findViewById(R.id.btnToot) - vFooterDivider1 = findViewById(R.id.vFooterDivider1) - vFooterDivider2 = findViewById(R.id.vFooterDivider2) - llColumnStrip = findViewById(R.id.llColumnStrip) - svColumnStrip = findViewById(R.id.svColumnStrip) - llQuickTootBar = findViewById(R.id.llQuickTootBar) - etQuickToot = findViewById(R.id.etQuickToot) - btnQuickToot = findViewById(R.id.btnQuickToot) - btnQuickTootMenu = findViewById(R.id.btnQuickTootMenu) - - val llFormRoot : LinearLayout = findViewById(R.id.llFormRoot) - - llFormRoot.setPadding(0, 0, 0, screenBottomPadding) - - etQuickToot.typeface = timeline_font - - when(Pref.ipJustifyWindowContentPortrait(pref)) { + + // ActAppSetting.BACK_ASK_ALWAYS + else -> { + + val closeableColumnList = getClosableColumnList() + + val dialog = ActionsDialog() + + + if (closeableColumnList.size == 1) { + val column = closeableColumnList.first() + dialog.addAction(getString(R.string.close_column)) { + closeColumn(column, bConfirmed = true) + } + } + + dialog.addAction(getString(R.string.open_column_list)) { Action_App.columnList(this@ActMain) } + dialog.addAction(getString(R.string.app_exit)) { this@ActMain.finish() } + dialog.show(this, null) + } + } + } + + internal fun initUI() { + setContentView(R.layout.act_main) + App1.initEdgeToEdge(this) + + quickTootVisibility = + TootVisibility.parseSavedVisibility(Pref.spQuickTootVisibility(pref)) + ?: quickTootVisibility + + + Column.reloadDefaultColor(this, pref) + + var sv = Pref.spTimelineFont(pref) + if (sv.isNotEmpty()) { + try { + timeline_font = Typeface.createFromFile(sv) + } catch (ex: Throwable) { + log.trace(ex) + } + } + + sv = Pref.spTimelineFontBold(pref) + if (sv.isNotEmpty()) { + try { + timeline_font_bold = Typeface.createFromFile(sv) + } catch (ex: Throwable) { + log.trace(ex) + } + + } else { + try { + timeline_font_bold = Typeface.create(timeline_font, Typeface.BOLD) + } catch (ex: Throwable) { + log.trace(ex) + } + } + + fun parseIconSize(stringPref: StringPref, minDp: Float = 1f): Int { + var icon_size_dp = stringPref.defVal.toFloat() + try { + sv = stringPref(pref) + val fv = if (sv.isEmpty()) Float.NaN else sv.toFloat() + if (fv.isFinite() && fv >= minDp) { + icon_size_dp = fv + } + } catch (ex: Throwable) { + log.trace(ex) + } + return (0.5f + icon_size_dp * density).toInt() + } + + avatarIconSize = parseIconSize(Pref.spAvatarIconSize) + notificationTlIconSize = parseIconSize(Pref.spNotificationTlIconSize) + boostButtonSize = parseIconSize(Pref.spBoostButtonSize) + replyIconSize = parseIconSize(Pref.spReplyIconSize) + headerIconSize = parseIconSize(Pref.spHeaderIconSize) + stripIconSize = parseIconSize(Pref.spStripIconSize) + screenBottomPadding = parseIconSize(Pref.spScreenBottomPadding, minDp = 0f) + + run { + var round_ratio = 33f + try { + if (Pref.bpDontRound(pref)) { + round_ratio = 0f + } else { + sv = Pref.spRoundRatio(pref) + if (sv.isNotEmpty()) { + val fv = sv.toFloat() + if (fv.isFinite()) { + round_ratio = fv + } + } + } + } catch (ex: Throwable) { + log.trace(ex) + } + Styler.round_ratio = clipRange(0f, 1f, round_ratio / 100f) * 0.5f + } + + run { + var boost_alpha = 0.8f + try { + val f = (Pref.spBoostAlpha.toInt(pref).toFloat() + 0.5f) / 100f + boost_alpha = when { + f >= 1f -> 1f + f < 0f -> 0.66f + else -> f + } + } catch (ex: Throwable) { + log.trace(ex) + } + Styler.boost_alpha = boost_alpha + } + + llEmpty = findViewById(R.id.llEmpty) + + drawer = findViewById(R.id.drawer_layout) + drawer.addDrawerListener(this) + + drawer.setExclusionSize(stripIconSize) + + + + + + SideMenuAdapter(this, handler, findViewById(R.id.nav_view), drawer) + + btnMenu = findViewById(R.id.btnMenu) + btnToot = findViewById(R.id.btnToot) + vFooterDivider1 = findViewById(R.id.vFooterDivider1) + vFooterDivider2 = findViewById(R.id.vFooterDivider2) + llColumnStrip = findViewById(R.id.llColumnStrip) + svColumnStrip = findViewById(R.id.svColumnStrip) + llQuickTootBar = findViewById(R.id.llQuickTootBar) + etQuickToot = findViewById(R.id.etQuickToot) + btnQuickToot = findViewById(R.id.btnQuickToot) + btnQuickTootMenu = findViewById(R.id.btnQuickTootMenu) + + val llFormRoot: LinearLayout = findViewById(R.id.llFormRoot) + + llFormRoot.setPadding(0, 0, 0, screenBottomPadding) + + etQuickToot.typeface = timeline_font + + when (Pref.ipJustifyWindowContentPortrait(pref)) { Pref.JWCP_START -> { val iconW = (stripIconSize * 1.5f + 0.5f).toInt() val padding = resources.displayMetrics.widthPixels / 2 - iconW - - fun ViewGroup.addViewBeforeLast(v : View) = addView(v, childCount - 1) + + fun ViewGroup.addViewBeforeLast(v: View) = addView(v, childCount - 1) (svColumnStrip.parent as LinearLayout).addViewBeforeLast( View(this).apply { layoutParams = LinearLayout.LayoutParams(padding, 0) @@ -1386,13 +1349,13 @@ class ActMain : AsyncActivity(), Column.Callback, View.OnClickListener, } ) } - + Pref.JWCP_END -> { val iconW = (stripIconSize * 1.5f + 0.5f).toInt() val borderWidth = (1f * density + 0.5f).toInt() val padding = resources.displayMetrics.widthPixels / 2 - iconW - borderWidth - - fun ViewGroup.addViewAfterFirst(v : View) = addView(v, 1) + + fun ViewGroup.addViewAfterFirst(v: View) = addView(v, 1) (svColumnStrip.parent as LinearLayout).addViewAfterFirst( View(this).apply { layoutParams = LinearLayout.LayoutParams(padding, 0) @@ -1404,99 +1367,99 @@ class ActMain : AsyncActivity(), Column.Callback, View.OnClickListener, } ) } - } - - if(! Pref.bpQuickTootBar(pref)) { - llQuickTootBar.visibility = View.GONE - } - - btnToot.setOnClickListener(this) - btnMenu.setOnClickListener(this) - btnQuickToot.setOnClickListener(this) - btnQuickTootMenu.setOnClickListener(this) - - if(Pref.bpDontUseActionButtonWithQuickTootBar(pref)) { - etQuickToot.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_MULTI_LINE - etQuickToot.imeOptions = EditorInfo.IME_ACTION_NONE - // 最後に指定する必要がある? - etQuickToot.maxLines = 5 - etQuickToot.isVerticalScrollBarEnabled = true - etQuickToot.isScrollbarFadingEnabled = false - } else { - etQuickToot.inputType = InputType.TYPE_CLASS_TEXT - etQuickToot.imeOptions = EditorInfo.IME_ACTION_SEND - etQuickToot.setOnEditorActionListener(TextView.OnEditorActionListener { _, actionId, _ -> - if(actionId == EditorInfo.IME_ACTION_SEND) { + } + + if (!Pref.bpQuickTootBar(pref)) { + llQuickTootBar.visibility = View.GONE + } + + btnToot.setOnClickListener(this) + btnMenu.setOnClickListener(this) + btnQuickToot.setOnClickListener(this) + btnQuickTootMenu.setOnClickListener(this) + + if (Pref.bpDontUseActionButtonWithQuickTootBar(pref)) { + etQuickToot.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_MULTI_LINE + etQuickToot.imeOptions = EditorInfo.IME_ACTION_NONE + // 最後に指定する必要がある? + etQuickToot.maxLines = 5 + etQuickToot.isVerticalScrollBarEnabled = true + etQuickToot.isScrollbarFadingEnabled = false + } else { + etQuickToot.inputType = InputType.TYPE_CLASS_TEXT + etQuickToot.imeOptions = EditorInfo.IME_ACTION_SEND + etQuickToot.setOnEditorActionListener(TextView.OnEditorActionListener { _, actionId, _ -> + if (actionId == EditorInfo.IME_ACTION_SEND) { btnQuickToot.performClick() return@OnEditorActionListener true } false }) - // 最後に指定する必要がある? - etQuickToot.maxLines = 1 - } - - svColumnStrip.isHorizontalFadingEdgeEnabled = true - - post_helper = PostHelper(this, pref, app_state.handler) - - val dm = resources.displayMetrics - - val density = dm.density - - var media_thumb_height = 64 - sv = Pref.spMediaThumbHeight(pref) - if(sv.isNotEmpty()) { - try { - val iv = Integer.parseInt(sv) - if(iv >= 32) { - media_thumb_height = iv - } - } catch(ex : Throwable) { - log.trace(ex) - } - - } - app_state.media_thumb_height = (0.5f + media_thumb_height * density).toInt() - - var column_w_min_dp = COLUMN_WIDTH_MIN_DP - sv = Pref.spColumnWidth(pref) - if(sv.isNotEmpty()) { - try { - val iv = Integer.parseInt(sv) - if(iv >= 100) { - column_w_min_dp = iv - } - } catch(ex : Throwable) { - log.trace(ex) - } - - } - val column_w_min = (0.5f + column_w_min_dp * density).toInt() - - val sw = dm.widthPixels - - - if(Pref.bpDisableTabletMode(pref) || sw < column_w_min * 2) { - // SmartPhone mode - phoneEnv = PhoneEnv() - } else { - // Tablet mode - tabletEnv = TabletEnv() - } - - val tmpPhonePager : MyViewPager = findViewById(R.id.viewPager) - val tmpTabletPager : RecyclerView = findViewById(R.id.rvPager) - - phoneTab({ env -> + // 最後に指定する必要がある? + etQuickToot.maxLines = 1 + } + + svColumnStrip.isHorizontalFadingEdgeEnabled = true + + post_helper = PostHelper(this, pref, app_state.handler) + + val dm = resources.displayMetrics + + val density = dm.density + + var media_thumb_height = 64 + sv = Pref.spMediaThumbHeight(pref) + if (sv.isNotEmpty()) { + try { + val iv = Integer.parseInt(sv) + if (iv >= 32) { + media_thumb_height = iv + } + } catch (ex: Throwable) { + log.trace(ex) + } + + } + app_state.media_thumb_height = (0.5f + media_thumb_height * density).toInt() + + var column_w_min_dp = COLUMN_WIDTH_MIN_DP + sv = Pref.spColumnWidth(pref) + if (sv.isNotEmpty()) { + try { + val iv = Integer.parseInt(sv) + if (iv >= 100) { + column_w_min_dp = iv + } + } catch (ex: Throwable) { + log.trace(ex) + } + + } + val column_w_min = (0.5f + column_w_min_dp * density).toInt() + + val sw = dm.widthPixels + + + if (Pref.bpDisableTabletMode(pref) || sw < column_w_min * 2) { + // SmartPhone mode + phoneEnv = PhoneEnv() + } else { + // Tablet mode + tabletEnv = TabletEnv() + } + + val tmpPhonePager: MyViewPager = findViewById(R.id.viewPager) + val tmpTabletPager: RecyclerView = findViewById(R.id.rvPager) + + phoneTab({ env -> tmpTabletPager.visibility = View.GONE env.pager = tmpPhonePager env.pager_adapter = ColumnPagerAdapter(this) env.pager.adapter = env.pager_adapter env.pager.addOnPageChangeListener(this) - + resizeAutoCW(sw) - + }, { env -> tmpPhonePager.visibility = View.GONE env.tablet_pager = tmpTabletPager @@ -1507,236 +1470,237 @@ class ActMain : AsyncActivity(), Column.Callback, View.OnClickListener, LinearLayoutManager.HORIZONTAL, false ) - - if(env.tablet_pager.itemDecorationCount == 0) { + + if (env.tablet_pager.itemDecorationCount == 0) { env.tablet_pager.addItemDecoration(TabletColumnDivider(this@ActMain)) } - - + + env.tablet_pager.adapter = env.tablet_pager_adapter env.tablet_pager.layoutManager = env.tablet_layout_manager env.tablet_pager.addOnScrollListener(object : RecyclerView.OnScrollListener() { - + override fun onScrollStateChanged( - recyclerView : RecyclerView, - newState : Int + recyclerView: RecyclerView, + newState: Int ) { super.onScrollStateChanged(recyclerView, newState) - + val vs = env.tablet_layout_manager.findFirstVisibleItemPosition() val ve = env.tablet_layout_manager.findLastVisibleItemPosition() // 端に近い方に合わせる val distance_left = abs(vs) val distance_right = abs(app_state.column_list.size - 1 - ve) - if(distance_left < distance_right) { + if (distance_left < distance_right) { scrollColumnStrip(vs) } else { scrollColumnStrip(ve) } } - + override fun onScrolled( - recyclerView : RecyclerView, - dx : Int, - dy : Int + recyclerView: RecyclerView, + dx: Int, + dy: Int ) { super.onScrolled(recyclerView, dx, dy) - updateColumnStripSelection(- 1, - 1f) + updateColumnStripSelection(-1, -1f) } }) - + env.tablet_pager.itemAnimator = null // val animator = env.tablet_pager.itemAnimator // if( animator is DefaultItemAnimator){ // animator.supportsChangeAnimations = false // } - + env.tablet_snap_helper = GravitySnapHelper(Gravity.START) env.tablet_snap_helper.attachToRecyclerView(env.tablet_pager) - + }) - - showFooterColor() - - post_helper.attachEditText( + + showFooterColor() + + post_helper.attachEditText( llFormRoot, etQuickToot, true, object : PostHelper.Callback2 { override fun onTextUpdate() {} - - override fun canOpenPopup() : Boolean { - return ! drawer.isDrawerOpen(GravityCompat.START) + + override fun canOpenPopup(): Boolean { + return !drawer.isDrawerOpen(GravityCompat.START) } }) - - showQuickTootVisibility() - } - - private fun isVisibleColumn(idx : Int) = phoneTab( + + showQuickTootVisibility() + } + + private fun isVisibleColumn(idx: Int) = phoneTab( { env -> val c = env.pager.currentItem c == idx }, { env -> - idx >= 0 && idx in env.visibleColumnsIndices - } + idx >= 0 && idx in env.visibleColumnsIndices + } ) - - private fun updateColumnStrip() { - llEmpty.vg(app_state.column_list.isEmpty()) - - val iconSize = stripIconSize - val rootW = (iconSize * 1.25f + 0.5f).toInt() - val rootH = (iconSize * 1.5f + 0.5f).toInt() - val iconTopMargin = (iconSize * 0.125f + 0.5f).toInt() - val barHeight = (iconSize * 0.094f + 0.5f).toInt() - val barTopMargin = (iconSize * 0.094f + 0.5f).toInt() - - // 両端のメニューと投稿ボタンの大きさ - val pad = (rootH - iconSize) shr 1 - for(btn in arrayOf(btnToot, btnMenu, btnQuickTootMenu, btnQuickToot)) { - btn.layoutParams.width = rootH // not W - btn.layoutParams.height = rootH - btn.setPaddingRelative(pad, pad, pad, pad) - } - - llColumnStrip.removeAllViews() - for(i in 0 until app_state.column_list.size) { - - val column = app_state.column_list[i] - - val viewRoot = layoutInflater.inflate(R.layout.lv_column_strip, llColumnStrip, false) - val ivIcon = viewRoot.findViewById(R.id.ivIcon) - val vAcctColor = viewRoot.findViewById(R.id.vAcctColor) - - // root: 48x48dp LinearLayout(vertical), gravity=center - viewRoot.layoutParams.width = rootW - viewRoot.layoutParams.height = rootH - - // ivIcon: 32x32dp marginTop="4dp" 図柄が32x32dp、パディングなし - ivIcon.layoutParams.width = iconSize - ivIcon.layoutParams.height = iconSize - (ivIcon.layoutParams as? LinearLayout.LayoutParams)?.topMargin = iconTopMargin - - // vAcctColor: 32x3dp marginTop="3dp" - vAcctColor.layoutParams.width = iconSize - vAcctColor.layoutParams.height = barHeight - (vAcctColor.layoutParams as? LinearLayout.LayoutParams)?.topMargin = barTopMargin - - viewRoot.tag = i - viewRoot.setOnClickListener { v -> - val idx = v.tag as Int - if(Pref.bpScrollTopFromColumnStrip(pref) && isVisibleColumn(idx)) { - app_state.column_list[i].viewHolder?.scrollToTop2() - return@setOnClickListener - } - scrollToColumn(idx) - } - viewRoot.contentDescription = column.getColumnName(true) - - viewRoot.backgroundDrawable = getAdaptiveRippleDrawableRound( + + private fun updateColumnStrip() { + llEmpty.vg(app_state.column_list.isEmpty()) + + val iconSize = stripIconSize + val rootW = (iconSize * 1.25f + 0.5f).toInt() + val rootH = (iconSize * 1.5f + 0.5f).toInt() + val iconTopMargin = (iconSize * 0.125f + 0.5f).toInt() + val barHeight = (iconSize * 0.094f + 0.5f).toInt() + val barTopMargin = (iconSize * 0.094f + 0.5f).toInt() + + // 両端のメニューと投稿ボタンの大きさ + val pad = (rootH - iconSize) shr 1 + for (btn in arrayOf(btnToot, btnMenu, btnQuickTootMenu, btnQuickToot)) { + btn.layoutParams.width = rootH // not W + btn.layoutParams.height = rootH + btn.setPaddingRelative(pad, pad, pad, pad) + } + + llColumnStrip.removeAllViews() + for (i in 0 until app_state.column_list.size) { + + val column = app_state.column_list[i] + + val viewRoot = layoutInflater.inflate(R.layout.lv_column_strip, llColumnStrip, false) + val ivIcon = viewRoot.findViewById(R.id.ivIcon) + val vAcctColor = viewRoot.findViewById(R.id.vAcctColor) + + // root: 48x48dp LinearLayout(vertical), gravity=center + viewRoot.layoutParams.width = rootW + viewRoot.layoutParams.height = rootH + + // ivIcon: 32x32dp marginTop="4dp" 図柄が32x32dp、パディングなし + ivIcon.layoutParams.width = iconSize + ivIcon.layoutParams.height = iconSize + (ivIcon.layoutParams as? LinearLayout.LayoutParams)?.topMargin = iconTopMargin + + // vAcctColor: 32x3dp marginTop="3dp" + vAcctColor.layoutParams.width = iconSize + vAcctColor.layoutParams.height = barHeight + (vAcctColor.layoutParams as? LinearLayout.LayoutParams)?.topMargin = barTopMargin + + viewRoot.tag = i + viewRoot.setOnClickListener { v -> + val idx = v.tag as Int + if (Pref.bpScrollTopFromColumnStrip(pref) && isVisibleColumn(idx)) { + app_state.column_list[i].viewHolder?.scrollToTop2() + return@setOnClickListener + } + scrollToColumn(idx) + } + viewRoot.contentDescription = column.getColumnName(true) + + viewRoot.backgroundDrawable = getAdaptiveRippleDrawableRound( this, column.getHeaderBackgroundColor(), column.getHeaderNameColor() ) - - ivIcon.setImageResource(column.getIconId()) - ivIcon.imageTintList = ColorStateList.valueOf(column.getHeaderNameColor()) - - // - val ac = AcctColor.load(column.access_info) - if(AcctColor.hasColorForeground(ac)) { - vAcctColor.setBackgroundColor(ac.color_fg) - } else { - vAcctColor.visibility = View.INVISIBLE - } - - // - llColumnStrip.addView(viewRoot) - } - svColumnStrip.requestLayout() - updateColumnStripSelection(- 1, - 1f) - - } - - private fun updateColumnStripSelection(position : Int, positionOffset : Float) { - handler.post(Runnable { - if(isFinishing) return@Runnable - - if(app_state.column_list.isEmpty()) { - llColumnStrip.setVisibleRange(- 1, - 1, 0f) + + ivIcon.setImageResource(column.getIconId()) + ivIcon.imageTintList = ColorStateList.valueOf(column.getHeaderNameColor()) + + // + val ac = AcctColor.load(column.access_info) + if (AcctColor.hasColorForeground(ac)) { + vAcctColor.setBackgroundColor(ac.color_fg) + } else { + vAcctColor.visibility = View.INVISIBLE + } + + // + llColumnStrip.addView(viewRoot) + } + svColumnStrip.requestLayout() + updateColumnStripSelection(-1, -1f) + + } + + private fun updateColumnStripSelection(position: Int, positionOffset: Float) { + handler.post(Runnable { + if (isFinishing) return@Runnable + + if (app_state.column_list.isEmpty()) { + llColumnStrip.setVisibleRange(-1, -1, 0f) } else { phoneTab({ env -> - if(position >= 0) { + if (position >= 0) { llColumnStrip.setVisibleRange(position, position, positionOffset) } else { val c = env.pager.currentItem llColumnStrip.setVisibleRange(c, c, 0f) } - + }, { env -> val vs = env.tablet_layout_manager.findFirstVisibleItemPosition() val ve = env.tablet_layout_manager.findLastVisibleItemPosition() - val vr = if(vs == RecyclerView.NO_POSITION || ve == RecyclerView.NO_POSITION) { - IntRange(- 1, - 2) // empty and less than zero + val vr = if (vs == RecyclerView.NO_POSITION || ve == RecyclerView.NO_POSITION) { + IntRange(-1, -2) // empty and less than zero } else { IntRange(vs, min(ve, vs + nScreenColumn - 1)) } var slide_ratio = 0f - if(vr.first <= vr.last) { + if (vr.first <= vr.last) { val child = env.tablet_layout_manager.findViewByPosition(vr.first) slide_ratio = clipRange(0f, 1f, abs((child?.left ?: 0) / nColumnWidth.toFloat())) } - + llColumnStrip.setVisibleRange(vr.first, vr.last, slide_ratio) }) } }) - } - - private fun scrollColumnStrip(select : Int) { - val child_count = llColumnStrip.childCount - if(select < 0 || select >= child_count) { - return - } - - val icon = llColumnStrip.getChildAt(select) - - val sv_width = (llColumnStrip.parent as View).width - val ll_width = llColumnStrip.width - val icon_width = icon.width - val icon_left = icon.left - - if(sv_width == 0 || ll_width == 0 || icon_width == 0) { - handler.postDelayed({ scrollColumnStrip(select) }, 20L) - } - - val sx = icon_left + icon_width / 2 - sv_width / 2 - svColumnStrip.smoothScrollTo(sx, 0) - - } - - // ActOAuthCallbackで受け取ったUriを処理する - private fun handleIntentUri(uri : Uri) { - - log.d("handleIntentUri ${uri}") - - when(uri.scheme) { + } + + private fun scrollColumnStrip(select: Int) { + val child_count = llColumnStrip.childCount + if (select < 0 || select >= child_count) { + return + } + + val icon = llColumnStrip.getChildAt(select) + + val sv_width = (llColumnStrip.parent as View).width + val ll_width = llColumnStrip.width + val icon_width = icon.width + val icon_left = icon.left + + if (sv_width == 0 || ll_width == 0 || icon_width == 0) { + handler.postDelayed({ scrollColumnStrip(select) }, 20L) + } + + val sx = icon_left + icon_width / 2 - sv_width / 2 + svColumnStrip.smoothScrollTo(sx, 0) + + } + + // ActOAuthCallbackで受け取ったUriを処理する + private fun handleIntentUri(uri: Uri) { + + log.d("handleIntentUri ${uri}") + + when (uri.scheme) { "subwaytooter", "misskeyclientproto" -> return try { - handleOAuth2CallbackUri(uri) - } catch(ex : Throwable) { + handleCustomSchemaUri(uri) + } catch (ex: Throwable) { log.trace(ex) + showToast(ex, "handleCustomSchemaUri failed.") } - } - - val url = uri.toString() - - val statusInfo = url.findStatusIdFromUrl() - if(statusInfo != null) { - // ステータスをアプリ内で開く - Action_Toot.conversationOtherInstance( + } + + val url = uri.toString() + + val statusInfo = url.findStatusIdFromUrl() + if (statusInfo != null) { + // ステータスをアプリ内で開く + Action_Toot.conversationOtherInstance( this@ActMain, defaultInsertPosition, statusInfo.url, @@ -1744,18 +1708,18 @@ class ActMain : AsyncActivity(), Column.Callback, View.OnClickListener, statusInfo.host, statusInfo.statusId ) - return - } - - // ユーザページをアプリ内で開く - var m = TootAccount.reAccountUrl.matcher(url) - if(m.find()) { - val host = m.groupEx(1) !! - val user = m.groupEx(2) !!.decodePercent() - val instance = m.groupEx(3)?.decodePercent() - - if(instance?.isNotEmpty() == true) { - Action_User.profile( + return + } + + // ユーザページをアプリ内で開く + var m = TootAccount.reAccountUrl.matcher(url) + if (m.find()) { + val host = m.groupEx(1)!! + val user = m.groupEx(2)!!.decodePercent() + val instance = m.groupEx(3)?.decodePercent() + + if (instance?.isNotEmpty() == true) { + Action_User.profile( this@ActMain, defaultInsertPosition, null, @@ -1764,8 +1728,8 @@ class ActMain : AsyncActivity(), Column.Callback, View.OnClickListener, user, original_url = url ) - } else { - Action_User.profile( + } else { + Action_User.profile( this@ActMain, defaultInsertPosition, null, @@ -1773,17 +1737,17 @@ class ActMain : AsyncActivity(), Column.Callback, View.OnClickListener, Host.parse(host), user ) - } - return - } - - // intentFilterの都合でこの形式のURLが飛んでくることはないのだが…。 - m = TootAccount.reAccountUrl2.matcher(url) - if(m.find()) { - val host = m.groupEx(1) !! - val user = m.groupEx(2) !!.decodePercent() - - Action_User.profile( + } + return + } + + // intentFilterの都合でこの形式のURLが飛んでくることはないのだが…。 + m = TootAccount.reAccountUrl2.matcher(url) + if (m.find()) { + val host = m.groupEx(1)!! + val user = m.groupEx(2)!!.decodePercent() + + Action_User.profile( this@ActMain, defaultInsertPosition, null, @@ -1791,328 +1755,322 @@ class ActMain : AsyncActivity(), Column.Callback, View.OnClickListener, Host.parse(host), user ) - return - } - - // このアプリでは処理できないURLだった - // 外部ブラウザを開きなおそうとすると無限ループの恐れがある - // アプリケーションチューザーを表示する - - val error_message = getString(R.string.cant_handle_uri_of, url) - - try { - val query_flag = if(Build.VERSION.SDK_INT >= 23) { - // Android 6.0以降 - // MATCH_DEFAULT_ONLY だと標準の設定に指定されたアプリがあるとソレしか出てこない - // MATCH_ALL を指定すると 以前と同じ挙動になる - PackageManager.MATCH_ALL - } else { - // Android 5.xまでは MATCH_DEFAULT_ONLY でマッチするすべてのアプリを取得できる - PackageManager.MATCH_DEFAULT_ONLY - } - - // queryIntentActivities に渡すURLは実在しないホストのものにする - val intent = Intent(Intent.ACTION_VIEW, "https://dummy.subwaytooter.club/".toUri()) - intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK - - val my_name = packageName - val resolveInfoList = packageManager.queryIntentActivities(intent, query_flag) - .filter { my_name != it.activityInfo.packageName } - - if(resolveInfoList.isEmpty()) { - throw RuntimeException("resolveInfoList is empty.") - } - - // このアプリ以外の選択肢を集める - val choice_list = resolveInfoList - .map { - Intent(Intent.ACTION_VIEW, uri).apply { - addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - `package` = it.activityInfo.packageName - setClassName(it.activityInfo.packageName, it.activityInfo.name) - } - }.toMutableList() - - val chooser = Intent.createChooser(choice_list.removeAt(0), error_message) - // 2つめ以降はEXTRAに渡す - chooser.putExtra(Intent.EXTRA_INITIAL_INTENTS, choice_list.toTypedArray()) - - // 指定した選択肢でチューザーを作成して開く - startActivity(chooser) - return - } catch(ex : Throwable) { - log.trace(ex) - } - - AlertDialog.Builder(this) - .setCancelable(true) - .setMessage(error_message) - .setPositiveButton(R.string.close, null) - .show() - - } - - private fun handleOAuth2CallbackUri(uri : Uri) { - - // 通知タップ - // subwaytooter://notification_click/?db_id=(db_id) - val dataIdString = uri.getQueryParameter("db_id") - if(dataIdString != null) { - - PollingWorker.queueNotificationClicked(this, uri) - - try { - val dataId = dataIdString.toLong() - val account = SavedAccount.loadAccount(this@ActMain, dataId) - if(account != null) { - var column = app_state.column_list.firstOrNull { - it.type == ColumnType.NOTIFICATIONS - && account == it.access_info - && ! it.system_notification_not_related - } - if(column != null) { - val index = app_state.column_list.indexOf(column) - scrollToColumn(index) - } else { - column = addColumn( - true, - defaultInsertPosition, - account, - ColumnType.NOTIFICATIONS - ) - } - // 通知を読み直す - if(! column.bInitialLoading) { - column.startLoading() - } - } - } catch(ex : Throwable) { - log.trace(ex) - } - - return - } - - // OAuth2 認証コールバック - // subwaytooter://oauth(\d*)/?... - TootTaskRunner(this@ActMain).run(object : TootTask { - - var ta : TootAccount? = null - var sa : SavedAccount? = null - var host : Host? = null - var ti : TootInstance? = null - - override fun background(client : TootApiClient) : TootApiResult? { - + return + } + + // このアプリでは処理できないURLだった + // 外部ブラウザを開きなおそうとすると無限ループの恐れがある + // アプリケーションチューザーを表示する + + val error_message = getString(R.string.cant_handle_uri_of, url) + + try { + val query_flag = if (Build.VERSION.SDK_INT >= 23) { + // Android 6.0以降 + // MATCH_DEFAULT_ONLY だと標準の設定に指定されたアプリがあるとソレしか出てこない + // MATCH_ALL を指定すると 以前と同じ挙動になる + PackageManager.MATCH_ALL + } else { + // Android 5.xまでは MATCH_DEFAULT_ONLY でマッチするすべてのアプリを取得できる + PackageManager.MATCH_DEFAULT_ONLY + } + + // queryIntentActivities に渡すURLは実在しないホストのものにする + val intent = Intent(Intent.ACTION_VIEW, "https://dummy.subwaytooter.club/".toUri()) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + + val my_name = packageName + val resolveInfoList = packageManager.queryIntentActivities(intent, query_flag) + .filter { my_name != it.activityInfo.packageName } + + if (resolveInfoList.isEmpty()) { + throw RuntimeException("resolveInfoList is empty.") + } + + // このアプリ以外の選択肢を集める + val choice_list = resolveInfoList + .map { + Intent(Intent.ACTION_VIEW, uri).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + `package` = it.activityInfo.packageName + setClassName(it.activityInfo.packageName, it.activityInfo.name) + } + }.toMutableList() + + val chooser = Intent.createChooser(choice_list.removeAt(0), error_message) + // 2つめ以降はEXTRAに渡す + chooser.putExtra(Intent.EXTRA_INITIAL_INTENTS, choice_list.toTypedArray()) + + // 指定した選択肢でチューザーを作成して開く + startActivity(chooser) + return + } catch (ex: Throwable) { + log.trace(ex) + } + + AlertDialog.Builder(this) + .setCancelable(true) + .setMessage(error_message) + .setPositiveButton(R.string.close, null) + .show() + + } + + private fun handleNotificationClick(uri: Uri, dataIdString: String) { + try { + val account = dataIdString.toLongOrNull()?.let { SavedAccount.loadAccount(this, it) } + if (account == null) { + showToast(true, "handleNotificationClick: missing SavedAccount. id=$dataIdString") + return + } + + PollingWorker.queueNotificationClicked(this, uri) + + val column = app_state.column_list.firstOrNull { + it.type == ColumnType.NOTIFICATIONS && + it.access_info == account && + !it.system_notification_not_related + }?.also { + scrollToColumn(app_state.column_list.indexOf(it)) + } ?: addColumn( + true, + defaultInsertPosition, + account, + ColumnType.NOTIFICATIONS + ) + + // 通知を読み直す + if (!column.bInitialLoading) column.startLoading() + } catch (ex: Throwable) { + log.trace(ex) + } + } + + private fun handleOAuth2Callback(uri: Uri) { + TootTaskRunner(this@ActMain).run(object : TootTask { + + var ta: TootAccount? = null + var sa: SavedAccount? = null + var host: Host? = null + var ti: TootInstance? = null + + override fun background(client: TootApiClient): TootApiResult? { + val uriStr = uri.toString() - if(uriStr.startsWith("subwaytooter://misskey/auth_callback") + if (uriStr.startsWith("subwaytooter://misskey/auth_callback") || uriStr.startsWith("misskeyclientproto://misskeyclientproto/auth_callback") ) { - + // Misskey 認証コールバック val token = uri.getQueryParameter("token") - if(token?.isEmpty() != false) { + if (token.isNullOrBlank()) return TootApiResult("missing token in callback URL") - } + val prefDevice = PrefDevice.prefDevice(this@ActMain) - - val db_id = prefDevice.getLong(PrefDevice.LAST_AUTH_DB_ID, - 1L) - + val instance = Host.parse( prefDevice.getString(PrefDevice.LAST_AUTH_INSTANCE, null) ?: return TootApiResult("missing instance name.") ) - - if(db_id != - 1L) { - try { + + when (val db_id = prefDevice.getLong(PrefDevice.LAST_AUTH_DB_ID, -1L)) { + + // new registration + -1L -> client.apiHost = instance + + // update access token + else -> try { val sa = SavedAccount.loadAccount(this@ActMain, db_id) ?: return TootApiResult("missing account db_id=$db_id") this.sa = sa client.account = sa - } catch(ex : Throwable) { + } catch (ex: Throwable) { log.trace(ex) return TootApiResult(ex.withCaption("invalid state")) } - } else { - client.apiHost = instance } - + val (ti, r2) = TootInstance.get(client) ti ?: return r2 - + this.ti = ti this.host = instance - val client_name = Pref.spClientName(this@ActMain) - val result = - client.authentication2Misskey(client_name, token, ti.misskeyVersion) - this.ta = TootParser( + + val parser = TootParser( this@ActMain, linkHelper = LinkHelper.create( instance, misskeyVersion = ti.misskeyVersion ) - ).account(result?.jsonObject) - return result - + ) + + return client.authentication2Misskey( + Pref.spClientName(this@ActMain), + token, + ti.misskeyVersion + )?.also { this.ta = parser.account(it.jsonObject) } + } else { // Mastodon 認証コールバック - + // エラー時 // subwaytooter://oauth(\d*)/ // ?error=access_denied // &error_description=%E3%83%AA%E3%82%BD%E3%83%BC%E3%82%B9%E3%81%AE%E6%89%80%E6%9C%89%E8%80%85%E3%81%BE%E3%81%9F%E3%81%AF%E8%AA%8D%E8%A8%BC%E3%82%B5%E3%83%BC%E3%83%90%E3%83%BC%E3%81%8C%E8%A6%81%E6%B1%82%E3%82%92%E6%8B%92%E5%90%A6%E3%81%97%E3%81%BE%E3%81%97%E3%81%9F%E3%80%82 // &state=db%3A3 - val error = uri.getQueryParameter("error_description") - if(error?.isNotEmpty() == true) { - return TootApiResult(error) - } - + val error = uri.getQueryParameter("error") + val error_description = uri.getQueryParameter("error_description") + if (error != null || error_description != null) + return TootApiResult(error_description.notBlank() ?: error.notBlank() + ?: "?") + // subwaytooter://oauth(\d*)/ // ?code=113cc036e078ac500d3d0d3ad345cd8181456ab087abc67270d40f40a4e9e3c2 // &state=host%3Amastodon.juggler.jp - + val code = uri.getQueryParameter("code") - if(code?.isEmpty() != false) { - return TootApiResult("missing code in callback url.") - } - val sv = uri.getQueryParameter("state") - if(sv?.isEmpty() != false) { + + if (code.isNullOrBlank()) + return TootApiResult("missing code in callback url.") + + if (sv.isNullOrBlank()) return TootApiResult("missing state in callback url.") - } - - for(param in sv.split(",")) { + + for (param in sv.split(",")) { when { - param.startsWith("db:") -> try { val dataId = param.substring(3).toLong(10) val sa = SavedAccount.loadAccount(this@ActMain, dataId) ?: return TootApiResult("missing account db_id=$dataId") this.sa = sa client.account = sa - } catch(ex : Throwable) { + } catch (ex: Throwable) { log.trace(ex) return TootApiResult(ex.withCaption("invalid state")) } - + param.startsWith("host:") -> { val host = Host.parse(param.substring(5)) client.apiHost = host } - - else -> { - // ignore other parameter - } + + // ignore other parameter } } - + val instance = client.apiHost ?: return TootApiResult("missing instance in callback url.") - + val (ti, r2) = TootInstance.get(client) ti ?: return r2 - + this.ti = ti this.host = instance - val client_name = Pref.spClientName(this@ActMain) - val result = client.authentication2(client_name, code) - this.ta = TootParser( + + val parser = TootParser( this@ActMain, linkHelper = LinkHelper.create(instance) - ).account(result?.jsonObject) - return result + ) + + return client.authentication2( + Pref.spClientName(this@ActMain), + code + )?.also { this.ta = parser.account(it.jsonObject) } } - } - - override fun handleResult(result : TootApiResult?) { + + override fun handleResult(result: TootApiResult?) { val host = this.host val ta = this.ta var sa = this.sa - - if(ta != null && host?.isValid == true && sa == null) { + + if (ta != null && host?.isValid == true && sa == null) { val acct = Acct.parse(ta.username, host) // アカウント追加時に、アプリ内に既にあるアカウントと同じものを登録していたかもしれない sa = SavedAccount.loadAccountByAcct(this@ActMain, acct.ascii) } - + afterAccountVerify(result, ta, sa, ti, host) } - + }) - } - - internal fun afterAccountVerify( - result : TootApiResult?, - ta : TootAccount?, - sa : SavedAccount?, - ti : TootInstance?, - host : Host? - ) : Boolean { - - val jsonObject = result?.jsonObject - val token_info = result?.tokenInfo - val error = result?.error - - when { - result == null -> { - // cancelled. - } - - error != null -> - showToast(true, "${result.error} ${result.requestInfo}".trim()) - - token_info == null -> showToast(true, "can't get access token.") - - jsonObject == null -> showToast(true, "can't parse json response.") - - // 自分のユーザネームを取れなかった - // …普通はエラーメッセージが設定されてるはずだが - ta == null -> showToast(true, "can't verify user credential.") - - // アクセストークン更新時 - // インスタンスは同じだと思うが、ユーザ名が異なる可能性がある - sa != null -> - if(sa.username != ta.username) { - showToast(true, R.string.user_name_not_match) - } else { - showToast( - - false, - R.string.access_token_updated_for, - sa.acct.pretty - ) - - // DBの情報を更新する - sa.updateTokenInfo(token_info) - - // 各カラムの持つアカウント情報をリロードする - reloadAccountSetting() - - // 自動でリロードする - for(it in app_state.column_list) { - if(it.access_info == sa) { - it.startLoading() - } - } - - // 通知の更新が必要かもしれない - PushSubscriptionHelper.clearLastCheck(sa) - PollingWorker.queueUpdateNotification(this@ActMain) - return true - } - - host != null -> { - // アカウント追加時 - val user = Acct.parse(ta.username, host) - - val apDomain = ti?.uri - if(apDomain == null) { - showToast(false, "Can't get ActivityPub domain name.") - return false - } - - val row_id = SavedAccount.insert( + } + + private fun handleCustomSchemaUri(uri: Uri) { + val dataIdString = uri.getQueryParameter("db_id") + if (dataIdString != null) { + // subwaytooter://notification_click/?db_id=(db_id) + handleNotificationClick(uri, dataIdString) + } else { + // OAuth2 認証コールバック + // subwaytooter://oauth(\d*)/?... + handleOAuth2Callback(uri) + } + } + + internal fun afterAccountVerify( + result: TootApiResult?, + ta: TootAccount?, + sa: SavedAccount?, + ti: TootInstance?, + host: Host? + ): Boolean { + result ?: return false + + val jsonObject = result.jsonObject + val token_info = result.tokenInfo + val error = result.error + + when { + error != null -> + showToast(true, "${result.error} ${result.requestInfo}".trim()) + + token_info == null -> + showToast(true, "can't get access token.") + + jsonObject == null -> + showToast(true, "can't parse json response.") + + // 自分のユーザネームを取れなかった + // …普通はエラーメッセージが設定されてるはずだが + ta == null -> showToast(true, "can't verify user credential.") + + // アクセストークン更新時 + // インスタンスは同じだと思うが、ユーザ名が異なる可能性がある + sa != null -> if (sa.username != ta.username) { + showToast(true, R.string.user_name_not_match) + } else { + showToast(false, R.string.access_token_updated_for, sa.acct.pretty) + + // DBの情報を更新する + sa.updateTokenInfo(token_info) + + // 各カラムの持つアカウント情報をリロードする + reloadAccountSetting() + + // 自動でリロードする + app_state.column_list + .filter { it.access_info == sa } + .forEach { it.startLoading() } + + // 通知の更新が必要かもしれない + PushSubscriptionHelper.clearLastCheck(sa) + PollingWorker.queueUpdateNotification(this@ActMain) + return true + } + + host != null -> { + // アカウント追加時 + val user = Acct.parse(ta.username, host) + + val apDomain = ti?.uri + if (apDomain == null) { + showToast(false, "Can't get ActivityPub domain name.") + return false + } + + val row_id = SavedAccount.insert( acct = user.ascii, host = host.ascii, domain = apDomain, @@ -2120,79 +2078,79 @@ class ActMain : AsyncActivity(), Column.Callback, View.OnClickListener, token = token_info, misskeyVersion = TootInstance.parseMisskeyVersion(token_info) ) - val account = SavedAccount.loadAccount(this@ActMain, row_id) - if(account != null) { - var bModified = false - - if(account.loginAccount?.locked == true) { - bModified = true - account.visibility = TootVisibility.PrivateFollowers - } - if(! account.isMisskey) { - val source = ta.source - if(source != null) { - val privacy = TootVisibility.parseMastodon(source.privacy) - if(privacy != null) { - bModified = true - account.visibility = privacy - } - - // XXX ta.source.sensitive パラメータを読んで「添付画像をデフォルトでNSFWにする」を実現する - // 現在、アカウント設定にはこの項目はない( 「NSFWな添付メディアを隠さない」はあるが全く別の効果) - } - - if(bModified) { - account.saveSetting() - } - } - - showToast(false, R.string.account_confirmed) - - // 通知の更新が必要かもしれない - PollingWorker.queueUpdateNotification(this@ActMain) - - // 適当にカラムを追加する - val count = SavedAccount.count - if(count > 1) { - addColumn(false, defaultInsertPosition, account, ColumnType.HOME) - } else { - addColumn(false, defaultInsertPosition, account, ColumnType.HOME) - addColumn(false, defaultInsertPosition, account, ColumnType.NOTIFICATIONS) - addColumn(false, defaultInsertPosition, account, ColumnType.LOCAL) - addColumn(false, defaultInsertPosition, account, ColumnType.FEDERATE) - } - - return true - } - } - } - return false - } - - // アクセストークンを手動で入力した場合 - fun checkAccessToken( - dialog_host : Dialog?, - dialog_token : Dialog?, - apiHost : Host, - access_token : String, - sa : SavedAccount? + val account = SavedAccount.loadAccount(this@ActMain, row_id) + if (account != null) { + var bModified = false + + if (account.loginAccount?.locked == true) { + bModified = true + account.visibility = TootVisibility.PrivateFollowers + } + if (!account.isMisskey) { + val source = ta.source + if (source != null) { + val privacy = TootVisibility.parseMastodon(source.privacy) + if (privacy != null) { + bModified = true + account.visibility = privacy + } + + // XXX ta.source.sensitive パラメータを読んで「添付画像をデフォルトでNSFWにする」を実現する + // 現在、アカウント設定にはこの項目はない( 「NSFWな添付メディアを隠さない」はあるが全く別の効果) + } + + if (bModified) { + account.saveSetting() + } + } + + showToast(false, R.string.account_confirmed) + + // 通知の更新が必要かもしれない + PollingWorker.queueUpdateNotification(this@ActMain) + + // 適当にカラムを追加する + val count = SavedAccount.count + if (count > 1) { + addColumn(false, defaultInsertPosition, account, ColumnType.HOME) + } else { + addColumn(false, defaultInsertPosition, account, ColumnType.HOME) + addColumn(false, defaultInsertPosition, account, ColumnType.NOTIFICATIONS) + addColumn(false, defaultInsertPosition, account, ColumnType.LOCAL) + addColumn(false, defaultInsertPosition, account, ColumnType.FEDERATE) + } + + return true + } + } + } + return false + } + + // アクセストークンを手動で入力した場合 + fun checkAccessToken( + dialog_host: Dialog?, + dialog_token: Dialog?, + apiHost: Host, + access_token: String, + sa: SavedAccount? ) { - - TootTaskRunner(this@ActMain).run(apiHost, object : TootTask { - - var ta : TootAccount? = null - var ti : TootInstance? = null - - override fun background(client : TootApiClient) : TootApiResult? { - + + TootTaskRunner(this@ActMain).run(apiHost, object : TootTask { + + var ta: TootAccount? = null + var ti: TootInstance? = null + + override fun background(client: TootApiClient): TootApiResult? { + val (instance, instanceResult) = TootInstance.get(client, apiHost) instance ?: return instanceResult this.ti = instance - + val misskeyVersion = instance.misskeyVersion - + val result = client.getUserCredential(access_token, misskeyVersion = misskeyVersion) - + this.ta = TootParser( this@ActMain, LinkHelper.create( @@ -2201,471 +2159,470 @@ class ActMain : AsyncActivity(), Column.Callback, View.OnClickListener, misskeyVersion = misskeyVersion ) ).account(result?.jsonObject) - + return result } - - override fun handleResult(result : TootApiResult?) { - if(afterAccountVerify(result, ta, sa, ti, apiHost)) { + + override fun handleResult(result: TootApiResult?) { + if (afterAccountVerify(result, ta, sa, ti, apiHost)) { dialog_host?.dismissSafe() dialog_token?.dismissSafe() } } }) - } - - // アクセストークンの手動入力(更新) - private fun checkAccessToken2(db_id : Long) { - - val sa = SavedAccount.loadAccount(this, db_id) ?: return - - DlgTextInput.show( + } + + // アクセストークンの手動入力(更新) + private fun checkAccessToken2(db_id: Long) { + + val sa = SavedAccount.loadAccount(this, db_id) ?: return + + DlgTextInput.show( this, getString(R.string.access_token_or_api_token), null, callback = object : DlgTextInput.Callback { - override fun onOK(dialog : Dialog, text : String) { + override fun onOK(dialog: Dialog, text: String) { checkAccessToken(null, dialog, sa.apiHost, text, sa) } - + override fun onEmptyError() { showToast(true, R.string.token_not_specified) } }) - } - - private fun reloadAccountSetting() { - val done_list = ArrayList() - for(column in app_state.column_list) { - val a = column.access_info - if(done_list.contains(a)) continue - done_list.add(a) - if(! a.isNA) a.reloadSetting(this@ActMain) - column.fireShowColumnHeader() - } - } - - fun reloadAccountSetting(account : SavedAccount) { - val done_list = ArrayList() - for(column in app_state.column_list) { - val a = column.access_info - if(a != account) continue - if(done_list.contains(a)) continue - done_list.add(a) - if(! a.isNA) a.reloadSetting(this@ActMain) - column.fireShowColumnHeader() - } - } - - fun closeColumn(column : Column, bConfirmed : Boolean = false) { - - if(column.dont_close) { - showToast(false, R.string.column_has_dont_close_option) - return - } - - if(! bConfirmed && ! Pref.bpDontConfirmBeforeCloseColumn(pref)) { - AlertDialog.Builder(this) - .setMessage(R.string.confirm_close_column) - .setNegativeButton(R.string.cancel, null) - .setPositiveButton(R.string.ok) { _, _ -> closeColumn(column, bConfirmed = true) } - .show() - return - } - - val page_delete = app_state.column_list.indexOf(column) - - phoneTab({ env -> + } + + private fun reloadAccountSetting() { + val done_list = ArrayList() + for (column in app_state.column_list) { + val a = column.access_info + if (done_list.contains(a)) continue + done_list.add(a) + if (!a.isNA) a.reloadSetting(this@ActMain) + column.fireShowColumnHeader() + } + } + + fun reloadAccountSetting(account: SavedAccount) { + val done_list = ArrayList() + for (column in app_state.column_list) { + val a = column.access_info + if (a != account) continue + if (done_list.contains(a)) continue + done_list.add(a) + if (!a.isNA) a.reloadSetting(this@ActMain) + column.fireShowColumnHeader() + } + } + + fun closeColumn(column: Column, bConfirmed: Boolean = false) { + + if (column.dont_close) { + showToast(false, R.string.column_has_dont_close_option) + return + } + + if (!bConfirmed && !Pref.bpDontConfirmBeforeCloseColumn(pref)) { + AlertDialog.Builder(this) + .setMessage(R.string.confirm_close_column) + .setNegativeButton(R.string.cancel, null) + .setPositiveButton(R.string.ok) { _, _ -> closeColumn(column, bConfirmed = true) } + .show() + return + } + + val page_delete = app_state.column_list.indexOf(column) + + phoneTab({ env -> val page_showing = env.pager.currentItem - + removeColumn(column) - - if(app_state.column_list.isNotEmpty() && page_delete > 0 && page_showing == page_delete) { + + if (app_state.column_list.isNotEmpty() && page_delete > 0 && page_showing == page_delete) { val idx = page_delete - 1 scrollToColumn(idx) val c = app_state.column_list[idx] - if(! c.bFirstInitialized) { + if (!c.bFirstInitialized) { c.startLoading() } } - + }, { removeColumn(column) - - if(app_state.column_list.isNotEmpty() && page_delete > 0) { + + if (app_state.column_list.isNotEmpty() && page_delete > 0) { val idx = page_delete - 1 scrollToColumn(idx) val c = app_state.column_list[idx] - if(! c.bFirstInitialized) { + if (!c.bFirstInitialized) { c.startLoading() } } }) - } - - fun closeColumnAll( - _lastColumnIndex : Int = - 1, - bConfirmed : Boolean = false + } + + fun closeColumnAll( + _lastColumnIndex: Int = -1, + bConfirmed: Boolean = false ) { - - if(! bConfirmed) { - AlertDialog.Builder(this) - .setMessage(R.string.confirm_close_column_all) - .setNegativeButton(R.string.cancel, null) - .setPositiveButton(R.string.ok) { _, _ -> closeColumnAll(_lastColumnIndex, true) } - .show() - return - } - - var lastColumnIndex = when(_lastColumnIndex) { - - 1 -> phoneTab( + + if (!bConfirmed) { + AlertDialog.Builder(this) + .setMessage(R.string.confirm_close_column_all) + .setNegativeButton(R.string.cancel, null) + .setPositiveButton(R.string.ok) { _, _ -> closeColumnAll(_lastColumnIndex, true) } + .show() + return + } + + var lastColumnIndex = when (_lastColumnIndex) { + -1 -> phoneTab( { it.pager.currentItem }, { 0 } ) - else -> _lastColumnIndex - } - - phoneOnly { env -> env.pager.adapter = null } - - for(i in (0 until app_state.column_list.size).reversed()) { - val column = app_state.column_list[i] - if(column.dont_close) continue - app_state.column_list.removeAt(i).dispose() - if(lastColumnIndex >= i) -- lastColumnIndex - } - - phoneTab( + else -> _lastColumnIndex + } + + phoneOnly { env -> env.pager.adapter = null } + + for (i in (0 until app_state.column_list.size).reversed()) { + val column = app_state.column_list[i] + if (column.dont_close) continue + app_state.column_list.removeAt(i).dispose() + if (lastColumnIndex >= i) --lastColumnIndex + } + + phoneTab( { env -> env.pager.adapter = env.pager_adapter }, { env -> resizeColumnWidth(env) } ) - - app_state.saveColumnList() - updateColumnStrip() - - if(app_state.column_list.isNotEmpty() && lastColumnIndex >= 0 && lastColumnIndex < app_state.column_list.size) { - scrollToColumn(lastColumnIndex) - val c = app_state.column_list[lastColumnIndex] - if(! c.bFirstInitialized) { - c.startLoading() - } - } - } - - ////////////////////////////////////////////////////////////// - // カラム追加系 - - fun addColumn( - indexArg : Int, - ai : SavedAccount, - type : ColumnType, - vararg params : Any - ) : Column { - return addColumn( + + app_state.saveColumnList() + updateColumnStrip() + + if (app_state.column_list.isNotEmpty() && lastColumnIndex >= 0 && lastColumnIndex < app_state.column_list.size) { + scrollToColumn(lastColumnIndex) + val c = app_state.column_list[lastColumnIndex] + if (!c.bFirstInitialized) { + c.startLoading() + } + } + } + +////////////////////////////////////////////////////////////// +// カラム追加系 + + fun addColumn( + indexArg: Int, + ai: SavedAccount, + type: ColumnType, + vararg params: Any + ): Column { + return addColumn( Pref.bpAllowColumnDuplication(pref), indexArg, ai, type, *params ) - } - - fun addColumn( - allowColumnDuplication : Boolean, - indexArg : Int, - ai : SavedAccount, - type : ColumnType, - vararg params : Any - ) : Column { - if(! allowColumnDuplication) { - // 既に同じカラムがあればそこに移動する - for(column in app_state.column_list) { - if(column.isSameSpec(ai, type, params)) { - val indexColumn = app_state.column_list.indexOf(column) - scrollToColumn(indexColumn) - return column - } - } - } - // - val col = Column(app_state, ai, this, type.id, *params) - val index = addColumn(col, indexArg) - scrollToColumn(index) - if(! col.bFirstInitialized) { - col.startLoading() - } - return col - } - - fun showColumnMatchAccount(account : SavedAccount) { - for(column in app_state.column_list) { - if(account == column.access_info) { - column.fireRebindAdapterItems() - } - } - } - - private fun showFooterColor() { - - val footer_button_bg_color = Pref.ipFooterButtonBgColor(pref) - val footer_button_fg_color = Pref.ipFooterButtonFgColor(pref) - val footer_tab_bg_color = Pref.ipFooterTabBgColor(pref) - val footer_tab_divider_color = Pref.ipFooterTabDividerColor(pref) - val footer_tab_indicator_color = Pref.ipFooterTabIndicatorColor(pref) - - val colorColumnStripBackground = footer_tab_bg_color.notZero() - ?: getAttributeColor(R.attr.colorColumnStripBackground) - - svColumnStrip.setBackgroundColor(colorColumnStripBackground) - llQuickTootBar.setBackgroundColor(colorColumnStripBackground) - - val colorButtonBg = footer_button_bg_color.notZero() - ?: colorColumnStripBackground - - val colorButtonFg = footer_button_fg_color.notZero() - ?: getAttributeColor(R.attr.colorRippleEffect) - - btnMenu.backgroundDrawable = - getAdaptiveRippleDrawableRound(this, colorButtonBg, colorButtonFg) - btnToot.backgroundDrawable = - getAdaptiveRippleDrawableRound(this, colorButtonBg, colorButtonFg) - btnQuickToot.backgroundDrawable = - getAdaptiveRippleDrawableRound(this, colorButtonBg, colorButtonFg) - btnQuickTootMenu.backgroundDrawable = - getAdaptiveRippleDrawableRound(this, colorButtonBg, colorButtonFg) - - val csl = ColorStateList.valueOf( + } + + fun addColumn( + allowColumnDuplication: Boolean, + indexArg: Int, + ai: SavedAccount, + type: ColumnType, + vararg params: Any + ): Column { + if (!allowColumnDuplication) { + // 既に同じカラムがあればそこに移動する + for (column in app_state.column_list) { + if (column.isSameSpec(ai, type, params)) { + val indexColumn = app_state.column_list.indexOf(column) + scrollToColumn(indexColumn) + return column + } + } + } + // + val col = Column(app_state, ai, this, type.id, *params) + val index = addColumn(col, indexArg) + scrollToColumn(index) + if (!col.bFirstInitialized) { + col.startLoading() + } + return col + } + + fun showColumnMatchAccount(account: SavedAccount) { + for (column in app_state.column_list) { + if (account == column.access_info) { + column.fireRebindAdapterItems() + } + } + } + + private fun showFooterColor() { + + val footer_button_bg_color = Pref.ipFooterButtonBgColor(pref) + val footer_button_fg_color = Pref.ipFooterButtonFgColor(pref) + val footer_tab_bg_color = Pref.ipFooterTabBgColor(pref) + val footer_tab_divider_color = Pref.ipFooterTabDividerColor(pref) + val footer_tab_indicator_color = Pref.ipFooterTabIndicatorColor(pref) + + val colorColumnStripBackground = footer_tab_bg_color.notZero() + ?: getAttributeColor(R.attr.colorColumnStripBackground) + + svColumnStrip.setBackgroundColor(colorColumnStripBackground) + llQuickTootBar.setBackgroundColor(colorColumnStripBackground) + + val colorButtonBg = footer_button_bg_color.notZero() + ?: colorColumnStripBackground + + val colorButtonFg = footer_button_fg_color.notZero() + ?: getAttributeColor(R.attr.colorRippleEffect) + + btnMenu.backgroundDrawable = + getAdaptiveRippleDrawableRound(this, colorButtonBg, colorButtonFg) + btnToot.backgroundDrawable = + getAdaptiveRippleDrawableRound(this, colorButtonBg, colorButtonFg) + btnQuickToot.backgroundDrawable = + getAdaptiveRippleDrawableRound(this, colorButtonBg, colorButtonFg) + btnQuickTootMenu.backgroundDrawable = + getAdaptiveRippleDrawableRound(this, colorButtonBg, colorButtonFg) + + val csl = ColorStateList.valueOf( footer_button_fg_color.notZero() ?: getAttributeColor(R.attr.colorVectorDrawable) ) - btnToot.imageTintList = csl - btnMenu.imageTintList = csl - btnQuickToot.imageTintList = csl - btnQuickTootMenu.imageTintList = csl - - val c = footer_tab_divider_color.notZero() - ?: colorColumnStripBackground - vFooterDivider1.setBackgroundColor(c) - vFooterDivider2.setBackgroundColor(c) - - llColumnStrip.indicatorColor = footer_tab_indicator_color.notZero() - ?: getAttributeColor(R.attr.colorAccent) - } - - ///////////////////////////////////////////////////////////////////////// - // タブレット対応で必要になった関数など - - private fun closeColumnSetting() : Boolean { - phoneTab({ env -> + btnToot.imageTintList = csl + btnMenu.imageTintList = csl + btnQuickToot.imageTintList = csl + btnQuickTootMenu.imageTintList = csl + + val c = footer_tab_divider_color.notZero() + ?: colorColumnStripBackground + vFooterDivider1.setBackgroundColor(c) + vFooterDivider2.setBackgroundColor(c) + + llColumnStrip.indicatorColor = footer_tab_indicator_color.notZero() + ?: getAttributeColor(R.attr.colorAccent) + } + +///////////////////////////////////////////////////////////////////////// +// タブレット対応で必要になった関数など + + private fun closeColumnSetting(): Boolean { + phoneTab({ env -> val vh = env.pager_adapter.getColumnViewHolder(env.pager.currentItem) - if(vh?.isColumnSettingShown == true) { + if (vh?.isColumnSettingShown == true) { vh.showColumnSetting(false) return@closeColumnSetting true } }, { env -> - for(i in 0 until env.tablet_layout_manager.childCount) { - - val columnViewHolder = when(val v = env.tablet_layout_manager.getChildAt(i)) { + for (i in 0 until env.tablet_layout_manager.childCount) { + + val columnViewHolder = when (val v = env.tablet_layout_manager.getChildAt(i)) { null -> null else -> (env.tablet_pager.getChildViewHolder(v) as? TabletColumnViewHolder)?.columnViewHolder } - - if(columnViewHolder?.isColumnSettingShown == true) { + + if (columnViewHolder?.isColumnSettingShown == true) { columnViewHolder.showColumnSetting(false) return@closeColumnSetting true } } }) - return false - } - - private fun addColumn(column : Column, indexArg : Int) : Int { - var index = indexArg - val size = app_state.column_list.size - if(index > size) index = size - - phoneOnly { env -> env.pager.adapter = null } - - app_state.column_list.add(index, column) - - phoneTab( + return false + } + + private fun addColumn(column: Column, indexArg: Int): Int { + var index = indexArg + val size = app_state.column_list.size + if (index > size) index = size + + phoneOnly { env -> env.pager.adapter = null } + + app_state.column_list.add(index, column) + + phoneTab( { env -> env.pager.adapter = env.pager_adapter }, { env -> resizeColumnWidth(env) } ) - - app_state.saveColumnList() - updateColumnStrip() - - return index - } - - private fun removeColumn(column : Column) { - val idx_column = app_state.column_list.indexOf(column) - if(idx_column == - 1) return - - phoneOnly { env -> env.pager.adapter = null } - - app_state.column_list.removeAt(idx_column).dispose() - - phoneTab( + + app_state.saveColumnList() + updateColumnStrip() + + return index + } + + private fun removeColumn(column: Column) { + val idx_column = app_state.column_list.indexOf(column) + if (idx_column == -1) return + + phoneOnly { env -> env.pager.adapter = null } + + app_state.column_list.removeAt(idx_column).dispose() + + phoneTab( { env -> env.pager.adapter = env.pager_adapter }, { env -> resizeColumnWidth(env) } ) - - app_state.saveColumnList() - updateColumnStrip() - } - - private fun setOrder(new_order : ArrayList) { - - phoneOnly { env -> env.pager.adapter = null } - - val ie = app_state.column_list.size - - val tmp_list = ArrayList() - val used_set = HashSet() - - // copy by new_order - for(i in new_order) { - if(0 <= i && i < ie) { - used_set.add(i) - tmp_list.add(app_state.column_list[i]) - } - } - - // dispose unused elements. - for(i in 0 until ie) { - if(used_set.contains(i)) continue - app_state.column_list[i].dispose() - } - app_state.column_list.clear() - app_state.column_list.addAll(tmp_list) - - phoneTab( + + app_state.saveColumnList() + updateColumnStrip() + } + + private fun setOrder(new_order: ArrayList) { + + phoneOnly { env -> env.pager.adapter = null } + + val ie = app_state.column_list.size + + val tmp_list = ArrayList() + val used_set = HashSet() + + // copy by new_order + for (i in new_order) { + if (0 <= i && i < ie) { + used_set.add(i) + tmp_list.add(app_state.column_list[i]) + } + } + + // dispose unused elements. + for (i in 0 until ie) { + if (used_set.contains(i)) continue + app_state.column_list[i].dispose() + } + app_state.column_list.clear() + app_state.column_list.addAll(tmp_list) + + phoneTab( { env -> env.pager.adapter = env.pager_adapter }, { env -> resizeColumnWidth(env) } ) - - app_state.saveColumnList() - updateColumnStrip() - } - - private fun resizeColumnWidth(env : TabletEnv) { - - var column_w_min_dp = COLUMN_WIDTH_MIN_DP - val sv = Pref.spColumnWidth(pref) - if(sv.isNotEmpty()) { - try { - val iv = Integer.parseInt(sv) - if(iv >= 100) { - column_w_min_dp = iv - } - } catch(ex : Throwable) { - log.trace(ex) - } - - } - - val dm = resources.displayMetrics - - val screen_width = dm.widthPixels - - val density = dm.density - var column_w_min = (0.5f + column_w_min_dp * density).toInt() - if(column_w_min < 1) column_w_min = 1 - - var column_w : Int - - if(screen_width < column_w_min * 2) { - // 最小幅で2つ表示できないのなら1カラム表示 - nScreenColumn = 1 - column_w = screen_width - } else { - - // カラム最小幅から計算した表示カラム数 - nScreenColumn = screen_width / column_w_min - if(nScreenColumn < 1) nScreenColumn = 1 - - // データのカラム数より大きくならないようにする - // (でも最小は1) - val column_count = app_state.column_list.size - if(column_count > 0 && column_count < nScreenColumn) { - nScreenColumn = column_count - } - - // 表示カラム数から計算したカラム幅 - column_w = screen_width / nScreenColumn - - // 最小カラム幅の1.5倍よりは大きくならないようにする - val column_w_max = (0.5f + column_w_min * 1.5f).toInt() - if(column_w > column_w_max) { - column_w = column_w_max - } - } - - nColumnWidth = column_w // dividerの幅を含む - - val divider_width = (0.5f + 1f * density).toInt() - column_w -= divider_width - env.tablet_pager_adapter.columnWidth = column_w // dividerの幅を含まない - // env.tablet_snap_helper.columnWidth = column_w //使われていない - - resizeAutoCW(column_w) // dividerの幅を含まない - - // 並べ直す - env.tablet_pager_adapter.notifyDataSetChanged() - } - - private fun scrollToColumn(index : Int, smoothScroll : Boolean = true) { - scrollColumnStrip(index) - phoneTab( - + + app_state.saveColumnList() + updateColumnStrip() + } + + private fun resizeColumnWidth(env: TabletEnv) { + + var column_w_min_dp = COLUMN_WIDTH_MIN_DP + val sv = Pref.spColumnWidth(pref) + if (sv.isNotEmpty()) { + try { + val iv = Integer.parseInt(sv) + if (iv >= 100) { + column_w_min_dp = iv + } + } catch (ex: Throwable) { + log.trace(ex) + } + + } + + val dm = resources.displayMetrics + + val screen_width = dm.widthPixels + + val density = dm.density + var column_w_min = (0.5f + column_w_min_dp * density).toInt() + if (column_w_min < 1) column_w_min = 1 + + var column_w: Int + + if (screen_width < column_w_min * 2) { + // 最小幅で2つ表示できないのなら1カラム表示 + nScreenColumn = 1 + column_w = screen_width + } else { + + // カラム最小幅から計算した表示カラム数 + nScreenColumn = screen_width / column_w_min + if (nScreenColumn < 1) nScreenColumn = 1 + + // データのカラム数より大きくならないようにする + // (でも最小は1) + val column_count = app_state.column_list.size + if (column_count > 0 && column_count < nScreenColumn) { + nScreenColumn = column_count + } + + // 表示カラム数から計算したカラム幅 + column_w = screen_width / nScreenColumn + + // 最小カラム幅の1.5倍よりは大きくならないようにする + val column_w_max = (0.5f + column_w_min * 1.5f).toInt() + if (column_w > column_w_max) { + column_w = column_w_max + } + } + + nColumnWidth = column_w // dividerの幅を含む + + val divider_width = (0.5f + 1f * density).toInt() + column_w -= divider_width + env.tablet_pager_adapter.columnWidth = column_w // dividerの幅を含まない + // env.tablet_snap_helper.columnWidth = column_w //使われていない + + resizeAutoCW(column_w) // dividerの幅を含まない + + // 並べ直す + env.tablet_pager_adapter.notifyDataSetChanged() + } + + private fun scrollToColumn(index: Int, smoothScroll: Boolean = true) { + scrollColumnStrip(index) + phoneTab( + // スマホはスムーススクロール基本ありだがたまにしない { env -> log.d("ipLastColumnPos beforeScroll=${env.pager.currentItem}") env.pager.setCurrentItem(index, smoothScroll) }, - + // タブレットでスムーススクロールさせると頻繁にオーバーランするので絶対しない { env -> log.d("ipLastColumnPos beforeScroll=${env.visibleColumnsIndices.first}") env.tablet_pager.scrollToPosition(index) } ) - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - - @Suppress("BlockingMethodInNonBlockingContext") - private fun importAppData(uri : Uri?) { - uri ?: return - - // remove all columns - phoneOnly { env -> env.pager.adapter = null } - - for(c in app_state.column_list) { - c.dispose() - } - app_state.column_list.clear() - - phoneTab( + } + +////////////////////////////////////////////////////////////////////////////////////////////// + + @Suppress("BlockingMethodInNonBlockingContext") + private fun importAppData(uri: Uri) { + + // remove all columns + phoneOnly { env -> env.pager.adapter = null } + + for (c in app_state.column_list) { + c.dispose() + } + app_state.column_list.clear() + + phoneTab( { env -> env.pager.adapter = env.pager_adapter }, { env -> resizeColumnWidth(env) } ) - - updateColumnStrip() - - - runWithProgress( + + updateColumnStrip() + + + runWithProgress( "importing app data", - + doInBackground = { progress -> - fun setProgressMessage(sv : String) = + fun setProgressMessage(sv: String) = runOnMainLooper { progress.setMessageEx(sv) } - - var newColumnList : ArrayList? = null - + + var newColumnList: ArrayList? = null + setProgressMessage("import data to local storage...") - + // アプリ内領域に一時ファイルを作ってコピーする val cacheDir = cacheDir cacheDir.mkdir() @@ -2674,7 +2631,7 @@ class ActMain : AsyncActivity(), Column.Callback, View.OnClickListener, "SubwayTooter.${Process.myPid()}.${Process.myTid()}.tmp" ) val source = contentResolver.openInputStream(uri) - if(source == null) { + if (source == null) { showToast(true, "openInputStream failed.") return@runWithProgress null } @@ -2683,35 +2640,35 @@ class ActMain : AsyncActivity(), Column.Callback, View.OnClickListener, IOUtils.copy(inStream, outStream) } } - + // 通知サービスを止める setProgressMessage("syncing notification poller…") PollingWorker.queueAppDataImportBefore(this@ActMain) - while(PollingWorker.mBusyAppDataImportBefore.get()) { + while (PollingWorker.mBusyAppDataImportBefore.get()) { delay(1000L) log.d("syncing polling task...") } - + // データを読み込む setProgressMessage("reading app data...") var zipEntryCount = 0 try { ZipInputStream(FileInputStream(file)).use { zipStream -> - while(true) { + while (true) { val entry = zipStream.nextEntry ?: break - ++ zipEntryCount + ++zipEntryCount try { // val entryName = entry.name - if(entryName.endsWith(".json")) { + if (entryName.endsWith(".json")) { newColumnList = AppDataExporter.decodeAppData( this@ActMain, JsonReader(InputStreamReader(zipStream, "UTF-8")) ) continue } - - if(AppDataExporter.restoreBackgroundImage( + + if (AppDataExporter.restoreBackgroundImage( this@ActMain, newColumnList, zipStream, @@ -2725,14 +2682,14 @@ class ActMain : AsyncActivity(), Column.Callback, View.OnClickListener, } } } - } catch(ex : Throwable) { + } catch (ex: Throwable) { log.trace(ex) - if(zipEntryCount != 0) { + if (zipEntryCount != 0) { showToast(ex, "importAppData failed.") } } // zipではなかった場合、zipEntryがない状態になる。例外はPH-1では出なかったが、出ても問題ないようにする。 - if(zipEntryCount == 0) { + if (zipEntryCount == 0) { InputStreamReader(FileInputStream(file), "UTF-8").use { inStream -> newColumnList = AppDataExporter.decodeAppData( this@ActMain, @@ -2740,20 +2697,20 @@ class ActMain : AsyncActivity(), Column.Callback, View.OnClickListener, ) } } - + newColumnList }, afterProc = { // cancelled. - if(it == null) return@runWithProgress - + if (it == null) return@runWithProgress + try { phoneOnly { env -> env.pager.adapter = null } - + app_state.column_list.clear() app_state.column_list.addAll(it) app_state.saveColumnList() - + phoneTab( { env -> env.pager.adapter = env.pager_adapter }, { env -> resizeColumnWidth(env) } @@ -2763,7 +2720,7 @@ class ActMain : AsyncActivity(), Column.Callback, View.OnClickListener, // 通知サービスをリスタート PollingWorker.queueAppDataImportAfter(this@ActMain) } - + showToast(true, R.string.import_completed_please_restart_app) finish() }, @@ -2774,144 +2731,154 @@ class ActMain : AsyncActivity(), Column.Callback, View.OnClickListener, window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) } ) - } - - override fun onDrawerSlide(drawerView : View, slideOffset : Float) { - post_helper.closeAcctPopup() - } - - override fun onDrawerOpened(drawerView : View) { - post_helper.closeAcctPopup() - } - - override fun onDrawerClosed(drawerView : View) { - post_helper.closeAcctPopup() - } - - override fun onDrawerStateChanged(newState : Int) { - post_helper.closeAcctPopup() - } - - private fun resizeAutoCW(column_w : Int) { - val sv = Pref.spAutoCWLines(pref) - nAutoCwLines = sv.optInt() ?: - 1 - if(nAutoCwLines > 0) { - val lv_pad = (0.5f + 12 * density).toInt() - val icon_width = avatarIconSize - val icon_end = (0.5f + 4 * density).toInt() - nAutoCwCellWidth = column_w - lv_pad * 2 - icon_width - icon_end - } - // この後各カラムは再描画される - } - - fun checkAutoCW(status : TootStatus, text : CharSequence) { - if(nAutoCwCellWidth <= 0) { - // 設定が無効 - status.auto_cw = null - return - } - - var auto_cw = status.auto_cw - if(auto_cw != null && - auto_cw.refActivity?.get() === this@ActMain && - auto_cw.cell_width == nAutoCwCellWidth - ) { - // 以前に計算した値がまだ使える - return - } - - if(auto_cw == null) { - auto_cw = TootStatus.AutoCW() - status.auto_cw = auto_cw - } - - // 計算時の条件(文字フォント、文字サイズ、カラム幅)を覚えておいて、再利用時に同じか確認する - auto_cw.refActivity = WeakReference(this@ActMain) - auto_cw.cell_width = nAutoCwCellWidth - auto_cw.decoded_spoiler_text = null - - // テキストをレイアウトして行数を測定 - val tv = TextView(this).apply { - layoutParams = - LinearLayout.LayoutParams(nAutoCwCellWidth, LinearLayout.LayoutParams.WRAP_CONTENT) - if(! timeline_font_size_sp.isNaN()) - textSize = timeline_font_size_sp - - val fv = timeline_spacing - if(fv != null) setLineSpacing(0f, fv) - - typeface = timeline_font - this.text = text - } - - tv.measure( + } + + override fun onDrawerSlide(drawerView: View, slideOffset: Float) { + post_helper.closeAcctPopup() + } + + override fun onDrawerOpened(drawerView: View) { + post_helper.closeAcctPopup() + } + + override fun onDrawerClosed(drawerView: View) { + post_helper.closeAcctPopup() + } + + override fun onDrawerStateChanged(newState: Int) { + post_helper.closeAcctPopup() + } + + private fun resizeAutoCW(column_w: Int) { + val sv = Pref.spAutoCWLines(pref) + nAutoCwLines = sv.optInt() ?: -1 + if (nAutoCwLines > 0) { + val lv_pad = (0.5f + 12 * density).toInt() + val icon_width = avatarIconSize + val icon_end = (0.5f + 4 * density).toInt() + nAutoCwCellWidth = column_w - lv_pad * 2 - icon_width - icon_end + } + // この後各カラムは再描画される + } + + fun checkAutoCW(status: TootStatus, text: CharSequence) { + if (nAutoCwCellWidth <= 0) { + // 設定が無効 + status.auto_cw = null + return + } + + var auto_cw = status.auto_cw + if (auto_cw != null && + auto_cw.refActivity?.get() === this@ActMain && + auto_cw.cell_width == nAutoCwCellWidth + ) { + // 以前に計算した値がまだ使える + return + } + + if (auto_cw == null) { + auto_cw = TootStatus.AutoCW() + status.auto_cw = auto_cw + } + + // 計算時の条件(文字フォント、文字サイズ、カラム幅)を覚えておいて、再利用時に同じか確認する + auto_cw.refActivity = WeakReference(this@ActMain) + auto_cw.cell_width = nAutoCwCellWidth + auto_cw.decoded_spoiler_text = null + + // テキストをレイアウトして行数を測定 + val tv = TextView(this).apply { + layoutParams = + LinearLayout.LayoutParams(nAutoCwCellWidth, LinearLayout.LayoutParams.WRAP_CONTENT) + if (!timeline_font_size_sp.isNaN()) + textSize = timeline_font_size_sp + + val fv = timeline_spacing + if (fv != null) setLineSpacing(0f, fv) + + typeface = timeline_font + this.text = text + } + + tv.measure( View.MeasureSpec.makeMeasureSpec(nAutoCwCellWidth, View.MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED) ) - val l = tv.layout - if(l != null) { - auto_cw.originalLineCount = l.lineCount - val line_count = auto_cw.originalLineCount - - if((nAutoCwLines > 0 && line_count > nAutoCwLines) - && status.spoiler_text.isEmpty() - && (status.mentions?.size ?: 0) <= nAutoCwLines - ) { - val sb = SpannableStringBuilder() - sb.append(getString(R.string.auto_cw_prefix)) - sb.append(text, 0, l.getLineEnd(nAutoCwLines - 1)) - var last = sb.length - while(last > 0) { - val c = sb[last - 1] - if(c == '\n' || Character.isWhitespace(c)) { - -- last - continue - } - break - } - if(last < sb.length) { - sb.delete(last, sb.length) - } - sb.append('…') - auto_cw.decoded_spoiler_text = sb - } - } - } - - private fun checkPrivacyPolicy() { - - // 既に表示中かもしれない - if(dlgPrivacyPolicy?.get()?.isShowing == true) return - - val res_id = when(getString(R.string.language_code)) { + val l = tv.layout + if (l != null) { + auto_cw.originalLineCount = l.lineCount + val line_count = auto_cw.originalLineCount + + if ((nAutoCwLines > 0 && line_count > nAutoCwLines) + && status.spoiler_text.isEmpty() + && (status.mentions?.size ?: 0) <= nAutoCwLines + ) { + val sb = SpannableStringBuilder() + sb.append(getString(R.string.auto_cw_prefix)) + sb.append(text, 0, l.getLineEnd(nAutoCwLines - 1)) + var last = sb.length + while (last > 0) { + val c = sb[last - 1] + if (c == '\n' || Character.isWhitespace(c)) { + --last + continue + } + break + } + if (last < sb.length) { + sb.delete(last, sb.length) + } + sb.append('…') + auto_cw.decoded_spoiler_text = sb + } + } + } + + private fun checkPrivacyPolicy() { + + // 既に表示中かもしれない + if (dlgPrivacyPolicy?.get()?.isShowing == true) return + + val res_id = when (getString(R.string.language_code)) { "ja" -> R.raw.privacy_policy_ja "fr" -> R.raw.privacy_policy_fr - else -> R.raw.privacy_policy_en - } - - // プライバシーポリシーデータの読み込み - val bytes = loadRawResource(res_id) - if(bytes.isEmpty()) return - - // 同意ずみなら表示しない - val digest = bytes.digestSHA256().encodeBase64Url() - if(digest == Pref.spAgreedPrivacyPolicyDigest(pref)) return - - val dialog = AlertDialog.Builder(this) - .setTitle(R.string.privacy_policy) - .setMessage(bytes.decodeUTF8()) - .setNegativeButton(R.string.cancel) { _, _ -> - finish() - } - .setOnCancelListener { - finish() - } - .setPositiveButton(R.string.agree) { _, _ -> - pref.edit().put(Pref.spAgreedPrivacyPolicyDigest, digest).apply() - } - .create() - dlgPrivacyPolicy = WeakReference(dialog) - dialog.show() - } - + else -> R.raw.privacy_policy_en + } + + // プライバシーポリシーデータの読み込み + val bytes = loadRawResource(res_id) + if (bytes.isEmpty()) return + + // 同意ずみなら表示しない + val digest = bytes.digestSHA256().encodeBase64Url() + if (digest == Pref.spAgreedPrivacyPolicyDigest(pref)) return + + val dialog = AlertDialog.Builder(this) + .setTitle(R.string.privacy_policy) + .setMessage(bytes.decodeUTF8()) + .setNegativeButton(R.string.cancel) { _, _ -> + finish() + } + .setOnCancelListener { + finish() + } + .setPositiveButton(R.string.agree) { _, _ -> + pref.edit().put(Pref.spAgreedPrivacyPolicyDigest, digest).apply() + } + .create() + dlgPrivacyPolicy = WeakReference(dialog) + dialog.show() + } + + private fun searchFromActivityResult(data: Intent?, columnType: ColumnType) = + data?.getStringExtra(Intent.EXTRA_TEXT)?.let { + addColumn( + false, + defaultInsertPosition, + SavedAccount.na, + columnType, + it + ) + } } diff --git a/app/src/main/java/jp/juggler/subwaytooter/PollingForegrounder.kt b/app/src/main/java/jp/juggler/subwaytooter/PollingForegrounder.kt index f5ee9eed..35afbfaf 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/PollingForegrounder.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/PollingForegrounder.kt @@ -14,6 +14,7 @@ import androidx.core.content.ContextCompat import jp.juggler.util.LogCategory import jp.juggler.subwaytooter.util.NotificationHelper +import kotlinx.coroutines.runBlocking class PollingForegrounder : IntentService("PollingForegrounder") { @@ -89,17 +90,17 @@ class PollingForegrounder : IntentService("PollingForegrounder") { override fun onHandleIntent(intent : Intent?) { if(intent == null) return - val tag = intent.getStringExtra(PollingWorker.EXTRA_TAG) - val context = applicationContext - PollingWorker.handleFCMMessage(this, tag, object : PollingWorker.JobStatusCallback { - override fun onStatus(sv : String) { - if(sv.isNotEmpty() && sv != last_status) { - log.d("onStatus %s", sv) - last_status = sv - startForeground(NOTIFICATION_ID_FOREGROUNDER, createNotification(context, sv)) - } + runBlocking { + val tag = intent.getStringExtra(PollingWorker.EXTRA_TAG) + val context = applicationContext + PollingWorker.handleFCMMessage(context, tag) { sv -> + if (sv.isEmpty() || sv==last_status) return@handleFCMMessage + // 状況が変化したらログと通知領域に出力する + last_status = sv + log.d("onStatus %s", sv) + startForeground(NOTIFICATION_ID_FOREGROUNDER, createNotification(context, sv)) } - }) + } } } diff --git a/app/src/main/java/jp/juggler/subwaytooter/PollingWorker.kt b/app/src/main/java/jp/juggler/subwaytooter/PollingWorker.kt index 48c0d2e8..cc52d8e4 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/PollingWorker.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/PollingWorker.kt @@ -15,7 +15,6 @@ import android.net.ConnectivityManager import android.net.Uri import android.net.wifi.WifiManager import android.os.Build -import android.os.Handler import android.os.PowerManager import android.os.SystemClock import android.service.notification.StatusBarNotification @@ -24,6 +23,7 @@ import androidx.core.content.ContextCompat import com.google.firebase.messaging.FirebaseMessaging import jp.juggler.subwaytooter.api.TootApiCallback import jp.juggler.subwaytooter.api.TootApiClient +import jp.juggler.subwaytooter.api.TootApiResult import jp.juggler.subwaytooter.api.TootParser import jp.juggler.subwaytooter.api.entity.* import jp.juggler.subwaytooter.table.* @@ -31,7 +31,9 @@ import jp.juggler.subwaytooter.table.NotificationCache.Companion.getEntityOrderI import jp.juggler.subwaytooter.table.NotificationCache.Companion.parseNotificationType import jp.juggler.subwaytooter.util.* import jp.juggler.util.* -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.ClosedReceiveChannelException import kotlinx.coroutines.tasks.await import okhttp3.Call import okhttp3.Request @@ -41,16 +43,12 @@ import java.util.* import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicReference +import kotlin.coroutines.coroutineContext import kotlin.math.max import kotlin.math.min class PollingWorker private constructor(contextArg: Context) { - interface JobStatusCallback { - - fun onStatus(sv: String) - } - enum class TrackingType(val str: String) { All("all"), Reply("reply"), @@ -68,6 +66,14 @@ class PollingWorker private constructor(contextArg: Context) { } + internal class Data(val access_info: SavedAccount, val notification: TootNotification) + + internal class InjectData { + + var account_db_id: Long = 0 + val list = ArrayList() + } + companion object { internal val log = LogCategory("PollingWorker") @@ -122,45 +128,44 @@ class PollingWorker private constructor(contextArg: Context) { return s } - suspend fun getFirebaseMessagingToken(context:Context):String?{ - val prefDevice = PrefDevice.prefDevice(context) - // 設定ファイルに保持されていたらそれを使う - prefDevice - .getString(PrefDevice.KEY_DEVICE_TOKEN, null) - ?.notEmpty()?.let{ return it} + suspend fun getFirebaseMessagingToken(context: Context): String? { + val prefDevice = PrefDevice.prefDevice(context) + // 設定ファイルに保持されていたらそれを使う + prefDevice + .getString(PrefDevice.KEY_DEVICE_TOKEN, null) + ?.notEmpty()?.let { return it } - // 古い形式 - // return FirebaseInstanceId.getInstance().getToken(FCM_SENDER_ID, FCM_SCOPE) - - // com.google.firebase:firebase-messaging.20.3.0 以降 - // implementation "org.jetbrains.kotlinx:kotlinx-coroutines-play-services:$kotlinx_coroutines_version" - try{ - val sv = FirebaseMessaging.getInstance().token.await() - return if (sv.isNullOrBlank()) { - log.e("getFirebaseMessagingToken: missing device token.") - null - } else { - prefDevice - .edit() - .putString(PrefDevice.KEY_DEVICE_TOKEN, sv) - .apply() - sv - } - }catch(ex:Throwable){ - log.trace(ex, "getFirebaseMessagingToken: could not get device token.") - return null - } - } + // 古い形式 + // return FirebaseInstanceId.getInstance().getToken(FCM_SENDER_ID, FCM_SCOPE) + // com.google.firebase:firebase-messaging.20.3.0 以降 + // implementation "org.jetbrains.kotlinx:kotlinx-coroutines-play-services:$kotlinx_coroutines_version" + try { + val sv = FirebaseMessaging.getInstance().token.await() + return if (sv.isNullOrBlank()) { + log.e("getFirebaseMessagingToken: missing device token.") + null + } else { + prefDevice + .edit() + .putString(PrefDevice.KEY_DEVICE_TOKEN, sv) + .apply() + sv + } + } catch (ex: Throwable) { + log.trace(ex, "getFirebaseMessagingToken: could not get device token.") + return null + } + } // インストールIDを生成する前に、各データの通知登録キャッシュをクリアする // トークンがまだ生成されていない場合、このメソッドは null を返します。 @Suppress("BlockingMethodInNonBlockingContext") suspend fun prepareInstallId( - context: Context, - job: JobItem? = null - ): String? { + context: Context, + job: JobItem? = null + ): String? { val prefDevice = PrefDevice.prefDevice(context) var sv = prefDevice.getString(PrefDevice.KEY_INSTALL_ID, null) @@ -169,8 +174,8 @@ class PollingWorker private constructor(contextArg: Context) { SavedAccount.clearRegistrationCache() try { - val device_token = getFirebaseMessagingToken(context) - ?: return null + val device_token = getFirebaseMessagingToken(context) + ?: return null val request = Request.Builder() .url("$APP_SERVER/counter") @@ -184,11 +189,11 @@ class PollingWorker private constructor(contextArg: Context) { if (!response.isSuccessful || body?.isEmpty() != false) { log.e( - TootApiClient.formatResponse( - response, - "getInstallId: get /counter failed." - ) - ) + TootApiClient.formatResponse( + response, + "getInstallId: get/counter failed." + ) + ) return null } @@ -224,14 +229,14 @@ class PollingWorker private constructor(contextArg: Context) { val minute = 60000L val intervalMillis = max( - minute * 5L, - minute * Pref.spPullNotificationCheckInterval.toInt(context.pref()) - ) + minute * 5L, + minute * Pref.spPullNotificationCheckInterval.toInt(context.pref()) + ) val flexMillis = max( - minute, - intervalMillis shr 1 - ) + minute, + intervalMillis shr 1 + ) fun JobInfo.Builder.setPeriodicCompat(intervalMillis: Long, flexMillis: Long) = this.apply { @@ -261,19 +266,19 @@ class PollingWorker private constructor(contextArg: Context) { // タスクの追加 private fun addTask( - context: Context, - removeOld: Boolean, - task_id: Int, - taskDataArg: JsonObject? - ) { + context: Context, + removeOld: Boolean, + task_id: Int, + taskDataArg: JsonObject? + ) { try { task_list.addLast( - context, - removeOld, - (taskDataArg ?: JsonObject()).apply { - put(EXTRA_TASK_ID, task_id) - } - ) + context, + removeOld, + (taskDataArg ?: JsonObject()).apply { + put(EXTRA_TASK_ID, task_id) + } + ) scheduleJob(context, JOB_TASK) } catch (ex: Throwable) { log.trace(ex) @@ -286,10 +291,10 @@ class PollingWorker private constructor(contextArg: Context) { } fun injectData( - context: Context, - account: SavedAccount, - src: List - ) { + context: Context, + account: SavedAccount, + src: List + ) { if (src.isEmpty()) return @@ -317,17 +322,17 @@ class PollingWorker private constructor(contextArg: Context) { uri ?: return null return jsonObject { putNotNull( - EXTRA_DB_ID, - uri.getQueryParameter("db_id")?.toLongOrNull() - ) + EXTRA_DB_ID, + uri.getQueryParameter("db_id")?.toLongOrNull() + ) putNotNull( - EXTRA_NOTIFICATION_TYPE, - uri.getQueryParameter("type")?.notEmpty() - ) + EXTRA_NOTIFICATION_TYPE, + uri.getQueryParameter("type")?.notEmpty() + ) putNotNull( - EXTRA_NOTIFICATION_ID, - uri.getQueryParameter("notificationId")?.notEmpty() - ) + EXTRA_NOTIFICATION_ID, + uri.getQueryParameter("notificationId")?.notEmpty() + ) } } @@ -371,29 +376,43 @@ class PollingWorker private constructor(contextArg: Context) { addTask(context, true, TASK_PACKAGE_REPLACED, null) } - internal val job_status = AtomicReference(null) + private val job_status = AtomicReference(null) - fun handleFCMMessage(context: Context, tag: String?, callback: JobStatusCallback) { - log.d("handleFCMMessage: start. tag=$tag") - val time_start = SystemClock.elapsedRealtime() - - callback.onStatus("=>") - - // タスクを追加 - val data = JsonObject().apply { - try { - putNotNull(EXTRA_TAG, tag) - this[EXTRA_TASK_ID] = TASK_FCM_MESSAGE - } catch (_: JsonException) { - } + private var workerStatus: String + get() = job_status.get() + set(x) { + log.d("workerStatus:$x") + job_status.set(x) } - task_list.addLast(context, true, data) + // IntentServiceが作ったスレッドから呼ばれる + suspend fun handleFCMMessage( + context: Context, + tag: String?, + progress: (String) -> Unit + ) { + log.d("handleFCMMessage: start. tag=$tag") - callback.onStatus("==>") + val time_start = SystemClock.elapsedRealtime() + + // この呼出でIntentServiceがstartForegroundする + progress("=>") + + // タスクを追加 + task_list.addLast( + context, + true, + JsonObject().apply { + this[EXTRA_TASK_ID] = TASK_FCM_MESSAGE + if (tag != null) this[EXTRA_TAG] = tag + } + ) + + progress("==>") // 疑似ジョブを開始 val pw = getInstance(context) + pw.addJobFCM() // 疑似ジョブが終了するまで待機する @@ -402,54 +421,40 @@ class PollingWorker private constructor(contextArg: Context) { val now = SystemClock.elapsedRealtime() if (!pw.hasJob(JOB_FCM)) { log.d( - "handleFCMMessage: JOB_FCM completed. time=%.2f", - (now - time_start) / 1000f - ) + "handleFCMMessage: JOB_FCM completed. time=%.2f", + (now - time_start) / 1000f + ) break } + // ジョブの状況を通知する - var sv: String? = job_status.get() - if (sv == null) sv = "(null)" - callback.onStatus(sv) + progress(job_status.get() ?: "(null)") // 少し待機 - try { - Thread.sleep(50L) - } catch (ex: InterruptedException) { - log.e(ex, "handleFCMMessage: blocking is interrupted.") - break - } - + delay(50L) } } } + internal val context: Context private val appState: AppState - internal val handler: Handler internal val pref: SharedPreferences private val connectivityManager: ConnectivityManager internal val notification_manager: NotificationManager internal val scheduler: JobScheduler private val power_manager: PowerManager? - internal val power_lock: PowerManager.WakeLock + private val power_lock: PowerManager.WakeLock private val wifi_manager: WifiManager? - internal val wifi_lock: WifiManager.WifiLock - - private var worker: Worker + private val wifi_lock: WifiManager.WifiLock internal val job_list = LinkedList() - internal class Data(val access_info: SavedAccount, val notification: TootNotification) - - internal class InjectData { - - var account_db_id: Long = 0 - val list = ArrayList() - } + private val workerJob: Job + private val workerNotifier = Channel(capacity = Channel.CONFLATED) init { - log.d("ctor") + log.d("init") val context = contextArg.applicationContext @@ -457,9 +462,8 @@ class PollingWorker private constructor(contextArg: Context) { // クラッシュレポートによると App1.onCreate より前にここを通る場合がある // データベースへアクセスできるようにする - this.appState = App1.prepare(context, "PollingWorker.ctor()") + this.appState = App1.prepare(context, "PollingWorker.init") this.pref = App1.pref - this.handler = appState.handler this.connectivityManager = systemService(context) ?: error("missing ConnectivityManager system service") @@ -479,16 +483,16 @@ class PollingWorker private constructor(contextArg: Context) { ?: error("missing WifiManager system service") power_lock = power_manager.newWakeLock( - PowerManager.PARTIAL_WAKE_LOCK, - PollingWorker::class.java.name - ) + PowerManager.PARTIAL_WAKE_LOCK, + PollingWorker::class.java.name + ) power_lock.setReferenceCounted(false) wifi_lock = if (Build.VERSION.SDK_INT >= 29) { wifi_manager.createWifiLock( - WifiManager.WIFI_MODE_FULL_HIGH_PERF, - PollingWorker::class.java.name - ) + WifiManager.WIFI_MODE_FULL_HIGH_PERF, + PollingWorker::class.java.name + ) } else { @Suppress("DEPRECATION") wifi_manager.createWifiLock(PollingWorker::class.java.name) @@ -496,99 +500,87 @@ class PollingWorker private constructor(contextArg: Context) { wifi_lock.setReferenceCounted(false) - // - worker = Worker() - worker.start() + workerJob = GlobalScope.launch(Dispatchers.Default) { worker() } } - inner class Worker : WorkerBase() { - - val bThreadCancelled = AtomicBoolean(false) - - override fun cancel() { - bThreadCancelled.set(true) - notifyEx() + @SuppressLint("WakelockTimeout") + private fun acquirePowerLock() { + log.d("acquire power lock...") + try { + if (!power_lock.isHeld) { + power_lock.acquire() + } + } catch (ex: Throwable) { + log.trace(ex) } - @SuppressLint("WakelockTimeout") - private fun acquirePowerLock() { - log.d("acquire power lock...") - try { - if (!power_lock.isHeld) { - power_lock.acquire() - } - } catch (ex: Throwable) { - log.trace(ex) + try { + if (!wifi_lock.isHeld) { + wifi_lock.acquire() } + } catch (ex: Throwable) { + log.trace(ex) + } + } - try { - if (!wifi_lock.isHeld) { - wifi_lock.acquire() - } - } catch (ex: Throwable) { - log.trace(ex) + private fun releasePowerLock() { + log.d("release power lock...") + try { + if (power_lock.isHeld) { + power_lock.release() } + } catch (ex: Throwable) { + log.trace(ex) } - private fun releasePowerLock() { - log.d("release power lock...") - try { - if (power_lock.isHeld) { - power_lock.release() - } - } catch (ex: Throwable) { - log.trace(ex) + try { + if (wifi_lock.isHeld) { + wifi_lock.release() } - - try { - if (wifi_lock.isHeld) { - wifi_lock.release() - } - } catch (ex: Throwable) { - log.trace(ex) - } - + } catch (ex: Throwable) { + log.trace(ex) } + } - override fun run() { - log.d("worker thread start.") - job_status.set("worker thread start.") - while (!bThreadCancelled.get()) { - try { - val item: JobItem? = synchronized(job_list) { + private suspend fun worker() { + workerStatus = "worker start." + try { + suspend fun isActive() = coroutineContext[Job]?.isActive == true + while (isActive()) { + while (true) { + handleJobItem(synchronized(job_list) { for (ji in job_list) { - if (bThreadCancelled.get()) break if (ji.mJobCancelled_.get()) continue if (ji.mWorkerAttached.compareAndSet(false, true)) { return@synchronized ji } } null - } - - if (item == null) { - job_status.set("no job to run.") - waitEx(86400000L) - continue - } - - job_status.set("start job " + item.jobId) - acquirePowerLock() - try { - item.refWorker.set(this@Worker) - item.run() - } finally { - job_status.set("end job " + item.jobId) - item.refWorker.set(null) - releasePowerLock() - } - } catch (ex: Throwable) { - log.trace(ex) + } ?: break) + } + try { + workerNotifier.receive() + } catch (ex: ClosedReceiveChannelException) { } - } - job_status.set("worker thread end.") - log.d("worker thread end.") + } finally { + workerStatus = "worker end." + } + } + + private suspend fun handleJobItem(item: JobItem) { + try { + workerStatus = "start job ${item.jobId}" + acquirePowerLock() + try { + item.run() + } finally { + releasePowerLock() + } + } catch (ex: Throwable) { + log.trace(ex) + } finally { + workerStatus = "end job ${item.jobId}" } } @@ -612,7 +604,7 @@ class PollingWorker private constructor(contextArg: Context) { } // JobService#onStartJob から呼ばれる - fun onStartJob(jobService: JobService, params: JobParameters): Boolean { + suspend fun onStartJob(jobService: JobService, params: JobParameters): Boolean { val item = JobItem(jobService, params) addJob(item, true) return true @@ -631,11 +623,11 @@ class PollingWorker private constructor(contextArg: Context) { } // FCMメッセージイベントから呼ばれる - private fun addJobFCM() { + private suspend fun addJobFCM() { addJob(JobItem(JOB_FCM), false) } - private fun addJob(item: JobItem, bRemoveOld: Boolean) { + private suspend fun addJob(item: JobItem, bRemoveOld: Boolean) { val jobId = item.jobId // 同じジョブ番号がジョブリストにあるか? @@ -656,7 +648,8 @@ class PollingWorker private constructor(contextArg: Context) { job_list.add(item) } - worker.notifyEx() + workerNotifier.send(Unit) + } // JobService#onStopJob から呼ばれる @@ -705,14 +698,9 @@ class PollingWorker private constructor(contextArg: Context) { var current_call: Call? = null - val refWorker = AtomicReference(null) val isJobCancelled: Boolean - get() { - if (mJobCancelled_.get()) return true - val worker = refWorker.get() - return worker != null && worker.bThreadCancelled.get() - } + get() = mJobCancelled_.get() || workerJob.isCancelled constructor(jobService: JobService, params: JobParameters) { this.jobParams = params @@ -726,45 +714,34 @@ class PollingWorker private constructor(contextArg: Context) { this.refJobService = null } - fun notifyWorkerThread() { - val worker = refWorker.get() - worker?.notifyEx() - } - - fun waitWorkerThread(ms: Long) { - val worker = refWorker.get() - worker?.waitEx(ms) - } - fun cancel(bReschedule: Boolean) { mJobCancelled_.set(true) mReschedule.set(bReschedule) current_call?.cancel() - notifyWorkerThread() + runBlocking { workerNotifier.send(Unit) } } - fun run() { - - job_status.set("job start.") + suspend fun run() = coroutineScope { + workerStatus = "job start." try { log.d("(JobItem.run jobId=${jobId}") if (isJobCancelled) throw JobCancelledException() - job_status.set("check network status..") + workerStatus = "check network status.." - val net_wait_start = SystemClock.elapsedRealtime() - while (true) { - val connectionState = App1.getAppState(context, "PollingWorker.JobItem.run()") - .networkTracker.connectionState - ?: break - if (isJobCancelled) throw JobCancelledException() - val now = SystemClock.elapsedRealtime() - val delta = now - net_wait_start - if (delta >= 10000L) { - log.d("network state timeout. $connectionState") - break + var connectionState: String? = null + try { + withTimeout(10000L) { + while (true) { + if (isJobCancelled) throw JobCancelledException() + connectionState = App1.getAppState(context, "PollingWorker.JobItem.run()") + .networkTracker.connectionState + ?: break // null if connected + delay(333L) + } } - waitWorkerThread(333L) + } catch (ex: TimeoutCancellationException) { + log.d("network state timeout. $connectionState") } muted_app = MutedApp.nameSet @@ -783,7 +760,8 @@ class PollingWorker private constructor(contextArg: Context) { // タスクがなかった場合でも定期実行ジョブからの実行ならポーリングを行う TaskRunner().runTask(this@JobItem, TASK_POLLING, JsonObject()) } - job_status.set("make next schedule.") + + workerStatus = "make next schedule." log.d("pollingComplete=${bPollingComplete},isJobCancelled=${isJobCancelled},bPollingRequired=${bPollingRequired.get()}") @@ -809,38 +787,37 @@ class PollingWorker private constructor(contextArg: Context) { log.trace(ex) log.e(ex, "job execution failed.") } finally { - job_status.set("job finished.") + workerStatus = "job finished." } - // ジョブ終了報告 - if (!isJobCancelled) { - handler.post(Runnable { - if (isJobCancelled) return@Runnable - synchronized(job_list) { - job_list.remove(this@JobItem) - } - - try { - val jobService = refJobService?.get() - if (jobService != null) { - val willReschedule = mReschedule.get() - log.d("sending jobFinished. willReschedule=$willReschedule") - jobService.jobFinished(jobParams, willReschedule) - } - } catch (ex: Throwable) { - log.trace(ex, "jobFinished failed(1).") - } - }) - } log.d(")JobItem.run jobId=${jobId}, cancel=${isJobCancelled}") - } + launch(Dispatchers.Main) { + if (isJobCancelled) return@launch + + synchronized(job_list) { + job_list.remove(this@JobItem) + } + + refJobService?.get()?.let { jobService -> + try { + // ジョブ終了報告 + val willReschedule = mReschedule.get() + log.d("sending jobFinished. willReschedule=$willReschedule") + jobService.jobFinished(jobParams, willReschedule) + } catch (ex: Throwable) { + log.trace(ex, "jobFinished failed(1).") + } + } + } + } } + private fun TrackingType.trackingTypeName() = when (this) { - TrackingType.NotReply -> NotificationHelper.TRACKING_NAME_DEFAULT - TrackingType.Reply -> NotificationHelper.TRACKING_NAME_REPLY - TrackingType.All -> NotificationHelper.TRACKING_NAME_DEFAULT + TrackingType.NotReply -> NotificationHelper.TRACKING_NAME_DEFAULT + TrackingType.Reply -> NotificationHelper.TRACKING_NAME_REPLY + TrackingType.All -> NotificationHelper.TRACKING_NAME_DEFAULT } internal inner class TaskRunner { @@ -850,323 +827,329 @@ class PollingWorker private constructor(contextArg: Context) { val error_instance = ArrayList() - fun runTask(job: JobItem, taskId: Int, taskData: JsonObject) { - try { - log.d("(runTask: taskId=${taskId}") - job_status.set("start task $taskId") + suspend fun runTask(job: JobItem, taskId: Int, taskData: JsonObject) { + workerStatus = "start task $taskId" - this.job = job - this.taskId = taskId + this.job = job + this.taskId = taskId + var process_db_id = -1L // - var process_db_id = -1L // + coroutineScope { + try { + when (taskId) { + TASK_APP_DATA_IMPORT_BEFORE -> { + scheduler.cancelAll() + for (a in SavedAccount.loadAccountList(context)) { + try { + val notification_tag = a.db_id.toString() + notification_manager.cancel(notification_tag, NOTIFICATION_ID) + } catch (ex: Throwable) { + log.trace(ex) + } - when (taskId) { - TASK_APP_DATA_IMPORT_BEFORE -> { - scheduler.cancelAll() - for (a in SavedAccount.loadAccountList(context)) { - try { - val notification_tag = a.db_id.toString() - notification_manager.cancel(notification_tag, NOTIFICATION_ID) - } catch (ex: Throwable) { - log.trace(ex) - } + } + mBusyAppDataImportBefore.set(false) + return@coroutineScope + } - } - mBusyAppDataImportBefore.set(false) - return - } + TASK_APP_DATA_IMPORT_AFTER -> { + mBusyAppDataImportAfter.set(false) + mBusyAppDataImportBefore.set(false) + NotificationTracking.resetPostAll() + // fall + } - TASK_APP_DATA_IMPORT_AFTER -> { - mBusyAppDataImportAfter.set(false) - mBusyAppDataImportBefore.set(false) - NotificationTracking.resetPostAll() - // fall - } - - } - - // アプリデータのインポート処理がビジーな間、他のジョブは実行されない - if (mBusyAppDataImportBefore.get() || mBusyAppDataImportAfter.get()) return - - // タスクによってはポーリング前にすることがある - when (taskId) { - TASK_DATA_INJECTED -> processInjectedData() - - TASK_BOOT_COMPLETED -> NotificationTracking.resetPostAll() - - TASK_PACKAGE_REPLACED -> NotificationTracking.resetPostAll() - - // デバイストークンが更新された - TASK_FCM_DEVICE_TOKEN -> { - } - - // プッシュ通知が届いた - TASK_FCM_MESSAGE -> { - var bDone = false - val tag = taskData.string(EXTRA_TAG) - if (tag != null) { - if (tag.startsWith("acct<>")) { - val acct = tag.substring(6) - val sa = SavedAccount.loadAccountByAcct(context, acct) - if (sa != null) { - NotificationCache.resetLastLoad(sa.db_id) - process_db_id = sa.db_id - bDone = true - } - } - if (!bDone) { - for (sa in SavedAccount.loadByTag(context, tag)) { - NotificationCache.resetLastLoad(sa.db_id) - process_db_id = sa.db_id - bDone = true - } - } - } - if (!bDone) { - // タグにマッチする情報がなかった場合、全部読み直す - NotificationCache.resetLastLoad() - } - } - - TASK_NOTIFICATION_CLEAR -> { - val db_id = taskData.long(EXTRA_DB_ID) - log.d("Notification clear! db_id=$db_id") - if (db_id != null) { - deleteCacheData(db_id) - } - } - - TASK_NOTIFICATION_DELETE -> { - val db_id = taskData.long(EXTRA_DB_ID) - val type = TrackingType.parseStr(taskData.string(EXTRA_NOTIFICATION_TYPE)) - val typeName = type.trackingTypeName() - val id = taskData.string(EXTRA_NOTIFICATION_ID) - log.d("Notification deleted! db_id=$db_id,type=$type,id=$id") - if (db_id != null) { - NotificationTracking.updateRead(db_id, typeName) - } - return - } - - TASK_NOTIFICATION_CLICK -> { - val db_id = taskData.long(EXTRA_DB_ID) - val type = TrackingType.parseStr(taskData.string(EXTRA_NOTIFICATION_TYPE)) - val typeName = type.trackingTypeName() - val id = taskData.string(EXTRA_NOTIFICATION_ID).notEmpty() - log.d("Notification clicked! db_id=$db_id,type=$type,id=$id") - if (db_id != null) { - // 通知をキャンセル - val notification_tag = when (typeName) { - "" -> "${db_id}/_" - else -> "${db_id}/$typeName" - } - if (id != null) { - val itemTag = "$notification_tag/$id" - notification_manager.cancel(itemTag, NOTIFICATION_ID) - } else { - notification_manager.cancel(notification_tag, NOTIFICATION_ID) - } - // DB更新処理 - NotificationTracking.updateRead(db_id, typeName) - } - return - } - - } - - job_status.set("make install id") - - // インストールIDを生成する - // インストールID生成時にSavedAccountテーブルを操作することがあるので - // アカウントリストの取得より先に行う - if (job.install_id == null) { - job.install_id = runBlocking { prepareInstallId(context, job) } - } - - // アカウント別に処理スレッドを作る - job_status.set("create account thread") - val thread_list = LinkedList() - for (_a in SavedAccount.loadAccountList(context)) { - if (_a.isPseudo) continue - if (process_db_id != -1L && _a.db_id != process_db_id) continue - val t = AccountThread(_a) - thread_list.add(t) - t.start() - } - while (true) { - // 同じホスト名が重複しないようにSetに集める - val liveSet = TreeSet() - for (t in thread_list) { - if (!t.isAlive) continue - if (job.isJobCancelled) t.cancel() - liveSet.add(t.account.apiHost) } - if (liveSet.isEmpty()) break - job_status.set("waiting " + liveSet.joinToString(", ") { it.pretty }) - job.waitWorkerThread(if (job.isJobCancelled) 100L else 1000L) + // アプリデータのインポート処理がビジーな間、他のジョブは実行されない + if (mBusyAppDataImportBefore.get() || mBusyAppDataImportAfter.get()) + return@coroutineScope + + // タスクによってはポーリング前にすることがある + when (taskId) { + TASK_DATA_INJECTED -> processInjectedData() + + TASK_BOOT_COMPLETED -> NotificationTracking.resetPostAll() + + TASK_PACKAGE_REPLACED -> NotificationTracking.resetPostAll() + + // デバイストークンが更新された + TASK_FCM_DEVICE_TOKEN -> { + } + + // プッシュ通知が届いた + TASK_FCM_MESSAGE -> { + var bDone = false + val tag = taskData.string(EXTRA_TAG) + if (tag != null) { + if (tag.startsWith("acct<>")) { + val acct = tag.substring(6) + val sa = SavedAccount.loadAccountByAcct(context, acct) + if (sa != null) { + NotificationCache.resetLastLoad(sa.db_id) + process_db_id = sa.db_id + bDone = true + } + } + if (!bDone) { + for (sa in SavedAccount.loadByTag(context, tag)) { + NotificationCache.resetLastLoad(sa.db_id) + process_db_id = sa.db_id + bDone = true + } + } + } + if (!bDone) { + // タグにマッチする情報がなかった場合、全部読み直す + NotificationCache.resetLastLoad() + } + } + + TASK_NOTIFICATION_CLEAR -> { + val db_id = taskData.long(EXTRA_DB_ID) + log.d("Notification clear! db_id=$db_id") + if (db_id != null) { + deleteCacheData(db_id) + } + } + + TASK_NOTIFICATION_DELETE -> { + val db_id = taskData.long(EXTRA_DB_ID) + val type = TrackingType.parseStr(taskData.string(EXTRA_NOTIFICATION_TYPE)) + val typeName = type.trackingTypeName() + val id = taskData.string(EXTRA_NOTIFICATION_ID) + log.d("Notification deleted! db_id=$db_id,type=$type,id=$id") + if (db_id != null) { + NotificationTracking.updateRead(db_id, typeName) + } + return@coroutineScope + } + + TASK_NOTIFICATION_CLICK -> { + val db_id = taskData.long(EXTRA_DB_ID) + val type = TrackingType.parseStr(taskData.string(EXTRA_NOTIFICATION_TYPE)) + val typeName = type.trackingTypeName() + val id = taskData.string(EXTRA_NOTIFICATION_ID).notEmpty() + log.d("Notification clicked! db_id=$db_id,type=$type,id=$id") + if (db_id != null) { + // 通知をキャンセル + val notification_tag = when (typeName) { + "" -> "${db_id}/_" + else -> "${db_id}/$typeName" + } + if (id != null) { + val itemTag = "$notification_tag/$id" + notification_manager.cancel(itemTag, NOTIFICATION_ID) + } else { + notification_manager.cancel(notification_tag, NOTIFICATION_ID) + } + // DB更新処理 + NotificationTracking.updateRead(db_id, typeName) + } + return@coroutineScope + } + } + + workerStatus = "make install id" + + // インストールIDを生成する + // インストールID生成時にSavedAccountテーブルを操作することがあるので + // アカウントリストの取得より先に行う + if (job.install_id == null) { + job.install_id = prepareInstallId(context, job) + } + + // アカウント別に処理スレッドを作る + workerStatus = "create account thread" + val thread_list = LinkedList() + suspend fun startForAccount(_a: SavedAccount) { + if (_a.isPseudo) return + thread_list.add(AccountRunner(_a).apply { start() }) + } + if (process_db_id != -1L) { + // process_db_id が指定されているなら、そのdb_idだけ処理する + SavedAccount.loadAccount(context, process_db_id)?.let { startForAccount(it) } + } else { + // 全てのアカウントを処理する + SavedAccount.loadAccountList(context).forEach { startForAccount(it) } + } + + while (true) { + // 同じホスト名が重複しないようにSetに集める + val liveSet = TreeSet() + for (t in thread_list) { + if (!t.isActive) continue + if (job.isJobCancelled) t.cancel() + liveSet.add(t.account.apiHost) + } + if (liveSet.isEmpty()) break + workerStatus = "waiting ${liveSet.joinToString(", ") { it.pretty }}" + delay(if (job.isJobCancelled) 100L else 1000L) + } + + synchronized(error_instance) { + createErrorNotification(error_instance) + } + + if (!job.isJobCancelled) job.bPollingComplete = true + + } catch (ex: Throwable) { + log.trace(ex, "task execution failed.") + } finally { + log.d(")runTask: taskId=$taskId") + workerStatus = "end task $taskId" } - - synchronized(error_instance) { - createErrorNotification(error_instance) - } - - if (!job.isJobCancelled) job.bPollingComplete = true - - } catch (ex: Throwable) { - log.trace(ex, "task execution failed.") - } finally { - log.d(")runTask: taskId=$taskId") - job_status.set("end task $taskId") } } - internal inner class AccountThread( - val account: SavedAccount - ) : Thread(), CurrentCallCallback { + internal inner class AccountRunner(val account: SavedAccount) { - private var current_call: Call? = null + private var suspendJob: Job? = null - private val client = TootApiClient(context, callback = object : TootApiCallback { - override val isApiCancelled: Boolean - get() = job.isJobCancelled - }) - - private val favMuteSet: HashSet get() = job.favMuteSet private lateinit var parser: TootParser + private lateinit var cache: NotificationCache - init { - client.currentCallCallback = this + private var currentCall: WeakReference? = null + + /////////////////// + + val isActive: Boolean + get() = suspendJob?.isActive ?: true + + private val onCallCreated: (Call) -> Unit = + { currentCall = WeakReference(it) } + + private val client = TootApiClient(context, callback = object : TootApiCallback { + override val isApiCancelled: Boolean + get() = job.isJobCancelled || (suspendJob?.isCancelled == true) + }).apply { + currentCallCallback = onCallCreated } - override fun onCallCreated(call: Call) { - current_call = call - } + private val favMuteSet: HashSet get() = job.favMuteSet fun cancel() { try { - current_call?.cancel() + currentCall?.get()?.cancel() } catch (ex: Throwable) { log.trace(ex) } - } - override fun run() = runBlocking { runSuspend() } - private suspend fun runSuspend(){ - try { - // 疑似アカウントはチェック対象外 - if (account.isPseudo) return + suspend fun start() { + coroutineScope { + this@AccountRunner.suspendJob = launch(Dispatchers.IO) { + runSuspend() + } + } + } - // 未確認アカウントはチェック対象外 - if (!account.isConfirmed) return + private val onError: (TootApiResult) -> Unit = { result -> + val sv = result.error + if (sv?.contains("Timeout") == true && !account.dont_show_timeout) { + synchronized(error_instance) { + if (!error_instance.any { it == sv }) error_instance.add(sv) + } + } + } - client.account = account + private suspend fun runSuspend() { + try { + // 疑似アカウントはチェック対象外 + if (account.isPseudo) return - val wps = PushSubscriptionHelper(context, account) + // 未確認アカウントはチェック対象外 + if (!account.isConfirmed) return - if (wps.flags != 0) { - job.bPollingRequired.set(true) + client.account = account - val (instance, instanceResult) = TootInstance.get(client) - if (instance == null) { - if (instanceResult != null) { - log.e("${instanceResult.error} ${instanceResult.requestInfo}".trim()) - account.updateNotificationError("${instanceResult.error} ${instanceResult.requestInfo}".trim()) - } - return - } + val wps = PushSubscriptionHelper(context, account) - if (job.isJobCancelled) return - } + if (wps.flags != 0) { + job.bPollingRequired.set(true) - wps.updateSubscription(client) ?: return // cancelled. + val (instance, instanceResult) = TootInstance.get(client) + if (instance == null) { + if (instanceResult != null) { + log.e("${instanceResult.error} ${instanceResult.requestInfo}".trim()) + account.updateNotificationError("${instanceResult.error} ${instanceResult.requestInfo}".trim()) + } + return + } - val wps_log = wps.log - if (wps_log.isNotEmpty()) - log.d("PushSubscriptionHelper: ${account.acct.pretty} $wps_log") + if (job.isJobCancelled) return + } - if (job.isJobCancelled) return + wps.updateSubscription(client) ?: return // cancelled. - if (wps.flags == 0) { - if (account.last_notification_error != null) { - account.updateNotificationError(null) - } - return - } + val wps_log = wps.log + if (wps_log.isNotEmpty()) + log.d("PushSubscriptionHelper: ${account.acct.pretty} $wps_log") - this.cache = NotificationCache(account.db_id).apply { - load() - request( - client, - account, - wps.flags, - onError = { result -> - val sv = result.error - if (sv?.contains("Timeout") == true && !account.dont_show_timeout) { - synchronized(error_instance) { - var bFound = false - for (x in error_instance) { - if (x == sv) { - bFound = true - break - } - } - if (!bFound) { - error_instance.add(sv) - } - } - } - }, - isCancelled = { - job.isJobCancelled - } - ) - } - if (job.isJobCancelled) return + if (job.isJobCancelled) return - this.parser = TootParser(context, account) + if (wps.flags == 0) { + if (account.last_notification_error != null) { + account.updateNotificationError(null) + } + return + } - if (Pref.bpSeparateReplyNotificationGroup(pref)) { - var tr = TrackingRunner( - trackingType = TrackingType.NotReply, - trackingName = NotificationHelper.TRACKING_NAME_DEFAULT - ) - tr.checkAccount() - if (job.isJobCancelled) return - tr.updateNotification() - // - tr = TrackingRunner( - trackingType = TrackingType.Reply, - trackingName = NotificationHelper.TRACKING_NAME_REPLY - ) - tr.checkAccount() - if (job.isJobCancelled) return - tr.updateNotification() + this.cache = NotificationCache(account.db_id).apply { + load() + requestAsync( + client, + account, + wps.flags, + onError = onError, + isCancelled = { job.isJobCancelled } + ) + } - } else { - val tr = TrackingRunner( - trackingType = TrackingType.All, - trackingName = NotificationHelper.TRACKING_NAME_DEFAULT - ) - tr.checkAccount() - if (job.isJobCancelled) return - tr.updateNotification() - } + if (job.isJobCancelled) return - } catch (ex: Throwable) { - log.trace(ex) - } finally { - job.notifyWorkerThread() - } - } + this.parser = TootParser(context, account) + + if (Pref.bpSeparateReplyNotificationGroup(pref)) { + var tr = TrackingRunner( + trackingType = TrackingType.NotReply, + trackingName = NotificationHelper.TRACKING_NAME_DEFAULT + ) + tr.checkAccount() + if (job.isJobCancelled) return + tr.updateNotification() + // + tr = TrackingRunner( + trackingType = TrackingType.Reply, + trackingName = NotificationHelper.TRACKING_NAME_REPLY + ) + tr.checkAccount() + if (job.isJobCancelled) return + tr.updateNotification() + + } else { + val tr = TrackingRunner( + trackingType = TrackingType.All, + trackingName = NotificationHelper.TRACKING_NAME_DEFAULT + ) + tr.checkAccount() + if (job.isJobCancelled) return + tr.updateNotification() + } + + } catch (ex: Throwable) { + log.trace(ex) + } finally { + workerNotifier.send(Unit) + } + } inner class TrackingRunner( - var trackingType: TrackingType = TrackingType.All, - var trackingName: String = "" - ) { + var trackingType: TrackingType = TrackingType.All, + var trackingName: String = "" + ) { private lateinit var nr: NotificationTracking private val duplicate_check = HashSet() @@ -1177,19 +1160,19 @@ class PollingWorker private constructor(contextArg: Context) { this.nr = NotificationTracking.load(account.db_id, trackingName) val jsonList = when (trackingType) { - TrackingType.All -> cache.data - TrackingType.Reply -> cache.data.filter { - when (parseNotificationType(account, it)) { - TootNotification.TYPE_REPLY, TootNotification.TYPE_MENTION -> true - else -> false - } - } - TrackingType.NotReply -> cache.data.filter { - !when (parseNotificationType(account, it)) { - TootNotification.TYPE_REPLY, TootNotification.TYPE_MENTION -> true - else -> false - } - } + TrackingType.All -> cache.data + TrackingType.Reply -> cache.data.filter { + when (parseNotificationType(account, it)) { + TootNotification.TYPE_REPLY, TootNotification.TYPE_MENTION -> true + else -> false + } + } + TrackingType.NotReply -> cache.data.filter { + !when (parseNotificationType(account, it)) { + TootNotification.TYPE_REPLY, TootNotification.TYPE_MENTION -> true + else -> false + } + } } // 新しい順に並んでいる。先頭から10件までを処理する。ただし処理順序は古い方から @@ -1225,17 +1208,17 @@ class PollingWorker private constructor(contextArg: Context) { // ふぁぼ魔ミュート when (notification.type) { - TootNotification.TYPE_REBLOG, - TootNotification.TYPE_FAVOURITE, - TootNotification.TYPE_FOLLOW, - TootNotification.TYPE_FOLLOW_REQUEST, - TootNotification.TYPE_FOLLOW_REQUEST_MISSKEY -> { - val who = notification.account - if (who != null && favMuteSet.contains(account.getFullAcct(who))) { - log.d("${account.getFullAcct(who)} is in favMuteSet.") - return - } - } + TootNotification.TYPE_REBLOG, + TootNotification.TYPE_FAVOURITE, + TootNotification.TYPE_FOLLOW, + TootNotification.TYPE_FOLLOW_REQUEST, + TootNotification.TYPE_FOLLOW_REQUEST_MISSKEY -> { + val who = notification.account + if (who != null && favMuteSet.contains(account.getFullAcct(who))) { + log.d("${account.getFullAcct(who)} is in favMuteSet.") + return + } + } } // 後から処理したものが先頭に来る @@ -1244,8 +1227,9 @@ class PollingWorker private constructor(contextArg: Context) { internal fun updateNotification() { + val notification_tag = when (trackingName) { - "" -> "${account.db_id}/_" + "" -> "${account.db_id}/_" else -> "${account.db_id}/$trackingName" } @@ -1311,9 +1295,9 @@ class PollingWorker private constructor(contextArg: Context) { } createNotification( - itemTag, - notificationId = item.notification.id.toString() - ) { builder -> + itemTag, + notificationId = item.notification.id.toString() + ) { builder -> builder.setWhen(item.notification.time_created_at) @@ -1322,11 +1306,11 @@ class PollingWorker private constructor(contextArg: Context) { val content = item.notification.status?.decoded_content?.notEmpty() if (content != null) { builder.setStyle( - NotificationCompat.BigTextStyle() - .setBigContentTitle(summary) - .setSummaryText(item.access_info.acct.pretty) - .bigText(content) - ) + NotificationCompat.BigTextStyle() + .setBigContentTitle(summary) + .setSummaryText(item.access_info.acct.pretty) + .bigText(content) + ) } else { builder.setContentText(item.access_info.acct.pretty) } @@ -1459,20 +1443,20 @@ class PollingWorker private constructor(contextArg: Context) { } private fun createNotification( - notification_tag: String, - notificationId: String? = null, - setContent: (builder: NotificationCompat.Builder) -> Unit - ) { + notification_tag: String, + notificationId: String? = null, + setContent: (builder: NotificationCompat.Builder) -> Unit + ) { log.d("showNotification[${account.acct.pretty}] creating notification(1)") val builder = if (Build.VERSION.SDK_INT >= 26) { // Android 8 から、通知のスタイルはユーザが管理することになった // NotificationChannel を端末に登録しておけば、チャネルごとに管理画面が作られる val channel = NotificationHelper.createNotificationChannel( - context, - account, - trackingName - ) + context, + account, + trackingName + ) NotificationCompat.Builder(context, channel.id) } else { NotificationCompat.Builder(context, "not_used") @@ -1481,10 +1465,10 @@ class PollingWorker private constructor(contextArg: Context) { builder.apply { val params = listOf( - "db_id" to account.db_id.toString(), - "type" to trackingType.str, - "notificationId" to notificationId - ).mapNotNull { + "db_id" to account.db_id.toString(), + "type" to trackingType.str, + "notificationId" to notificationId + ).mapNotNull { val second = it.second if (second == null) { null @@ -1494,32 +1478,32 @@ class PollingWorker private constructor(contextArg: Context) { }.joinToString("&") setContentIntent( - PendingIntent.getActivity( - context, - 257, - Intent(context, ActCallback::class.java).apply { - data = - "subwaytooter://notification_click/?$params".toUri() + PendingIntent.getActivity( + context, + 257, + Intent(context, ActCallback::class.java).apply { + data = + "subwaytooter://notification_click/?$params".toUri() - // FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY を付与してはいけない - addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - }, - PendingIntent.FLAG_UPDATE_CURRENT - ) - ) + // FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY を付与してはいけない + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + }, + PendingIntent.FLAG_UPDATE_CURRENT + ) + ) setDeleteIntent( - PendingIntent.getBroadcast( - context, - 257, - Intent(context, EventReceiver::class.java).apply { - action = EventReceiver.ACTION_NOTIFICATION_DELETE - data = - "subwaytooter://notification_delete/?$params".toUri() - }, - PendingIntent.FLAG_UPDATE_CURRENT - ) - ) + PendingIntent.getBroadcast( + context, + 257, + Intent(context, EventReceiver::class.java).apply { + action = EventReceiver.ACTION_NOTIFICATION_DELETE + data = + "subwaytooter://notification_delete/?$params".toUri() + }, + PendingIntent.FLAG_UPDATE_CURRENT + ) + ) setAutoCancel(true) @@ -1548,61 +1532,64 @@ class PollingWorker private constructor(contextArg: Context) { } } + private fun getNotificationLine(item: Data): String { + val name = when (Pref.bpShowAcctInSystemNotification(pref)) { - false -> item.notification.accountRef?.decoded_display_name + false -> item.notification.accountRef?.decoded_display_name - true -> { - val acctPretty = item.notification.accountRef?.get()?.acct?.pretty - if (acctPretty?.isNotEmpty() == true) { - "@$acctPretty" - } else { - null - } - } + true -> { + val acctPretty = item.notification.accountRef?.get()?.acct?.pretty + if (acctPretty?.isNotEmpty() == true) { + "@$acctPretty" + } else { + null + } + } } ?: "?" - return when (item.notification.type) { - TootNotification.TYPE_MENTION, - TootNotification.TYPE_REPLY -> - "- " + context.getString(R.string.display_name_replied_by, name) - TootNotification.TYPE_RENOTE, - TootNotification.TYPE_REBLOG -> - "- " + context.getString(R.string.display_name_boosted_by, name) + return "- " + when (item.notification.type) { + TootNotification.TYPE_MENTION, + TootNotification.TYPE_REPLY -> + context.getString(R.string.display_name_replied_by, name) - TootNotification.TYPE_QUOTE -> - "- " + context.getString(R.string.display_name_quoted_by, name) + TootNotification.TYPE_RENOTE, + TootNotification.TYPE_REBLOG -> + context.getString(R.string.display_name_boosted_by, name) - TootNotification.TYPE_STATUS -> - "- " + context.getString(R.string.display_name_posted_by, name) + TootNotification.TYPE_QUOTE -> + context.getString(R.string.display_name_quoted_by, name) - TootNotification.TYPE_FOLLOW -> - "- " + context.getString(R.string.display_name_followed_by, name) + TootNotification.TYPE_STATUS -> + context.getString(R.string.display_name_posted_by, name) - TootNotification.TYPE_UNFOLLOW -> - "- " + context.getString(R.string.display_name_unfollowed_by, name) + TootNotification.TYPE_FOLLOW -> + context.getString(R.string.display_name_followed_by, name) - TootNotification.TYPE_FAVOURITE -> - "- " + context.getString(R.string.display_name_favourited_by, name) + TootNotification.TYPE_UNFOLLOW -> + context.getString(R.string.display_name_unfollowed_by, name) - TootNotification.TYPE_REACTION -> - "- " + context.getString(R.string.display_name_reaction_by, name) + TootNotification.TYPE_FAVOURITE -> + context.getString(R.string.display_name_favourited_by, name) - TootNotification.TYPE_VOTE, - TootNotification.TYPE_POLL_VOTE_MISSKEY -> - "- " + context.getString(R.string.display_name_voted_by, name) + TootNotification.TYPE_REACTION -> + context.getString(R.string.display_name_reaction_by, name) - TootNotification.TYPE_FOLLOW_REQUEST, - TootNotification.TYPE_FOLLOW_REQUEST_MISSKEY -> - "- " + context.getString(R.string.display_name_follow_request_by, name) + TootNotification.TYPE_VOTE, + TootNotification.TYPE_POLL_VOTE_MISSKEY -> + context.getString(R.string.display_name_voted_by, name) - TootNotification.TYPE_FOLLOW_REQUEST_ACCEPTED_MISSKEY -> - "- " + context.getString(R.string.display_name_follow_request_accepted_by, name) + TootNotification.TYPE_FOLLOW_REQUEST, + TootNotification.TYPE_FOLLOW_REQUEST_MISSKEY -> + context.getString(R.string.display_name_follow_request_by, name) - TootNotification.TYPE_POLL -> - "- " + context.getString(R.string.end_of_polling_from, name) + TootNotification.TYPE_FOLLOW_REQUEST_ACCEPTED_MISSKEY -> + context.getString(R.string.display_name_follow_request_accepted_by, name) - else -> "- " + "?" + TootNotification.TYPE_POLL -> + context.getString(R.string.end_of_polling_from, name) + + else -> "?" } } @@ -1633,22 +1620,22 @@ class PollingWorker private constructor(contextArg: Context) { // FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY を付与してはいけない intent_click.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) val pi_click = PendingIntent.getActivity( - context, - 3, - intent_click, - PendingIntent.FLAG_UPDATE_CURRENT - ) + context, + 3, + intent_click, + PendingIntent.FLAG_UPDATE_CURRENT + ) val builder = if (Build.VERSION.SDK_INT >= 26) { // Android 8 から、通知のスタイルはユーザが管理することになった // NotificationChannel を端末に登録しておけば、チャネルごとに管理画面が作られる val channel = NotificationHelper.createNotificationChannel( - context, - "ErrorNotification", - "Error", - null, - 2 /* NotificationManager.IMPORTANCE_LOW */ - ) + context, + "ErrorNotification", + "Error", + null, + 2 /* NotificationManager.IMPORTANCE_LOW */ + ) NotificationCompat.Builder(context, channel.id) } else { NotificationCompat.Builder(context, "not_used") @@ -1659,11 +1646,11 @@ class PollingWorker private constructor(contextArg: Context) { .setAutoCancel(true) .setSmallIcon(R.drawable.ic_notification) // ここは常に白テーマのアイコンを使う .setColor( - ContextCompat.getColor( - context, - R.color.Light_colorAccent - ) - ) // ここは常に白テーマの色を使う + ContextCompat.getColor( + context, + R.color.Light_colorAccent + ) + ) // ここは常に白テーマの色を使う .setWhen(System.currentTimeMillis()) .setGroup(context.packageName + ":" + "Error") diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/TootApiClient.kt b/app/src/main/java/jp/juggler/subwaytooter/api/TootApiClient.kt index 90cdc005..24e34e2a 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api/TootApiClient.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/api/TootApiClient.kt @@ -1,7 +1,6 @@ package jp.juggler.subwaytooter.api import android.content.Context -import android.content.SharedPreferences import jp.juggler.subwaytooter.* import jp.juggler.subwaytooter.api.entity.* import jp.juggler.subwaytooter.table.ClientInfo @@ -12,37 +11,11 @@ import okhttp3.* import java.util.* class TootApiClient( - internal val context: Context, - internal val httpClient: SimpleHttpClient = SimpleHttpClientImpl( - context, - App1.ok_http_client - ), - internal val callback: TootApiCallback + internal val context: Context, + internal val httpClient: SimpleHttpClient = + SimpleHttpClientImpl(context,App1.ok_http_client), + internal val callback: TootApiCallback ) { - - // 認証に関する設定を保存する - internal val pref: SharedPreferences - - // インスタンスのホスト名 - var apiHost: Host? = null - - // アカウントがある場合に使用する - var account: SavedAccount? = null - set(value) { - apiHost = value?.apiHost - field = value - } - - var currentCallCallback: CurrentCallCallback? - get() = httpClient.currentCallCallback - set(value) { - httpClient.currentCallCallback = value - } - - init { - pref = context.pref() - } - companion object { private val log = LogCategory("TootApiClient") @@ -75,10 +48,10 @@ class TootApiClient( { json: JsonObject -> json["error"]?.toString() } internal fun simplifyErrorHtml( - response: Response, - sv: String, - jsonErrorParser: (json: JsonObject) -> String? = DEFAULT_JSON_ERROR_PARSER - ): String { + response: Response, + sv: String, + jsonErrorParser: (json: JsonObject) -> String? = DEFAULT_JSON_ERROR_PARSER + ): String { // JsonObjectとして解釈できるならエラーメッセージを検出する try { @@ -104,11 +77,11 @@ class TootApiClient( } fun formatResponse( - response: Response, - caption: String, - bodyString: String? = null, - jsonErrorParser: (json: JsonObject) -> String? = DEFAULT_JSON_ERROR_PARSER - ): String { + response: Response, + caption: String, + bodyString: String? = null, + jsonErrorParser: (json: JsonObject) -> String? = DEFAULT_JSON_ERROR_PARSER + ): String { val sb = StringBuilder() try { // body は既に読み終わっているか、そうでなければこれから読む @@ -155,53 +128,53 @@ class TootApiClient( if (ti.versionGE(TootInstance.MISSKEY_VERSION_11)) { // https://github.com/syuilo/misskey/blob/master/src/server/api/kinds.ts arrayOf( - "read:account", - "write:account", - "read:blocks", - "write:blocks", - "read:drive", - "write:drive", - "read:favorites", - "write:favorites", - "read:following", - "write:following", - "read:messaging", - "write:messaging", - "read:mutes", - "write:mutes", - "write:notes", - "read:notifications", - "write:notifications", - "read:reactions", - "write:reactions", - "write:votes" - ) + "read:account", + "write:account", + "read:blocks", + "write:blocks", + "read:drive", + "write:drive", + "read:favorites", + "write:favorites", + "read:following", + "write:following", + "read:messaging", + "write:messaging", + "read:mutes", + "write:mutes", + "write:notes", + "read:notifications", + "write:notifications", + "read:reactions", + "write:reactions", + "write:votes" + ) } else { // https://github.com/syuilo/misskey/issues/2341 arrayOf( - "account-read", - "account-write", - "account/read", - "account/write", - "drive-read", - "drive-write", - "favorite-read", - "favorite-write", - "favorites-read", - "following-read", - "following-write", - "messaging-read", - "messaging-write", - "note-read", - "note-write", - "notification-read", - "notification-write", - "reaction-read", - "reaction-write", - "vote-read", - "vote-write" + "account-read", + "account-write", + "account/read", + "account/write", + "drive-read", + "drive-write", + "favorite-read", + "favorite-write", + "favorites-read", + "following-read", + "following-write", + "messaging-read", + "messaging-write", + "note-read", + "note-write", + "notification-read", + "notification-write", + "reaction-read", + "reaction-write", + "vote-read", + "vote-write" - ) + ) } // APIのエラーを回避するため、重複を排除する @@ -222,6 +195,25 @@ class TootApiClient( } + // 認証に関する設定を保存する + internal val pref = context.pref() + + // インスタンスのホスト名 + var apiHost: Host? = null + + // アカウントがある場合に使用する + var account: SavedAccount? = null + set(value) { + apiHost = value?.apiHost + field = value + } + + var currentCallCallback: (Call) -> Unit + get() = httpClient.onCallCreated + set(value) { + httpClient.onCallCreated = value + } + @Suppress("unused") internal val isApiCancelled: Boolean get() = callback.isApiCancelled @@ -239,11 +231,11 @@ class TootApiClient( // リクエストをokHttpに渡してレスポンスを取得する internal inline fun sendRequest( - result: TootApiResult, - progressPath: String? = null, - tmpOkhttpClient: OkHttpClient? = null, - block: () -> Request - ): Boolean { + result: TootApiResult, + progressPath: String? = null, + tmpOkhttpClient: OkHttpClient? = null, + block: () -> Request + ): Boolean { return try { result.response = null result.bodyString = null @@ -254,10 +246,10 @@ class TootApiClient( result.requestInfo = "${request.method} ${progressPath ?: request.url.encodedPath}" callback.publishApiProgress( - context.getString( - R.string.request_api, request.method, progressPath ?: request.url.encodedPath - ) - ) + context.getString( + R.string.request_api, request.method, progressPath ?: request.url.encodedPath + ) + ) val response = httpClient.getResponse(request, tmpOkhttpClient = tmpOkhttpClient) result.response = response @@ -266,13 +258,53 @@ class TootApiClient( } catch (ex: Throwable) { result.setError( - "${result.caption}: ${ - ex.withCaption( - context.resources, - R.string.network_error - ) - }" - ) + "${result.caption}: ${ + ex.withCaption( + context.resources, + R.string.network_error + ) + }" + ) + false + } + } + + // リクエストをokHttpに渡してレスポンスを取得する + private suspend inline fun sendRequestAsync( + result: TootApiResult, + progressPath: String? = null, + tmpOkhttpClient: OkHttpClient? = null, + block: () -> Request + ): Boolean { + return try { + result.response = null + result.bodyString = null + result.data = null + + val request = block() + + result.requestInfo = "${request.method} ${progressPath ?: request.url.encodedPath}" + + callback.publishApiProgress( + context.getString( + R.string.request_api, request.method, progressPath ?: request.url.encodedPath + ) + ) + + val response = httpClient.getResponseAsync(request, tmpOkhttpClient = tmpOkhttpClient) + result.response = response + + null == result.error + + } catch (ex: Throwable) { + result.setError( + "${result.caption}: ${ + ex.withCaption( + context.resources, + R.string.network_error + ) + }" + ) false } } @@ -280,10 +312,10 @@ class TootApiClient( // レスポンスがエラーかボディがカラならエラー状態を設定する // 例外を出すかも internal fun readBodyString( - result: TootApiResult, - progressPath: String? = null, - jsonErrorParser: (json: JsonObject) -> String? = DEFAULT_JSON_ERROR_PARSER - ): String? { + result: TootApiResult, + progressPath: String? = null, + jsonErrorParser: (json: JsonObject) -> String? = DEFAULT_JSON_ERROR_PARSER + ): String? { if (isApiCancelled) return null @@ -291,12 +323,12 @@ class TootApiClient( val request = response.request publishApiProgress( - context.getString( - R.string.reading_api, - request.method, - progressPath ?: result.caption - ) - ) + context.getString( + R.string.reading_api, + request.method, + progressPath ?: result.caption + ) + ) val bodyString = response.body?.string() if (isApiCancelled) return null @@ -310,11 +342,11 @@ class TootApiClient( if (!response.isSuccessful || bodyString?.isEmpty() != false) { result.error = formatResponse( - response, - result.caption, - if (bodyString?.isNotEmpty() == true) bodyString else NO_INFORMATION, - jsonErrorParser - ) + response, + result.caption, + if (bodyString?.isNotEmpty() == true) bodyString else NO_INFORMATION, + jsonErrorParser + ) } return if (result.error != null) { @@ -329,10 +361,10 @@ class TootApiClient( // レスポンスがエラーかボディがカラならエラー状態を設定する // 例外を出すかも private fun readBodyBytes( - result: TootApiResult, - progressPath: String? = null, - jsonErrorParser: (json: JsonObject) -> String? = DEFAULT_JSON_ERROR_PARSER - ): ByteArray? { + result: TootApiResult, + progressPath: String? = null, + jsonErrorParser: (json: JsonObject) -> String? = DEFAULT_JSON_ERROR_PARSER + ): ByteArray? { if (isApiCancelled) return null @@ -340,12 +372,12 @@ class TootApiClient( val request = response.request publishApiProgress( - context.getString( - R.string.reading_api, - request.method, - progressPath ?: result.caption - ) - ) + context.getString( + R.string.reading_api, + request.method, + progressPath ?: result.caption + ) + ) val bodyBytes = response.body?.bytes() if (isApiCancelled) return null @@ -353,11 +385,11 @@ class TootApiClient( if (!response.isSuccessful || bodyBytes?.isEmpty() != false) { result.error = formatResponse( - response, - result.caption, - if (bodyBytes?.isNotEmpty() == true) bodyBytes.decodeUTF8() else NO_INFORMATION, - jsonErrorParser - ) + response, + result.caption, + if (bodyBytes?.isNotEmpty() == true) bodyBytes.decodeUTF8() else NO_INFORMATION, + jsonErrorParser + ) } return if (result.error != null) { @@ -370,17 +402,16 @@ class TootApiClient( } private fun parseBytes( - result: TootApiResult, - progressPath: String? = null, - jsonErrorParser: (json: JsonObject) -> String? = DEFAULT_JSON_ERROR_PARSER - ): TootApiResult? { + result: TootApiResult, + progressPath: String? = null, + jsonErrorParser: (json: JsonObject) -> String? = DEFAULT_JSON_ERROR_PARSER + ): TootApiResult? { val response = result.response!! // nullにならないはず try { readBodyBytes(result, progressPath, jsonErrorParser) ?: return if (isApiCancelled) null else result - } catch (ex: Throwable) { log.trace(ex) result.error = @@ -390,10 +421,10 @@ class TootApiClient( } internal fun parseString( - result: TootApiResult, - progressPath: String? = null, - jsonErrorParser: (json: JsonObject) -> String? = DEFAULT_JSON_ERROR_PARSER - ): TootApiResult? { + result: TootApiResult, + progressPath: String? = null, + jsonErrorParser: (json: JsonObject) -> String? = DEFAULT_JSON_ERROR_PARSER + ): TootApiResult? { val response = result.response!! // nullにならないはず @@ -413,10 +444,10 @@ class TootApiClient( // レスポンスからJSONデータを読む internal fun parseJson( - result: TootApiResult, - progressPath: String? = null, - jsonErrorParser: (json: JsonObject) -> String? = DEFAULT_JSON_ERROR_PARSER - ): TootApiResult? // 引数に指定したresultそのものか、キャンセルされたらnull + result: TootApiResult, + progressPath: String? = null, + jsonErrorParser: (json: JsonObject) -> String? = DEFAULT_JSON_ERROR_PARSER + ): TootApiResult? // 引数に指定したresultそのものか、キャンセルされたらnull { val response = result.response!! // nullにならないはず @@ -480,9 +511,9 @@ class TootApiClient( ////////////////////////////////////////////////////////////////////// fun request( - path: String, - request_builder: Request.Builder = Request.Builder() - ): TootApiResult? { + path: String, + request_builder: Request.Builder = Request.Builder() + ): TootApiResult? { val result = TootApiResult.makeWithCaption(apiHost?.pretty) if (result.error != null) return result @@ -511,6 +542,39 @@ class TootApiClient( } } + + suspend fun requestAsync( + path: String, + request_builder: Request.Builder = Request.Builder() + ): TootApiResult? { + val result = TootApiResult.makeWithCaption(apiHost?.pretty) + if (result.error != null) return result + + val account = this.account // may null + + try { + if (!sendRequestAsync(result) { + + log.d("request: $path") + + request_builder.url("https://${apiHost?.ascii}$path") + + val access_token = account?.getAccessToken() + if (access_token?.isNotEmpty() == true) { + request_builder.header("Authorization", "Bearer $access_token") + } + + request_builder.build() + + }) return result + + return parseJson(result) + } finally { + val error = result.error + if (error != null) log.d("error: $error") + } + } + ////////////////////////////////////////////////////////////////////// // misskey authentication @@ -594,9 +658,9 @@ class TootApiClient( } private fun registerClientMisskey( - scope_array: JsonArray, - client_name: String - ): TootApiResult? { + scope_array: JsonArray, + client_name: String + ): TootApiResult? { val result = TootApiResult.makeWithCaption(apiHost?.pretty) if (result.error != null) return result if (sendRequest(result) { @@ -699,10 +763,10 @@ class TootApiClient( // oAuth2認証の続きを行う fun authentication2Misskey( - clientNameArg: String, - token: String, - misskeyVersion: Int - ): TootApiResult? { + clientNameArg: String, + token: String, + misskeyVersion: Int + ): TootApiResult? { val result = TootApiResult.makeWithCaption(apiHost?.pretty) if (result.error != null) return result val instance = result.caption // same to instance @@ -834,9 +898,9 @@ class TootApiClient( // client_credentialを無効にする private fun revokeClientCredential( - client_info: JsonObject, - client_credential: String - ): TootApiResult? { + client_info: JsonObject, + client_credential: String + ): TootApiResult? { val result = TootApiResult.makeWithCaption(apiHost?.pretty) if (result.error != null) return result @@ -885,10 +949,10 @@ class TootApiClient( } private fun prepareClientMastodon( - clientNameArg: String, - ti: TootInstance, - forceUpdateClient: Boolean = false - ): TootApiResult? { + clientNameArg: String, + ti: TootInstance, + forceUpdateClient: Boolean = false + ): TootApiResult? { // 前準備 val result = TootApiResult.makeWithCaption(apiHost?.pretty) if (result.error != null) return result @@ -977,10 +1041,10 @@ class TootApiClient( } private fun authentication1Mastodon( - clientNameArg: String, - ti: TootInstance, - forceUpdateClient: Boolean = false - ): TootApiResult? { + clientNameArg: String, + ti: TootInstance, + forceUpdateClient: Boolean = false + ): TootApiResult? { if (ti.instanceType == TootInstance.InstanceType.Pixelfed) { return TootApiResult("currently Pixelfed instance is not supported.") @@ -996,11 +1060,11 @@ class TootApiClient( // クライアントを登録してブラウザで開くURLを生成する fun authentication1( - clientNameArg: String, - forceUpdateClient: Boolean = false - ): TootApiResult? { + clientNameArg: String, + forceUpdateClient: Boolean = false + ): TootApiResult? { - val (ti, ri) = TootInstance.get(this) + val (ti, ri) = TootInstance.get(this) ti ?: return ri return when { ti.misskeyVersion > 0 -> authentication1Misskey(clientNameArg, ti) @@ -1054,8 +1118,8 @@ class TootApiClient( // アクセストークン手動入力でアカウントを更新する場合、アカウントの情報を取得する fun getUserCredential( - access_token: String, tokenInfo: JsonObject = JsonObject(), misskeyVersion: Int = 0 - ): TootApiResult? { + access_token: String, tokenInfo: JsonObject = JsonObject(), misskeyVersion: Int = 0 + ): TootApiResult? { if (misskeyVersion > 0) { val result = TootApiResult.makeWithCaption(apiHost?.pretty) if (result.error != null) return result @@ -1107,16 +1171,16 @@ class TootApiClient( fun createUser1(clientNameArg: String): TootApiResult? { - val (ti, ri) = TootInstance.get(this) + val (ti, ri) = TootInstance.get(this) ti ?: return ri return when (ti.instanceType) { - TootInstance.InstanceType.Misskey -> - TootApiResult("Misskey has no API to create new account") - TootInstance.InstanceType.Pleroma -> - TootApiResult("Pleroma has no API to create new account") - TootInstance.InstanceType.Pixelfed -> - TootApiResult("Pixelfed has no API to create new account") + TootInstance.InstanceType.Misskey -> + TootApiResult("Misskey has no API to create new account") + TootInstance.InstanceType.Pleroma -> + TootApiResult("Pleroma has no API to create new account") + TootInstance.InstanceType.Pixelfed -> + TootApiResult("Pixelfed has no API to create new account") else -> prepareClientMastodon(clientNameArg, ti) // result.JsonObject に credentialつきのclient_info を格納して返す @@ -1125,13 +1189,13 @@ class TootApiClient( // ユーザ名入力の後に呼ばれる fun createUser2Mastodon( - client_info: JsonObject, - username: String, - email: String, - password: String, - agreement: Boolean, - reason: String? - ): TootApiResult? { + client_info: JsonObject, + username: String, + email: String, + password: String, + agreement: Boolean, + reason: String? + ): TootApiResult? { val result = TootApiResult.makeWithCaption(apiHost?.pretty) if (result.error != null) return result @@ -1166,10 +1230,9 @@ class TootApiClient( //////////////////////////////////////////////////////////////////////// // JSONデータ以外を扱うリクエスト - fun http(req: Request): TootApiResult? { + fun http(req: Request): TootApiResult { val result = TootApiResult.makeWithCaption(req.url.host) if (result.error != null) return result - sendRequest(result, progressPath = null) { req } return result } @@ -1184,12 +1247,9 @@ class TootApiClient( // } // 疑似アカウントでステータスURLからステータスIDを取得するためにHTMLを取得する - fun getHttp(url: String): TootApiResult? { + fun getHttp(url: String):TootApiResult?{ val result = http(Request.Builder().url(url).build()) - if (result != null && result.error == null) { - parseString(result) - } - return result + return if (result.error != null) result else parseString(result) } fun getHttpBytes(url: String): Pair { @@ -1206,9 +1266,9 @@ class TootApiClient( } fun webSocket( - path: String, - ws_listener: WebSocketListener - ): Pair { + path: String, + ws_listener: WebSocketListener + ): Pair { var ws: WebSocket? = null val result = TootApiResult.makeWithCaption(apiHost?.pretty) if (result.error != null) return Pair(result, null) @@ -1244,8 +1304,8 @@ class TootApiClient( // query: query_string after ? ( ? itself is excluded ) fun TootApiClient.requestMastodonSearch( - parser: TootParser, - query: String + parser: TootParser, + query: String ): Pair { var searchApiVersion = 2 @@ -1267,8 +1327,8 @@ fun TootApiClient.requestMastodonSearch( // result.data に TootAccountRefを格納して返す。もしくはエラーかキャンセル fun TootApiClient.syncAccountByUrl( - accessInfo: SavedAccount, - who_url: String + accessInfo: SavedAccount, + who_url: String ): Pair { // misskey由来のアカウントURLは https://host/@user@instance などがある @@ -1288,18 +1348,18 @@ fun TootApiClient.syncAccountByUrl( val acct = TootAccount.getAcctFromUrl(who_url) ?: return Pair( - TootApiResult(context.getString(R.string.user_id_conversion_failed)), - null - ) + TootApiResult(context.getString(R.string.user_id_conversion_failed)), + null + ) var ar: TootAccountRef? = null val result = request( - "/api/users/show", - accessInfo.putMisskeyApiToken().apply { - put("username", acct.username) - acct.host?.let { put("host", it.ascii) } - }.toPostRequestBuilder() - ) + "/api/users/show", + accessInfo.putMisskeyApiToken().apply { + put("username", acct.username) + acct.host?.let { put("host", it.ascii) } + }.toPostRequestBuilder() + ) ?.apply { ar = TootAccountRef.mayNull(parser, parser.account(jsonObject)) if (ar == null && error == null) { @@ -1308,10 +1368,10 @@ fun TootApiClient.syncAccountByUrl( } Pair(result, ar) } else { - val (apiResult, searchResult) = requestMastodonSearch( - parser, - "q=${who_url.encodePercent()}&resolve=true" - ) + val (apiResult, searchResult) = requestMastodonSearch( + parser, + "q=${who_url.encodePercent()}&resolve=true" + ) val ar = searchResult?.accounts?.firstOrNull() if (apiResult != null && apiResult.error == null && ar == null) { apiResult.setError(context.getString(R.string.user_id_conversion_failed)) @@ -1321,27 +1381,27 @@ fun TootApiClient.syncAccountByUrl( } fun TootApiClient.syncAccountByAcct( - accessInfo: SavedAccount, - acctArg: String + accessInfo: SavedAccount, + acctArg: String ): Pair = syncAccountByAcct(accessInfo, Acct.parse(acctArg)) fun TootApiClient.syncAccountByAcct( - accessInfo: SavedAccount, - acct: Acct + accessInfo: SavedAccount, + acct: Acct ): Pair { val parser = TootParser(context, accessInfo) return if (accessInfo.isMisskey) { var ar: TootAccountRef? = null val result = request( - "/api/users/show", - accessInfo.putMisskeyApiToken() - .apply { - if (acct.isValid) put("username", acct.username) - if (acct.host != null) put("host", acct.host.ascii) - } - .toPostRequestBuilder() - ) + "/api/users/show", + accessInfo.putMisskeyApiToken() + .apply { + if (acct.isValid) put("username", acct.username) + if (acct.host != null) put("host", acct.host.ascii) + } + .toPostRequestBuilder() + ) ?.apply { ar = TootAccountRef.mayNull(parser, parser.account(jsonObject)) if (ar == null && error == null) { @@ -1350,10 +1410,10 @@ fun TootApiClient.syncAccountByAcct( } Pair(result, ar) } else { - val (apiResult, searchResult) = requestMastodonSearch( - parser, - "q=${acct.ascii.encodePercent()}&resolve=true" - ) + val (apiResult, searchResult) = requestMastodonSearch( + parser, + "q=${acct.ascii.encodePercent()}&resolve=true" + ) val ar = searchResult?.accounts?.firstOrNull() if (apiResult != null && apiResult.error == null && ar == null) { apiResult.setError(context.getString(R.string.user_id_conversion_failed)) @@ -1364,8 +1424,8 @@ fun TootApiClient.syncAccountByAcct( } fun TootApiClient.syncStatus( - accessInfo: SavedAccount, - urlArg: String + accessInfo: SavedAccount, + urlArg: String ): Pair { var url = urlArg @@ -1380,18 +1440,18 @@ fun TootApiClient.syncStatus( TootApiClient(context, callback = callback) .apply { apiHost = host } .request( - "/api/notes/show", - JsonObject().apply { - put("noteId", noteId) - } - .toPostRequestBuilder() - ) + "/api/notes/show", + JsonObject().apply { + put("noteId", noteId) + } + .toPostRequestBuilder() + ) ?.also { result -> TootParser( - context, - linkHelper = LinkHelper.create(host, misskeyVersion = 10), - serviceType = ServiceType.MISSKEY - ) + context, + linkHelper = LinkHelper.create(host, misskeyVersion = 10), + serviceType = ServiceType.MISSKEY + ) .status(result.jsonObject) ?.apply { if (accessInfo.matchHost(host)) { @@ -1409,12 +1469,12 @@ fun TootApiClient.syncStatus( return if (accessInfo.isMisskey) { var targetStatus: TootStatus? = null val result = request( - "/api/ap/show", - accessInfo.putMisskeyApiToken().apply { - put("uri", url) - } - .toPostRequestBuilder() - ) + "/api/ap/show", + accessInfo.putMisskeyApiToken().apply { + put("uri", url) + } + .toPostRequestBuilder() + ) ?.apply { targetStatus = parser.parseMisskeyApShow(jsonObject) as? TootStatus if (targetStatus == null && error == null) { @@ -1423,10 +1483,10 @@ fun TootApiClient.syncStatus( } Pair(result, targetStatus) } else { - val (apiResult, searchResult) = requestMastodonSearch( - parser, - "q=${url.encodePercent()}&resolve=true" - ) + val (apiResult, searchResult) = requestMastodonSearch( + parser, + "q=${url.encodePercent()}&resolve=true" + ) val targetStatus = searchResult?.statuses?.firstOrNull() if (apiResult != null && apiResult.error == null && targetStatus == null) { apiResult.setError(context.getString(R.string.cant_sync_toot)) @@ -1437,8 +1497,8 @@ fun TootApiClient.syncStatus( } fun TootApiClient.syncStatus( - accessInfo: SavedAccount, - statusRemote: TootStatus + accessInfo: SavedAccount, + statusRemote: TootStatus ): Pair { // URL->URIの順に試す diff --git a/app/src/main/java/jp/juggler/subwaytooter/table/NotificationCache.kt b/app/src/main/java/jp/juggler/subwaytooter/table/NotificationCache.kt index 9f840aa3..b138a48f 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/table/NotificationCache.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/table/NotificationCache.kt @@ -262,7 +262,7 @@ class NotificationCache(private val account_db_id : Long) { } - fun request( + suspend fun requestAsync( client : TootApiClient, account : SavedAccount, flags : Int, @@ -291,9 +291,9 @@ class NotificationCache(private val account_db_id : Long) { } val result = if(account.isMisskey) { - client.request(path, account.putMisskeyApiToken().toPostRequestBuilder()) + client.requestAsync(path, account.putMisskeyApiToken().toPostRequestBuilder()) } else { - client.request(path) + client.requestAsync(path) } if(result == null) { diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/AppOpener.kt b/app/src/main/java/jp/juggler/subwaytooter/util/AppOpener.kt index 7627d68e..761bac2f 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/util/AppOpener.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/util/AppOpener.kt @@ -48,9 +48,21 @@ private fun Activity.startActivityExcludeMyApp( val myName = packageName val filter: (ResolveInfo) -> Boolean = { - it.activityInfo.packageName != myName && - it.activityInfo.exported && - -1 == it.activityInfo.packageName.indexOf("com.huawei.android.internal") + when{ + it.activityInfo.packageName == myName -> false + !it.activityInfo.exported -> false + + // Huaweiの謎Activityのせいでうまく働かないことがある + -1 != it.activityInfo.packageName.indexOf("com.huawei.android.internal") -> false + + // 標準アプリが設定されていない場合、アプリを選択するためのActivityが出てくる場合がある + it.activityInfo.packageName == "android" -> false + it.activityInfo.javaClass.name.startsWith( "com.android.internal") -> false + it.activityInfo.javaClass.name.startsWith("com.android.systemui") -> false + + // たぶんChromeとかfirefoxとか + else -> true + } } // resolveActivity がこのアプリ以外のActivityを返すなら、それがベストなんだろう diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/SimpleHttpClient.kt b/app/src/main/java/jp/juggler/subwaytooter/util/SimpleHttpClient.kt index 25fcf127..59d1f336 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/util/SimpleHttpClient.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/util/SimpleHttpClient.kt @@ -4,57 +4,69 @@ import android.content.Context import okhttp3.* import jp.juggler.subwaytooter.App1 import jp.juggler.util.LogCategory +import ru.gildor.coroutines.okhttp.await // okhttpそのままだとモックしづらいので // リクエストを投げてレスポンスを得る部分をインタフェースにまとめる -interface CurrentCallCallback { - fun onCallCreated(call : Call) -} - interface SimpleHttpClient { - var currentCallCallback : CurrentCallCallback? - - fun getResponse( - request : Request, - tmpOkhttpClient : OkHttpClient? = null - ) : Response - - fun getWebSocket( - request : Request, - webSocketListener : WebSocketListener - ) : WebSocket + + var onCallCreated: (Call) -> Unit + + fun getResponse( + request: Request, + tmpOkhttpClient: OkHttpClient? = null + ): Response + + suspend fun getResponseAsync( + request: Request, + tmpOkhttpClient: OkHttpClient? = null + ): Response + + fun getWebSocket( + request: Request, + webSocketListener: WebSocketListener + ): WebSocket } class SimpleHttpClientImpl( - val context : Context, - private val okHttpClient : OkHttpClient + val context: Context, + private val okHttpClient: OkHttpClient ) : SimpleHttpClient { - - - companion object { - val log = LogCategory("SimpleHttpClientImpl") - } - - override var currentCallCallback : CurrentCallCallback? = null - - override fun getResponse( - request : Request, - tmpOkhttpClient : OkHttpClient? - ) : Response { + + companion object { + val log = LogCategory("SimpleHttpClientImpl") + } + + override var onCallCreated: (Call) -> Unit = {} + + override fun getResponse( + request: Request, + tmpOkhttpClient: OkHttpClient? + ): Response { + App1.getAppState(context).networkTracker.checkNetworkState() + val call = (tmpOkhttpClient ?: this.okHttpClient).newCall(request) + onCallCreated(call) + return call.execute() + } + + override suspend fun getResponseAsync( + request: Request, + tmpOkhttpClient: OkHttpClient? + ): Response { App1.getAppState(context).networkTracker.checkNetworkState() val call = (tmpOkhttpClient ?: this.okHttpClient).newCall(request) - currentCallCallback?.onCallCreated(call) - return call.execute() + onCallCreated(call) + return call.await() } - - override fun getWebSocket( - request : Request, - webSocketListener : WebSocketListener - ) : WebSocket { - App1.getAppState(context).networkTracker.checkNetworkState() - return okHttpClient.newWebSocket(request, webSocketListener) - } - - + + override fun getWebSocket( + request: Request, + webSocketListener: WebSocketListener + ): WebSocket { + App1.getAppState(context).networkTracker.checkNetworkState() + return okHttpClient.newWebSocket(request, webSocketListener) + } + + }