package jp.juggler.subwaytooter import android.app.Activity import android.app.Dialog import android.content.Intent import android.content.SharedPreferences import android.content.pm.PackageManager import android.content.res.ColorStateList import android.content.res.Configuration import android.graphics.Typeface import android.net.Uri import android.os.* import android.text.InputType import android.text.Spannable import android.text.SpannableStringBuilder import android.util.JsonReader import android.view.* import android.view.inputmethod.EditorInfo import android.widget.* import androidx.appcompat.app.AlertDialog import androidx.core.view.GravityCompat import androidx.drawerlayout.widget.DrawerLayout import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.viewpager.widget.ViewPager import jp.juggler.subwaytooter.action.* import jp.juggler.subwaytooter.api.* import jp.juggler.subwaytooter.api.entity.* import jp.juggler.subwaytooter.api.entity.TootStatus.Companion.findStatusIdFromUrl import jp.juggler.subwaytooter.api.entity.TootTag.Companion.findHashtagFromUrl import jp.juggler.subwaytooter.dialog.AccountPicker import jp.juggler.subwaytooter.dialog.ActionsDialog import jp.juggler.subwaytooter.dialog.DlgQuickTootMenu import jp.juggler.subwaytooter.dialog.DlgTextInput import jp.juggler.subwaytooter.notification.PollingWorker import jp.juggler.subwaytooter.span.MyClickableSpan import jp.juggler.subwaytooter.table.AcctColor import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.subwaytooter.util.* import jp.juggler.subwaytooter.view.* import jp.juggler.util.* import kotlinx.coroutines.delay import org.apache.commons.io.IOUtils import org.jetbrains.anko.backgroundDrawable import org.jetbrains.anko.imageResource import java.io.File import java.io.FileInputStream import java.io.FileOutputStream import java.io.InputStreamReader import java.lang.ref.WeakReference import java.util.* import java.util.zip.ZipInputStream import kotlin.math.abs import kotlin.math.max import kotlin.math.min class ActMain : AsyncActivity(), 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)) { app_state.columnList.forEach { it.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 = column?.access_info, tagList = hashtagList.notEmpty(), whoRef = whoRef, linkInfo = linkInfo ) } private val dlgQuickTootMenu = DlgQuickTootMenu(this, object : DlgQuickTootMenu.Callback { override var visibility: TootVisibility get() = quickTootVisibility set(value) { if (value != quickTootVisibility) { quickTootVisibility = value pref.edit().put(Pref.spQuickTootVisibility, value.id.toString()).apply() showQuickTootVisibility() } } override fun onMacro(text: String) { val editable = etQuickToot.text if (editable?.isNotEmpty() == true) { val start = etQuickToot.selectionStart val end = etQuickToot.selectionEnd editable.replace(start, end, text) etQuickToot.requestFocus() etQuickToot.setSelection(start + text.length) } else { etQuickToot.setText(text) etQuickToot.requestFocus() etQuickToot.setSelection(text.length) } } }) val viewPool = RecyclerView.RecycledViewPool() // スマホモードなら現在のカラムを、タブレットモードなら-1Lを返す // (カラム一覧画面のデフォルト選択位置に使われる) val currentColumn: Int get() = phoneTab( { it.pager.currentItem }, { -1 } ) // 新しいカラムをどこに挿入するか // 現在のページの次の位置か、終端 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() { val list = app_state.columnList return visibleColumnsIndices.mapNotNull { list.elementAtOrNull(it) } } // デフォルトの投稿先アカウントを探す。アカウント選択が必要な状況ならnull val currentPostTarget: SavedAccount? get() = phoneTab( { env -> val c = env.pager_adapter.getColumn(env.pager.currentItem) return when { c == null || c.access_info.isPseudo -> null else -> c.access_info } }, { env -> val db_id = Pref.lpTabletTootDefaultAccount(App1.pref) if (db_id != -1L) { val a = SavedAccount.loadAccount(this@ActMain, db_id) if (a != null && !a.isPseudo) return a } val accounts = ArrayList() for (c in env.visibleColumns) { try { val a = c.access_info // 画面内に疑似アカウントがあれば常にアカウント選択が必要 if (a.isPseudo) { accounts.clear() break } // 既出でなければ追加する if (null == accounts.find { it == a }) accounts.add(a) } catch (ex: Throwable) { } } 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 EmojiDecoder.handleUnicodeEmoji = Pref.bpInAppUnicodeEmoji(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.columnCount > 0) { val column_pos = Pref.ipLastColumnPos(pref) log.d("ipLastColumnPos load $column_pos") // 前回最後に表示していたカラムの位置にスクロールする if (column_pos in 0 until app_state.columnCount) { 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 への参照を全カラムから除去する app_state.columnList.forEach { it.removeColumnViewHolderByActivity(this) } } override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) log.w("onNewIntent: isResumed=$isResumed") } override fun onConfigurationChanged(newConfig: Configuration) { log.w("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() .takeIf { it != RecyclerView.NO_POSITION } ?.let { outState.putInt(STATE_CURRENT_PAGE, it) } } ) app_state.columnList.forEach { it.saveScrollPosition() } } override fun onRestoreInstanceState(savedInstanceState: Bundle) { log.d("onRestoreInstanceState") super.onRestoreInstanceState(savedInstanceState) val pos = savedInstanceState.getInt(STATE_CURRENT_PAGE) // 注意:開始は0じゃなく1 if (pos in 1 until app_state.columnCount) { 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() ?: attrColor(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 = app_state.columnList .mapIndexedNotNull { index, column -> if (!column.access_info.isNA && null == SavedAccount.loadAccount(this@ActMain, column.access_info.db_id)) { null } else { index } } if (new_order.size != app_state.columnCount) { setOrder(new_order) } te = SystemClock.elapsedRealtime() if (te - ts >= 100L) log.w("onStart: ${te - ts}ms : column order") ts = SystemClock.elapsedRealtime() // 背景画像を表示しない設定が変更された時にカラムの背景を設定しなおす app_state.columnList.forEach { column -> 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() // 画面復帰時に再取得などを行う app_state.columnList.forEach { it.onStart() } te = SystemClock.elapsedRealtime() if (te - ts >= 100L) log.w("onStart: ${te - ts}ms :column.onStart") ts = SystemClock.elapsedRealtime() // 画面復帰時にストリーミング接続を開始する app_state.streamManager.onScreenStart() te = SystemClock.elapsedRealtime() if (te - ts >= 100L) log.w("onStart: ${te - ts}ms :multi_stream_reader.onScreenStart") ts = SystemClock.elapsedRealtime() // カラムの表示範囲インジケータを更新 updateColumnStripSelection(-1, -1f) te = SystemClock.elapsedRealtime() if (te - ts >= 100L) log.w("onStart: ${te - ts}ms :updateColumnStripSelection") ts = SystemClock.elapsedRealtime() app_state.columnList.forEach { it.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.streamManager.onScreenStop() app_state.columnList.forEach { it.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) 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() app_state.columnList.forEach { it.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 { app_state.column(position)?.let { column -> 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)) { drawer.openDrawer(GravityCompat.START) } 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") } // スマホモードならラムダを実行する。タブレットモードなら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 = app_state.columnIndex(column)?.let{ it+1 } ?: defaultInsertPosition 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 fun refreshAfterPost() { val posted_acct = this.posted_acct val posted_status_id = this.posted_status_id if (posted_acct != null && posted_status_id == null) { // 予約投稿なら予約投稿リストをリロードする app_state.columnList.forEach { column -> 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) { app_state.columnList.forEach { it.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) { app_state.columnList .filter{ it.access_info.acct == posted_acct} .forEach { it.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, 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( 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) { 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 { override fun onPostComplete( target_account: SavedAccount, status: TootStatus ) { etQuickToot.setText("") posted_acct = target_account.acct posted_status_id = status.id posted_reply_id = status.in_reply_to_id posted_redraft_id = null refreshAfterPost() } override fun onScheduledPostComplete(target_account: SavedAccount) { } }) } private fun isOrderChanged(new_order: List): Boolean { if (new_order.size != app_state.columnCount) return true for(i in new_order.indices){ if (new_order[i] != i) return true } 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)) { setOrder(order) } val select = data.getIntExtra(ActColumnList.EXTRA_SELECTION, -1) if (select in 0 until app_state.columnCount) { scrollToColumn(select) } } REQUEST_APP_ABOUT -> if (data != null) { val search = data.getStringExtra(ActAbout.EXTRA_SEARCH) if (search?.isNotEmpty() == true) { Action_Account.timeline( this@ActMain, defaultInsertPosition, ColumnType.SEARCH, args = arrayOf(search, true) ) } return } REQUEST_CODE_NICKNAME -> { updateColumnStrip() app_state.columnList.forEach { it.fireShowColumnHeader() } } 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) { 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) } else { posted_status_id = null } } REQUEST_CODE_COLUMN_COLOR -> if (data != null) { app_state.saveColumnList() val idx = data.getIntExtra(ActColumnCustomize.EXTRA_COLUMN_INDEX, 0) app_state.column(idx)?.let{ it.fireColumnColor() it.fireShowContent( reason = "ActMain column color changed", reset = true ) } updateColumnStrip() } REQUEST_CODE_LANGUAGE_FILTER -> if (data != null) { app_state.saveColumnList() val idx = data.getIntExtra(ActLanguageFilter.EXTRA_COLUMN_INDEX, 0) app_state.column(idx)?.onLanguageFilterChanged() } } } when (requestCode) { REQUEST_CODE_ACCOUNT_SETTING -> { updateColumnStrip() app_state.columnList.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) { data?.data?.let { importAppData(it) } } } 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.columnCount == 0 ) { finish() return } // カラム設定が開いているならカラム設定を閉じる if (closeColumnSetting()) { return } fun getClosableColumnList(): List { val visibleColumnList = ArrayList() phoneTab({ env -> try { app_state.column(env.pager.currentItem)?.addTo(visibleColumnList) } catch (ex: Throwable) { } }, { env -> visibleColumnList.addAll(env.visibleColumns) }) 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) { 0 -> { if (Pref.bpExitAppWhenCloseProtectedColumn(pref) && Pref.bpDontConfirmBeforeCloseColumn(pref) ) { this@ActMain.finish() } else { showToast(false, R.string.missing_closeable_column) } } 1 -> { closeColumn(closeableColumnList.first()) } else -> { showToast( false, R.string.cant_close_column_by_back_button_when_multiple_column_shown ) } } } // 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) (svColumnStrip.parent as LinearLayout).addViewBeforeLast( View(this).apply { layoutParams = LinearLayout.LayoutParams(padding, 0) } ) llQuickTootBar.addViewBeforeLast( View(this).apply { layoutParams = LinearLayout.LayoutParams(padding, 0) } ) } 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) (svColumnStrip.parent as LinearLayout).addViewAfterFirst( View(this).apply { layoutParams = LinearLayout.LayoutParams(padding, 0) } ) llQuickTootBar.addViewAfterFirst( View(this).apply { layoutParams = LinearLayout.LayoutParams(padding, 0) } ) } } 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 -> 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 env.tablet_pager_adapter = TabletColumnPagerAdapter(this) env.tablet_layout_manager = LinearLayoutManager( this, LinearLayoutManager.HORIZONTAL, false ) 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 ) { 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.columnCount - 1 - ve) if (distance_left < distance_right) { scrollColumnStrip(vs) } else { scrollColumnStrip(ve) } } override fun onScrolled( recyclerView: RecyclerView, dx: Int, dy: Int ) { super.onScrolled(recyclerView, dx, dy) 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( llFormRoot, etQuickToot, true, object : PostHelper.Callback2 { override fun onTextUpdate() {} override fun canOpenPopup(): Boolean { return !drawer.isDrawerOpen(GravityCompat.START) } }) showQuickTootVisibility() } private fun isVisibleColumn(idx: Int) = phoneTab( { env -> val c = env.pager.currentItem c == idx }, { env -> idx >= 0 && idx in env.visibleColumnsIndices } ) private fun updateColumnStrip() { llEmpty.vg(app_state.columnCount==0) 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() app_state.columnList.forEachIndexed { index, column -> 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 = index viewRoot.setOnClickListener { v -> val idx = v.tag as Int if (Pref.bpScrollTopFromColumnStrip(pref) && isVisibleColumn(idx)) { column.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.columnCount == 0 ) { llColumnStrip.setVisibleRange(-1, -1, 0f) } else { phoneTab({ env -> 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 } else { IntRange(vs, min(ve, vs + nScreenColumn - 1)) } var slide_ratio = 0f 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) { "subwaytooter", "misskeyclientproto" -> return try { 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( this@ActMain, defaultInsertPosition, statusInfo.url, statusInfo.statusId, 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( this@ActMain, defaultInsertPosition, null, "https://$instance/@$user", Host.parse(instance), user, original_url = url ) } else { Action_User.profile( this@ActMain, defaultInsertPosition, null, url, 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( this@ActMain, defaultInsertPosition, null, url, 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 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 columnList = app_state.columnList val column =columnList.firstOrNull { it.type == ColumnType.NOTIFICATIONS && it.access_info == account && !it.system_notification_not_related }?.also { scrollToColumn(columnList.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 suspend fun background(client: TootApiClient): TootApiResult? { val uriStr = uri.toString() if (uriStr.startsWith("subwaytooter://misskey/auth_callback") || uriStr.startsWith("misskeyclientproto://misskeyclientproto/auth_callback") ) { // Misskey 認証コールバック val token = uri.getQueryParameter("token") if (token.isNullOrBlank()) return TootApiResult("missing token in callback URL") val prefDevice = PrefDevice.prefDevice(this@ActMain) val instance = Host.parse( prefDevice.getString(PrefDevice.LAST_AUTH_INSTANCE, null) ?: return TootApiResult("missing instance name.") ) 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) { log.trace(ex) return TootApiResult(ex.withCaption("invalid state")) } } val (ti, r2) = TootInstance.get(client) ti ?: return r2 this.ti = ti this.host = instance val parser = TootParser( this@ActMain, linkHelper = LinkHelper.create( instance, misskeyVersion = ti.misskeyVersion ) ) 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") 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") val sv = uri.getQueryParameter("state") 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(",")) { 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) { log.trace(ex) return TootApiResult(ex.withCaption("invalid state")) } param.startsWith("host:") -> { val host = Host.parse(param.substring(5)) client.apiHost = host } // 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 parser = TootParser( this@ActMain, linkHelper = LinkHelper.create(instance) ) return client.authentication2( Pref.spClientName(this@ActMain), code )?.also { this.ta = parser.account(it.jsonObject) } } } override suspend fun handleResult(result: TootApiResult?) { val host = this.host val ta = this.ta var sa = this.sa 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) } }) } 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.columnList .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, account = jsonObject, 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? ) { TootTaskRunner(this@ActMain).run(apiHost, object : TootTask { var ta: TootAccount? = null var ti: TootInstance? = null override suspend 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( apiHost, apDomainArg = instance.uri?.let { Host.parse(it) }, misskeyVersion = misskeyVersion ) ).account(result?.jsonObject) return result } override suspend 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( this, getString(R.string.access_token_or_api_token), null, callback = object : DlgTextInput.Callback { 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.columnList) { val a = column.access_info if (done_list.contains(a)) continue done_list.add(a) if (!a.isNA) a.reloadSetting(this) column.fireShowColumnHeader() } } fun reloadAccountSetting(account: SavedAccount) { val done_list = ArrayList() for (column in app_state.columnList) { 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 } app_state.columnIndex(column)?.let{ page_delete -> phoneTab({ env -> val page_showing = env.pager.currentItem removeColumn(column) if ( page_showing == page_delete) { scrollAndLoad(page_showing-1) } }, { removeColumn(column) scrollAndLoad( page_delete - 1) }) } } 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( { it.pager.currentItem }, { 0 } ) else -> _lastColumnIndex } phoneOnly { env -> env.pager.adapter = null } app_state.editColumnList { list -> for (i in list.indices.reversed()) { val column = list[i] if (column.dont_close) continue list.removeAt(i).dispose() if (lastColumnIndex >= i) --lastColumnIndex } } phoneTab( { env -> env.pager.adapter = env.pager_adapter }, { env -> resizeColumnWidth(env) } ) updateColumnStrip() scrollAndLoad(lastColumnIndex) } private fun scrollAndLoad(idx:Int){ val c = app_state.column(idx) ?:return scrollToColumn(idx) 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) { // 既に同じカラムがあればそこに移動する app_state.columnList.forEachIndexed { i, column -> if (column.isSameSpec(ai, type, params)) { scrollToColumn(i) return column } } } // val col = Column(app_state, ai, type.id, *params) val index = addColumn(col, indexArg) scrollAndLoad(index) return col } fun showColumnMatchAccount(account: SavedAccount) { app_state.columnList.forEach { column -> 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() ?: attrColor(R.attr.colorColumnStripBackground) svColumnStrip.setBackgroundColor(colorColumnStripBackground) llQuickTootBar.setBackgroundColor(colorColumnStripBackground) val colorButtonBg = footer_button_bg_color.notZero() ?: colorColumnStripBackground val colorButtonFg = footer_button_fg_color.notZero() ?: attrColor(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() ?: attrColor(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() ?: attrColor(R.attr.colorAccent) } ///////////////////////////////////////////////////////////////////////// // タブレット対応で必要になった関数など private fun closeColumnSetting(): Boolean { phoneTab({ env -> val vh = env.pager_adapter.getColumnViewHolder(env.pager.currentItem) 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)) { null -> null else -> (env.tablet_pager.getChildViewHolder(v) as? TabletColumnViewHolder)?.columnViewHolder } if (columnViewHolder?.isColumnSettingShown == true) { columnViewHolder.showColumnSetting(false) return@closeColumnSetting true } } }) return false } private fun addColumn(column: Column, indexArg: Int): Int { val index = indexArg.clip(0, app_state.columnCount) phoneOnly { env -> env.pager.adapter = null } app_state.editColumnList { it.add(index, column) } phoneTab( { env -> env.pager.adapter = env.pager_adapter }, { env -> resizeColumnWidth(env) } ) updateColumnStrip() return index } private fun removeColumn(column: Column) { val idx_column = app_state.columnIndex(column) ?: return phoneOnly { env -> env.pager.adapter = null } app_state.editColumnList { it.removeAt(idx_column).dispose() } phoneTab( { env -> env.pager.adapter = env.pager_adapter }, { env -> resizeColumnWidth(env) } ) updateColumnStrip() } private fun setOrder(new_order: List) { phoneOnly { env -> env.pager.adapter = null } app_state.editColumnList { list -> // columns with new order val tmp_list = new_order.mapNotNull { i -> list.elementAtOrNull(i) } val used_set = new_order.toSet() list.forEachIndexed { i, v -> if (!used_set.contains(i)) v.dispose() } list.clear() 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.columnCount 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) { // remove all columns phoneOnly { env -> env.pager.adapter = null } app_state.editColumnList(save = false) { list -> list.forEach { it.dispose() } list.clear() } phoneTab( { env -> env.pager.adapter = env.pager_adapter }, { env -> resizeColumnWidth(env) } ) updateColumnStrip() runWithProgress( "importing app data", doInBackground = { progress -> fun setProgressMessage(sv: String) = runOnMainLooper { progress.setMessageEx(sv) } var newColumnList: ArrayList? = null setProgressMessage("import data to local storage...") // アプリ内領域に一時ファイルを作ってコピーする val cacheDir = cacheDir cacheDir.mkdir() val file = File( cacheDir, "SubwayTooter.${Process.myPid()}.${Process.myTid()}.tmp" ) val source = contentResolver.openInputStream(uri) if (source == null) { showToast(true, "openInputStream failed.") return@runWithProgress null } source.use { inStream -> FileOutputStream(file).use { outStream -> IOUtils.copy(inStream, outStream) } } // 通知サービスを止める setProgressMessage("syncing notification poller…") PollingWorker.queueAppDataImportBefore(this@ActMain) 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) { val entry = zipStream.nextEntry ?: break ++zipEntryCount try { // val entryName = entry.name if (entryName.endsWith(".json")) { newColumnList = AppDataExporter.decodeAppData( this@ActMain, JsonReader(InputStreamReader(zipStream, "UTF-8")) ) continue } if (AppDataExporter.restoreBackgroundImage( this@ActMain, newColumnList, zipStream, entryName ) ) { continue } } finally { zipStream.closeEntry() } } } } catch (ex: Throwable) { log.trace(ex) if (zipEntryCount != 0) { showToast(ex, "importAppData failed.") } } // zipではなかった場合、zipEntryがない状態になる。例外はPH-1では出なかったが、出ても問題ないようにする。 if (zipEntryCount == 0) { InputStreamReader(FileInputStream(file), "UTF-8").use { inStream -> newColumnList = AppDataExporter.decodeAppData( this@ActMain, JsonReader(inStream) ) } } newColumnList }, afterProc = { // cancelled. if (it == null) return@runWithProgress try { phoneOnly { env -> env.pager.adapter = null } app_state.editColumnList { list -> list.clear() list.addAll(it) } phoneTab( { env -> env.pager.adapter = env.pager_adapter }, { env -> resizeColumnWidth(env) } ) updateColumnStrip() } finally { // 通知サービスをリスタート PollingWorker.queueAppDataImportAfter(this@ActMain) } showToast(true, R.string.import_completed_please_restart_app) finish() }, preProc = { window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) }, postProc = { 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( 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)) { "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() } private fun searchFromActivityResult(data: Intent?, columnType: ColumnType) = data?.getStringExtra(Intent.EXTRA_TEXT)?.let { addColumn( false, defaultInsertPosition, SavedAccount.na, columnType, it ) } }