package jp.juggler.subwaytooter import android.content.Context import android.content.Intent import android.net.Uri import android.os.Bundle import android.os.Handler import android.text.Editable import android.text.InputType import android.text.TextWatcher import android.view.KeyEvent import android.view.View import android.view.ViewGroup import android.view.ViewTreeObserver import android.view.inputmethod.EditorInfo import android.widget.AdapterView import android.widget.ArrayAdapter import androidx.appcompat.app.AppCompatActivity import jp.juggler.subwaytooter.action.saveWindowSize import jp.juggler.subwaytooter.actpost.ActPostStates import jp.juggler.subwaytooter.actpost.CompletionHelper import jp.juggler.subwaytooter.actpost.FeaturedTagCache import jp.juggler.subwaytooter.actpost.addAttachment import jp.juggler.subwaytooter.actpost.applyMushroomText import jp.juggler.subwaytooter.actpost.onPickCustomThumbnailImpl import jp.juggler.subwaytooter.actpost.onPostAttachmentCompleteImpl import jp.juggler.subwaytooter.actpost.openAttachment import jp.juggler.subwaytooter.actpost.openMushroom import jp.juggler.subwaytooter.actpost.openVisibilityPicker import jp.juggler.subwaytooter.actpost.performAccountChooser import jp.juggler.subwaytooter.actpost.performAttachmentClick import jp.juggler.subwaytooter.actpost.performMore import jp.juggler.subwaytooter.actpost.performPost import jp.juggler.subwaytooter.actpost.performSchedule import jp.juggler.subwaytooter.actpost.removeReply import jp.juggler.subwaytooter.actpost.resetSchedule import jp.juggler.subwaytooter.actpost.restoreState import jp.juggler.subwaytooter.actpost.saveDraft import jp.juggler.subwaytooter.actpost.saveState import jp.juggler.subwaytooter.actpost.showContentWarningEnabled import jp.juggler.subwaytooter.actpost.showMediaAttachment import jp.juggler.subwaytooter.actpost.showMediaAttachmentProgress import jp.juggler.subwaytooter.actpost.showPoll import jp.juggler.subwaytooter.actpost.showQuotedRenote import jp.juggler.subwaytooter.actpost.showReplyTo import jp.juggler.subwaytooter.actpost.showVisibility import jp.juggler.subwaytooter.actpost.updateText import jp.juggler.subwaytooter.actpost.updateTextCount import jp.juggler.subwaytooter.api.entity.TootScheduled import jp.juggler.subwaytooter.api.entity.TootStatus import jp.juggler.subwaytooter.databinding.ActPostBinding import jp.juggler.subwaytooter.pref.PrefB import jp.juggler.subwaytooter.span.MyClickableSpan import jp.juggler.subwaytooter.span.MyClickableSpanHandler import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.subwaytooter.util.AttachmentPicker import jp.juggler.subwaytooter.util.AttachmentUploader import jp.juggler.subwaytooter.util.PostAttachment import jp.juggler.subwaytooter.util.loadLanguageList import jp.juggler.subwaytooter.util.openBrowser import jp.juggler.subwaytooter.view.MyEditText import jp.juggler.subwaytooter.view.MyNetworkImageView import jp.juggler.util.backPressed import jp.juggler.util.coroutine.launchAndShowError import jp.juggler.util.coroutine.launchIO import jp.juggler.util.coroutine.launchMain import jp.juggler.util.data.GetContentResultEntry import jp.juggler.util.log.LogCategory import jp.juggler.util.log.showToast import jp.juggler.util.string import jp.juggler.util.ui.ActivityResultHandler import jp.juggler.util.ui.isNotOk import kotlinx.coroutines.Job import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.ClosedReceiveChannelException import kotlinx.coroutines.delay import java.lang.ref.WeakReference import java.util.concurrent.CancellationException import java.util.concurrent.ConcurrentHashMap class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callback, MyClickableSpanHandler, AttachmentPicker.Callback { companion object { private val log = LogCategory("ActPost") var refActPost: WeakReference? = null const val EXTRA_POSTED_ACCT = "posted_acct" const val EXTRA_POSTED_STATUS_ID = "posted_status_id" const val EXTRA_POSTED_REPLY_ID = "posted_reply_id" const val EXTRA_POSTED_REDRAFT_ID = "posted_redraft_id" const val EXTRA_MULTI_WINDOW = "multiWindow" const val KEY_ACCOUNT_DB_ID = "account_db_id" const val KEY_REPLY_STATUS = "reply_status" const val KEY_REDRAFT_STATUS = "redraft_status" const val KEY_EDIT_STATUS = "edit_status" const val KEY_INITIAL_TEXT = "initial_text" const val KEY_SHARED_INTENT = "sent_intent" const val KEY_QUOTE = "quote" const val KEY_SCHEDULED_STATUS = "scheduled_status" const val STATE_ALL = "all" ///////////////////////////////////////////////// fun createIntent( context: Context, accountDbId: Long, multiWindowMode: Boolean, // 再編集する投稿。アカウントと同一のタンスであること redraftStatus: TootStatus? = null, // 編集する投稿。アカウントと同一のタンスであること editStatus: TootStatus? = null, // 返信対象の投稿。同一タンス上に同期済みであること replyStatus: TootStatus? = null, //初期テキスト initialText: String? = null, // 外部アプリから共有されたインテント sharedIntent: Intent? = null, // 返信ではなく引用トゥートを作成する quote: Boolean = false, //(Mastodon) 予約投稿の編集 scheduledStatus: TootScheduled? = null, ) = Intent(context, ActPost::class.java).apply { putExtra(EXTRA_MULTI_WINDOW, multiWindowMode) putExtra(KEY_ACCOUNT_DB_ID, accountDbId) initialText?.let { putExtra(KEY_INITIAL_TEXT, it) } redraftStatus?.let { putExtra(KEY_REDRAFT_STATUS, it.json.toString()) } editStatus?.let { putExtra(KEY_EDIT_STATUS, it.json.toString()) } replyStatus?.let { putExtra(KEY_REPLY_STATUS, it.json.toString()) putExtra(KEY_QUOTE, quote) } sharedIntent?.let { putExtra(KEY_SHARED_INTENT, it) } scheduledStatus?.let { putExtra(KEY_SCHEDULED_STATUS, it.src.toString()) } } } val views by lazy { ActPostBinding.inflate(layoutInflater) } lateinit var ivMedia: List lateinit var etChoices: List lateinit var handler: Handler lateinit var appState: AppState lateinit var attachmentUploader: AttachmentUploader lateinit var attachmentPicker: AttachmentPicker lateinit var completionHelper: CompletionHelper var density: Float = 0f val languages by lazy { loadLanguageList() } private lateinit var progressChannel: Channel /////////////////////////////////////////////////// // SavedAccount.acctAscii => FeaturedTagCache val featuredTagCache = ConcurrentHashMap() // background job var jobFeaturedTag: WeakReference? = null var jobMaxCharCount: WeakReference? = null /////////////////////////////////////////////////// var states = ActPostStates() var accountList: List = emptyList() var account: SavedAccount? = null var attachmentList = ArrayList() var isPostComplete: Boolean = false var scheduledStatus: TootScheduled? = null ///////////////////////////////////////////////////////////////////// val isMultiWindowPost: Boolean get() = intent.getBooleanExtra(EXTRA_MULTI_WINDOW, false) val arMushroom = ActivityResultHandler(log) { r -> if (r.isNotOk) return@ActivityResultHandler r.data?.string("replace_key")?.let { text -> when (states.mushroomInput) { 0 -> applyMushroomText(views.etContent, text) 1 -> applyMushroomText(views.etContentWarning, text) else -> for (i in 0..3) { if (states.mushroomInput == i + 2) { applyMushroomText(etChoices[i], text) } } } } } //////////////////////////////////////////////////////////////// override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) backPressed { launchAndShowError { finish() // 戻るボタンを押したときとonPauseで2回保存することになるが、 // 同じ内容はDB上は重複しないはず… saveDraft() } } if (isMultiWindowPost) ActMain.refActMain?.get()?.closeList?.add(WeakReference(this)) App1.setActivityTheme(this) appState = App1.getAppState(this) handler = appState.handler attachmentUploader = AttachmentUploader(this, handler) attachmentPicker = AttachmentPicker(this, this) density = resources.displayMetrics.density arMushroom.register(this) progressChannel = Channel(capacity = Channel.CONFLATED) initUI() // 進捗表示チャネルの回収コルーチン launchAndShowError { try { while (true) { progressChannel.receive() showMediaAttachmentProgress() delay(1000L) } } catch (ex: Throwable) { when (ex) { is CancellationException, is ClosedReceiveChannelException -> Unit else -> log.e(ex, "can't show media progress.") } } } // 初期化の続きをコルーチンでやる launchAndShowError { when (savedInstanceState) { null -> updateText(intent, saveDraft = false) else -> restoreState(savedInstanceState) } } } override fun onDestroy() { try { progressChannel.close() } catch (ex: Throwable) { log.e(ex, "progressChannel close failed.") } completionHelper.onDestroy() attachmentUploader.onActivityDestroy() super.onDestroy() } override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) saveState(outState) } override fun onRestoreInstanceState(savedInstanceState: Bundle) { super.onRestoreInstanceState(savedInstanceState) showContentWarningEnabled() showMediaAttachment() showVisibility() updateTextCount() launchAndShowError { showReplyTo() } showPoll() showQuotedRenote() } override fun onResume() { super.onResume() refActPost = WeakReference(this) } override fun onPause() { super.onPause() if (!isPostComplete) launchMain { try { // 編集中にホーム画面を押したり他アプリに移動する場合は下書きを保存する // やや過剰な気がするが、自アプリに戻ってくるときにランチャーからアイコンタップされると // メイン画面より上にあるアクティビティはすべて消されてしまうので // このタイミングで保存するしかない saveDraft() } catch (ex: Throwable) { log.e(ex, "can't save draft.") showToast(ex, "can't save draft.") } } } override fun onClick(v: View) { refActPost = WeakReference(this) when (v.id) { R.id.btnAccount -> performAccountChooser() R.id.ivAccount -> performAccountChooser() R.id.btnVisibility -> openVisibilityPicker() R.id.btnAttachment -> openAttachment() R.id.ivMedia1 -> performAttachmentClick(0) R.id.ivMedia2 -> performAttachmentClick(1) R.id.ivMedia3 -> performAttachmentClick(2) R.id.ivMedia4 -> performAttachmentClick(3) R.id.btnPost -> performPost() R.id.btnRemoveReply -> launchAndShowError { removeReply() } R.id.btnMore -> performMore() R.id.btnPlugin -> launchAndShowError { openMushroom() } R.id.btnEmojiPicker -> completionHelper.openEmojiPickerFromMore() R.id.btnFeaturedTag -> completionHelper.openFeaturedTagList( featuredTagCache[account?.acct?.ascii ?: ""]?.list ) R.id.ibSchedule -> performSchedule() R.id.ibScheduleReset -> resetSchedule() } } override fun onKeyShortcut(keyCode: Int, event: KeyEvent?): Boolean { return when { super.onKeyShortcut(keyCode, event) -> true event?.isCtrlPressed == true && keyCode == KeyEvent.KEYCODE_T -> { views.btnPost.performClick() true } else -> false } } override fun onMyClickableSpanClicked(viewClicked: View, span: MyClickableSpan) { openBrowser(span.linkInfo.url) } override fun onPickAttachment(uri: Uri, mimeType: String?) { addAttachment(uri, mimeType) } override fun onPostAttachmentProgress() { launchIO { try { progressChannel.send(Unit) } catch (ex: Throwable) { log.w(ex, "progressChannel send failed.") } } } override fun onPostAttachmentComplete(pa: PostAttachment) { onPostAttachmentCompleteImpl(pa) } override fun resumeCustomThumbnailTarget(id: String?): PostAttachment? { id ?: return null return attachmentList.find { it.attachment?.id?.toString() == id } } override fun onPickCustomThumbnail(pa: PostAttachment, src: GetContentResultEntry) { onPickCustomThumbnailImpl(pa, src) } fun initUI() { setContentView(views.root) if (PrefB.bpPostButtonBarTop.value) { val bar = findViewById(R.id.llFooterBar) val parent = bar.parent as ViewGroup parent.removeView(bar) parent.addView(bar, 0) } if (!isMultiWindowPost) { fixHorizontalMargin(findViewById(R.id.scrollView)) fixHorizontalMargin(findViewById(R.id.llFooterBar)) } views.root.callbackOnSizeChanged = { _, _, _, _ -> if (isMultiWindowPost) saveWindowSize() // ビューのw,hはシステムバーその他を含まないので使わない } // https://github.com/tateisu/SubwayTooter/issues/123 // 早い段階で指定する必要がある views.etContent.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_MULTI_LINE views.etContent.imeOptions = EditorInfo.IME_ACTION_NONE views.spPollType.apply { this.adapter = ArrayAdapter( this@ActPost, android.R.layout.simple_spinner_item, arrayOf( getString(R.string.poll_dont_make), getString(R.string.poll_make), ) ).apply { setDropDownViewResource(R.layout.lv_spinner_dropdown) } this.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { override fun onNothingSelected(parent: AdapterView<*>?) { showPoll() updateTextCount() } override fun onItemSelected( parent: AdapterView<*>?, view: View?, position: Int, id: Long, ) { showPoll() updateTextCount() } } } ivMedia = listOf( views.ivMedia1, views.ivMedia2, views.ivMedia3, views.ivMedia4, ) etChoices = listOf( views.etChoice1, views.etChoice2, views.etChoice3, views.etChoice4, ) arrayOf( views.ibSchedule, views.ibScheduleReset, views.btnAccount, views.btnVisibility, views.btnAttachment, views.btnPost, views.btnRemoveReply, views.btnFeaturedTag, views.btnPlugin, views.btnEmojiPicker, views.btnMore, views.ivAccount, ).forEach { it.setOnClickListener(this) } ivMedia.forEach { it.setOnClickListener(this) } views.cbContentWarning.setOnCheckedChangeListener { _, _ -> showContentWarningEnabled() } completionHelper = CompletionHelper(this, appState.handler) completionHelper.attachEditText( views.root, views.etContent, false, object : CompletionHelper.Callback2 { override fun onTextUpdate() { updateTextCount() } override fun canOpenPopup(): Boolean = true }) val textWatcher: TextWatcher = object : TextWatcher { override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {} override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {} override fun afterTextChanged(editable: Editable) { updateTextCount() } } views.etContentWarning.addTextChangedListener(textWatcher) for (et in etChoices) { et.addTextChangedListener(textWatcher) } val scrollListener: ViewTreeObserver.OnScrollChangedListener = ViewTreeObserver.OnScrollChangedListener { completionHelper.onScrollChanged() } views.scrollView.viewTreeObserver.addOnScrollChangedListener(scrollListener) views.etContent.contentCallback = { addAttachment(it) } views.spLanguage.adapter = ArrayAdapter( this, android.R.layout.simple_spinner_item, languages.map { it.second }.toTypedArray() ).apply { setDropDownViewResource(R.layout.lv_spinner_dropdown) } } }