/* * Twidere - Twitter client for Android * * Copyright (C) 2012-2014 Mariotaku Lee * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package org.mariotaku.twidere.activity import android.accounts.AccountManager import android.animation.AnimatorSet import android.animation.ObjectAnimator import android.annotation.SuppressLint import android.app.Activity import android.app.Dialog import android.content.ActivityNotFoundException import android.content.DialogInterface import android.content.Intent import android.graphics.Canvas import android.graphics.Rect import android.location.* import android.net.Uri import android.os.Build import android.os.Bundle import androidx.core.app.ActivityCompat import androidx.core.widget.TextViewCompat import androidx.appcompat.app.AlertDialog import androidx.appcompat.view.SupportMenuInflater import androidx.appcompat.widget.ActionMenuView.OnMenuItemClickListener import androidx.recyclerview.widget.FixedLinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.ViewHolder import androidx.recyclerview.widget.ItemTouchHelper import android.text.Editable import android.text.Spannable import android.text.Spanned import android.text.TextUtils import android.text.method.LinkMovementMethod import android.text.style.* import android.view.* import android.view.View.OnClickListener import android.view.View.OnLongClickListener import android.view.animation.DecelerateInterpolator import android.widget.ImageView import android.widget.Toast import com.bumptech.glide.Glide import com.twitter.twittertext.Extractor import kotlinx.android.synthetic.main.activity_compose.* import nl.komponents.kovenant.task import org.mariotaku.abstask.library.AbstractTask import org.mariotaku.abstask.library.TaskStarter import org.mariotaku.kpreferences.get import org.mariotaku.kpreferences.set import org.mariotaku.ktextension.* import org.mariotaku.library.objectcursor.ObjectCursor import org.mariotaku.microblog.library.mastodon.annotation.StatusVisibility import org.mariotaku.pickncrop.library.MediaPickerActivity import org.mariotaku.twidere.Constants.* import org.mariotaku.twidere.R import org.mariotaku.twidere.adapter.BaseRecyclerViewAdapter import org.mariotaku.twidere.adapter.MediaPreviewAdapter import org.mariotaku.twidere.annotation.AccountType import org.mariotaku.twidere.constant.* import org.mariotaku.twidere.constant.IntentConstants.EXTRA_SCREEN_NAME import org.mariotaku.twidere.extension.* import org.mariotaku.twidere.extension.model.* import org.mariotaku.twidere.extension.text.twitter.ReplyTextAndMentions import org.mariotaku.twidere.extension.text.twitter.extractReplyTextAndMentions import org.mariotaku.twidere.fragment.* import org.mariotaku.twidere.fragment.PermissionRequestDialog.PermissionRequestCancelCallback import org.mariotaku.twidere.model.* import org.mariotaku.twidere.model.analyzer.PurchaseFinished import org.mariotaku.twidere.model.draft.UpdateStatusActionExtras import org.mariotaku.twidere.model.schedule.ScheduleInfo import org.mariotaku.twidere.model.util.AccountUtils import org.mariotaku.twidere.model.util.ParcelableLocationUtils import org.mariotaku.twidere.preference.ComponentPickerPreference import org.mariotaku.twidere.provider.TwidereDataStore.Drafts import org.mariotaku.twidere.service.LengthyOperationsService import org.mariotaku.twidere.task.compose.AbsAddMediaTask import org.mariotaku.twidere.task.compose.AbsDeleteMediaTask import org.mariotaku.twidere.task.twitter.UpdateStatusTask import org.mariotaku.twidere.text.MarkForDeleteSpan import org.mariotaku.twidere.text.style.EmojiSpan import org.mariotaku.twidere.util.* import org.mariotaku.twidere.util.EditTextEnterHandler.EnterListener import org.mariotaku.twidere.util.dagger.GeneralComponent import org.mariotaku.twidere.util.premium.ExtraFeaturesService import org.mariotaku.twidere.util.text.StatusTextValidator import org.mariotaku.twidere.util.view.SimpleTextWatcher import org.mariotaku.twidere.util.view.ViewAnimator import org.mariotaku.twidere.util.view.ViewProperties import org.mariotaku.twidere.view.CheckableLinearLayout import org.mariotaku.twidere.view.ExtendedRecyclerView import org.mariotaku.twidere.view.ShapedImageView import org.mariotaku.twidere.view.helper.SimpleItemTouchHelperCallback import org.mariotaku.twidere.view.holder.compose.MediaPreviewViewHolder import java.io.IOException import java.lang.ref.WeakReference import java.text.Normalizer import java.util.* import javax.inject.Inject import kotlin.collections.ArrayList import kotlin.math.abs import android.Manifest.permission as AndroidPermission @SuppressLint("RestrictedApi") class ComposeActivity : BaseActivity(), OnMenuItemClickListener, OnClickListener, OnLongClickListener, ActionMode.Callback, PermissionRequestCancelCallback, EditAltTextDialogFragment.EditAltTextCallback { // Utility classes @Inject lateinit var extractor: Extractor @Inject lateinit var locationManager: LocationManager private lateinit var itemTouchHelper: ItemTouchHelper private lateinit var bottomMenuAnimator: ViewAnimator private val supportMenuInflater by lazy { SupportMenuInflater(this) } private val backTimeoutRunnable = Runnable { navigateBackPressed = false } // Adapters private lateinit var mediaPreviewAdapter: MediaPreviewAdapter private lateinit var accountsAdapter: AccountIconsAdapter // State fields private var shouldSkipDraft: Boolean = false private var editSummaryEnabled: Boolean = false private var hasEditSummary: Boolean = false private var hasAttachmentStatusVisibility: Boolean = false private var hasStatusVisibility: Boolean = false private var hasLocationOption: Boolean = false private var ignoreMentions: Boolean = false private var shouldSaveAccounts: Boolean = false private var shouldSaveVisibility: Boolean = false private var statusShortenerUsed: Boolean = false private var navigateBackPressed: Boolean = false private var replyToSelf: Boolean = false // Data fields private var recentLocation: ParcelableLocation? = null private var inReplyToStatus: ParcelableStatus? = null private var mentionUser: ParcelableUser? = null private var originalText: String? = null private var possiblySensitive: Boolean = false private var imageUploaderUsed: Boolean = false private var textChanged: Boolean = false private var composeKeyMetaState: Int = 0 private var draft: Draft? = null private var nameFirst: Boolean = false private var draftUniqueId: String? = null private var statusVisibility: String? = null private var scheduleInfo: ScheduleInfo? = null set(value) { field = value updateUpdateStatusIcon() } // Listeners private var locationListener: LocationListener? = null private val draftAction: String get() = draft?.action_type ?: when (intent.action) { INTENT_ACTION_REPLY -> Draft.Action.REPLY INTENT_ACTION_QUOTE -> Draft.Action.QUOTE else -> Draft.Action.UPDATE_STATUS } private val media: Array get() = mediaList.toTypedArray() private val mediaList: List get() = mediaPreviewAdapter.asList() private var isAccountSelectorVisible: Boolean get() = bottomMenuAnimator.currentChild == accountSelector set(visible) { bottomMenuAnimator.showView(if (visible) accountSelector else composeMenu, true) displaySelectedAccountsIcon() } private val hasMedia: Boolean get() = mediaPreviewAdapter.itemCount > 0 private val isQuote: Boolean get() = INTENT_ACTION_QUOTE == intent.action private val isQuotingProtectedStatus: Boolean get() { val status = inReplyToStatus if (!isQuote || status == null) return false return !status.can_retweet && status.account_key != status.user_key } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) GeneralComponent.get(this).inject(this) nameFirst = preferences[nameFirstKey] setContentView(R.layout.activity_compose) bottomMenuAnimator = ViewAnimator() bottomMenuAnimator.setupViews() mediaPreviewAdapter = MediaPreviewAdapter(this, requestManager) mediaPreviewAdapter.listener = object : MediaPreviewAdapter.Listener { override fun onEditClick(position: Int, holder: MediaPreviewViewHolder) { attachedMediaPreview.showContextMenuForChild(holder.itemView) } override fun onRemoveClick(position: Int, holder: MediaPreviewViewHolder) { mediaPreviewAdapter.remove(position) updateMediaState() setMenu() } override fun onStartDrag(viewHolder: ViewHolder) { itemTouchHelper.startDrag(viewHolder) } } itemTouchHelper = ItemTouchHelper(AttachedMediaItemTouchHelperCallback(mediaPreviewAdapter.touchAdapter)) setFinishOnTouchOutside(false) val am = AccountManager.get(this) val accounts = AccountUtils.getAccounts(am) if (accounts.isEmpty()) { Toast.makeText(this, R.string.message_toast_no_account, Toast.LENGTH_SHORT).show() shouldSkipDraft = true finish() return } val accountDetails = AccountUtils.getAllAccountDetails(am, accounts, true) val defaultAccountKeys = accountDetails.mapToArray(AccountDetails::key) menuBar.setOnMenuItemClickListener(this) setupEditText() accountSelectorButton.setOnClickListener(this) replyLabel.setOnClickListener(this) hintLabel.spannable = HtmlSpanBuilder.fromHtml(getString(R.string.hint_status_reply_to_user_removed)).apply { val dialogSpan = getSpans(0, length, URLSpan::class.java).firstOrNull { "#dialog" == it.url } if (dialogSpan != null) { val spanStart = getSpanStart(dialogSpan) val spanEnd = getSpanEnd(dialogSpan) removeSpan(dialogSpan) setSpan(object : ClickableSpan() { override fun onClick(widget: View) { MessageDialogFragment.show(supportFragmentManager, message = getString(R.string.message_status_reply_to_user_removed_explanation), tag = "status_reply_to_user_removed_explanation") } }, spanStart, spanEnd, Spanned.SPAN_INCLUSIVE_INCLUSIVE) } } hintLabel.movementMethod = LinkMovementMethod.getInstance() hintLabel.linksClickable = true accountSelector.layoutManager = FixedLinearLayoutManager(this).apply { orientation = LinearLayoutManager.HORIZONTAL reverseLayout = false stackFromEnd = false } accountsAdapter = AccountIconsAdapter(this).apply { setAccounts(accountDetails) } accountSelector.adapter = accountsAdapter attachedMediaPreview.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false) attachedMediaPreview.adapter = mediaPreviewAdapter registerForContextMenu(attachedMediaPreview) itemTouchHelper.attachToRecyclerView(attachedMediaPreview) attachedMediaPreview.addItemDecoration(PreviewGridItemDecoration(resources.getDimensionPixelSize(R.dimen.element_spacing_small))) if (savedInstanceState == null) { // The context was first created val notificationId = intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1) val notificationAccount = intent.getParcelableExtra(EXTRA_NOTIFICATION_ACCOUNT) if (notificationId != -1) { twitterWrapper.clearNotificationAsync(notificationId, notificationAccount) } if (!handleIntent(intent)) { handleDefaultIntent(intent) } showLabelAndHint(intent) val selectedAccountKeys = accountsAdapter.selectedAccountKeys if (selectedAccountKeys.isNullOrEmpty()) { val idsInPrefs = kPreferences[composeAccountsKey]?.asList() ?: emptyList() val intersection = defaultAccountKeys.intersect(idsInPrefs) if (intersection.isEmpty()) { accountsAdapter.selectedAccountKeys = defaultAccountKeys } else { accountsAdapter.selectedAccountKeys = intersection.toTypedArray() } } if (statusVisibility == null) { statusVisibility = preferences[composeStatusVisibilityKey] } originalText = ParseUtils.parseString(editText.text) } val menu = menuBar.menu supportMenuInflater.inflate(R.menu.menu_compose, menu) ThemeUtils.wrapMenuIcon(menuBar) updateStatus.setOnClickListener(this) updateStatus.setOnLongClickListener(this) val composeExtensionsIntent = Intent(INTENT_ACTION_EXTENSION_COMPOSE) val imageExtensionsIntent = Intent(INTENT_ACTION_EXTENSION_EDIT_IMAGE) val mediaMenuItem = menu.findItem(R.id.status_attachment) if (mediaMenuItem != null && mediaMenuItem.hasSubMenu()) { val subMenu = mediaMenuItem.subMenu MenuUtils.addIntentToMenu(this, subMenu, composeExtensionsIntent, MENU_GROUP_COMPOSE_EXTENSION) MenuUtils.addIntentToMenu(this, subMenu, imageExtensionsIntent, MENU_GROUP_IMAGE_EXTENSION) } updateViewStyle() bottomMenuAnimator.showView(composeMenu, false) textChanged = false resetButtonsStates() } override fun onDestroy() { if (shouldSkipDraft || !isFinishing) { super.onDestroy() return } if (intent.getBooleanExtra(EXTRA_SAVE_DRAFT, true) && hasComposingStatus()) { saveToDrafts() Toast.makeText(this, R.string.message_toast_status_saved_to_draft, Toast.LENGTH_SHORT).show() } else { discardTweet() } super.onDestroy() } override fun onStart() { super.onStart() imageUploaderUsed = !ComponentPickerPreference.isNoneValue(kPreferences[mediaUploaderKey]) statusShortenerUsed = !ComponentPickerPreference.isNoneValue(kPreferences[statusShortenerKey]) if (kPreferences[attachLocationKey]) { if (checkAnySelfPermissionsGranted(AndroidPermission.ACCESS_COARSE_LOCATION, AndroidPermission.ACCESS_FINE_LOCATION)) { try { startLocationUpdateIfEnabled() } catch (e: SecurityException) { } } } setMenu() updateTextCount() val textSize = preferences[textSizeKey] editText.textSize = textSize * 1.25f } override fun onStop() { saveAccountSelection() saveVisibility() try { locationListener?.let { locationManager.removeUpdates(it) locationListener = null } } catch (ignore: SecurityException) { // That should not happen } super.onStop() } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) when (requestCode) { REQUEST_TAKE_PHOTO, REQUEST_PICK_MEDIA -> { if (resultCode == Activity.RESULT_OK && data != null) { val src = MediaPickerActivity.getMediaUris(data) TaskStarter.execute(AddMediaTask(this, src, null, copySrc = false, deleteSrc = false )) val extras = data.getBundleExtra(MediaPickerActivity.EXTRA_EXTRAS) if (extras?.getBoolean(EXTRA_IS_POSSIBLY_SENSITIVE) == true) { possiblySensitive = true } } } REQUEST_EDIT_IMAGE -> { if (resultCode == Activity.RESULT_OK && data != null) { if (data.data != null) { setMenu() updateTextCount() } } } REQUEST_EXTENSION_COMPOSE -> { if (resultCode == Activity.RESULT_OK && data != null) { // The latter two is for compatibility val text = data.getCharSequenceExtra(Intent.EXTRA_TEXT) ?: data.getStringExtra(EXTRA_TEXT) ?: data.getStringExtra(EXTRA_APPEND_TEXT) val isReplaceMode = data.getBooleanExtra(EXTRA_IS_REPLACE_MODE, data.getStringExtra(EXTRA_APPEND_TEXT) == null) if (text != null) { val editable = editText.editableText if (editable == null || isReplaceMode) { editText.setText(text) } else { editable.replace(editText.selectionStart, editText.selectionEnd, text) } setMenu() updateTextCount() } val src = MediaPickerActivity.getMediaUris(data)?.takeIf(Array::isNotEmpty) ?: data.getParcelableExtra(EXTRA_IMAGE_URI)?.let { arrayOf(it) } if (src != null) { TaskStarter.execute(AddMediaTask(this, src, null, copySrc = false, deleteSrc = false )) } } } REQUEST_PURCHASE_EXTRA_FEATURES -> { if (resultCode == Activity.RESULT_OK) { Analyzer.log(PurchaseFinished.create(data!!)) } } REQUEST_SET_SCHEDULE -> { if (resultCode == Activity.RESULT_OK) { scheduleInfo = data?.getParcelableExtra(EXTRA_SCHEDULE_INFO) } } REQUEST_ADD_GIF -> { if (resultCode == Activity.RESULT_OK && data != null) { val intent = ThemedMediaPickerActivity.withThemed(this@ComposeActivity) .getMedia(data.data!!) .extras(Bundle { this[EXTRA_IS_POSSIBLY_SENSITIVE] = data.getBooleanExtra(EXTRA_IS_POSSIBLY_SENSITIVE, false) }) .build() startActivityForResult(intent, REQUEST_PICK_MEDIA) } } } } override fun onSaveInstanceState(outState: Bundle) { outState.putParcelableArray(EXTRA_ACCOUNT_KEYS, accountsAdapter.selectedAccountKeys) outState.putParcelableArrayList(EXTRA_MEDIA, ArrayList(mediaList)) outState.putBoolean(EXTRA_IS_POSSIBLY_SENSITIVE, possiblySensitive) outState.putParcelable(EXTRA_STATUS, inReplyToStatus) outState.putParcelable(EXTRA_USER, mentionUser) outState.putParcelable(EXTRA_DRAFT, draft) outState.putParcelable(EXTRA_SCHEDULE_INFO, scheduleInfo) outState.putString(EXTRA_VISIBILITY, statusVisibility) outState.putBoolean(EXTRA_SHOULD_SAVE_ACCOUNTS, shouldSaveAccounts) outState.putString(EXTRA_ORIGINAL_TEXT, originalText) outState.putString(EXTRA_DRAFT_UNIQUE_ID, draftUniqueId) super.onSaveInstanceState(outState) } override fun onRestoreInstanceState(savedInstanceState: Bundle) { super.onRestoreInstanceState(savedInstanceState) // Restore from previous saved state accountsAdapter.selectedAccountKeys = savedInstanceState .getNullableTypedArray(EXTRA_ACCOUNT_KEYS) ?: emptyArray() possiblySensitive = savedInstanceState.getBoolean(EXTRA_IS_POSSIBLY_SENSITIVE) val mediaList = savedInstanceState.getParcelableArrayList(EXTRA_MEDIA) if (mediaList != null) { addMedia(mediaList) } inReplyToStatus = savedInstanceState.getParcelable(EXTRA_STATUS) mentionUser = savedInstanceState.getParcelable(EXTRA_USER) draft = savedInstanceState.getParcelable(EXTRA_DRAFT) shouldSaveAccounts = savedInstanceState.getBoolean(EXTRA_SHOULD_SAVE_ACCOUNTS) shouldSaveVisibility = savedInstanceState.getBoolean(EXTRA_SHOULD_SAVE_VISIBILITY) originalText = savedInstanceState.getString(EXTRA_ORIGINAL_TEXT) draftUniqueId = savedInstanceState.getString(EXTRA_DRAFT_UNIQUE_ID) scheduleInfo = savedInstanceState.getParcelable(EXTRA_SCHEDULE_INFO) statusVisibility = savedInstanceState.getString(EXTRA_VISIBILITY) showLabelAndHint(intent) resetButtonsStates() } override fun onClick(view: View) { when (view) { updateStatus -> { confirmAndUpdateStatus() } accountSelectorButton -> { isAccountSelectorVisible = !isAccountSelectorVisible } replyLabel -> { if (replyLabel.visibility != View.VISIBLE) return replyLabel.isSingleLine = replyLabel.lineCount > 1 } } } override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { return true } override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { return true } override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { return false } override fun onDestroyActionMode(mode: ActionMode) { val window = window val contentView = window.findViewById(android.R.id.content) contentView.setPadding(contentView.paddingLeft, 0, contentView.paddingRight, contentView.paddingBottom) } override fun onLongClick(v: View): Boolean { when (v) { updateStatus -> { Utils.showMenuItemToast(v, getString(R.string.action_send), true) return true } } return false } override fun onMenuItemClick(item: MenuItem): Boolean { when (item.itemId) { R.id.take_photo -> { requestOrTakePhoto() } R.id.record_video -> { requestOrCaptureVideo() } R.id.add_media -> { requestOrPickMedia() } R.id.drafts -> { IntentUtils.openDrafts(this) } R.id.delete -> { val media = this.media val task = DeleteMediaTask(this, media) task.callback = { result -> setProgressVisible(false) removeMedia(media.filterIndexed { i, _ -> result[i] }) setMenu() if (result.contains(false)) { Toast.makeText(this, R.string.message_toast_error_occurred, Toast.LENGTH_SHORT).show() } } TaskStarter.execute(task) } R.id.toggle_sensitive -> { if (!hasMedia) return true possiblySensitive = !possiblySensitive setMenu() updateTextCount() } R.id.schedule -> { val provider = statusScheduleProvider ?: return true startActivityForResult(provider.createSetScheduleIntent(), REQUEST_SET_SCHEDULE) } R.id.add_gif -> { val provider = gifShareProvider ?: return true startActivityForResult(provider.createGifSelectorIntent(), REQUEST_ADD_GIF) } R.id.edit_summary -> { editSummaryEnabled = true updateSummaryTextState() } else -> { when (item.groupId) { R.id.location_option -> { locationMenuItemSelected(item) } R.id.visibility_option, R.id.attachment_visibility_option -> { visibilityMenuItemSelected(item) } else -> { extensionIntentItemSelected(item) } } } } return true } override fun dispatchTouchEvent(ev: MotionEvent): Boolean { when (ev.actionMasked) { MotionEvent.ACTION_DOWN -> { if (isAccountSelectorVisible && !TwidereViewUtils.hitView(ev, accountSelectorButton)) { val layoutManager = accountSelector.layoutManager ?: return super.dispatchTouchEvent(ev) val clickedItem = (0 until layoutManager.childCount).any { val child = layoutManager.getChildAt(it) child != null && TwidereViewUtils.hitView(ev, child) } if (!clickedItem) { isAccountSelectorVisible = false return true } } } } return super.dispatchTouchEvent(ev) } override fun onTouchEvent(event: MotionEvent): Boolean { if (event.action == MotionEvent.ACTION_DOWN) { val decorView = window.peekDecorView() if (decorView != null && !TwidereViewUtils.hitView(event, decorView) && !hasComposingStatus()) { onBackPressed() return true } } return super.onTouchEvent(event) } override fun getMenuInflater(): MenuInflater { return supportMenuInflater } override fun dispatchKeyEvent(event: KeyEvent): Boolean { val keyCode = event.keyCode if (KeyEvent.isModifierKey(keyCode)) { val action = event.action if (action == MotionEvent.ACTION_DOWN) { composeKeyMetaState = composeKeyMetaState or KeyboardShortcutsHandler.getMetaStateForKeyCode(keyCode) } else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { composeKeyMetaState = composeKeyMetaState and KeyboardShortcutsHandler.getMetaStateForKeyCode(keyCode).inv() } } return super.dispatchKeyEvent(event) } override fun onCreateContextMenu(menu: ContextMenu, v: View, menuInfo: ContextMenu.ContextMenuInfo) { if (menuInfo !is ExtendedRecyclerView.ContextMenuInfo) return when (menuInfo.recyclerViewId) { R.id.attachedMediaPreview -> { menu.setHeaderTitle(R.string.edit_media) supportMenuInflater.inflate(R.menu.menu_attached_media_edit, menu) } } } override fun onContextItemSelected(item: MenuItem): Boolean { val menuInfo = item.menuInfo as? ExtendedRecyclerView.ContextMenuInfo ?: run { return super.onContextItemSelected(item) } when (menuInfo.recyclerViewId) { R.id.attachedMediaPreview -> { when (item.itemId) { R.id.edit_description -> { val position = menuInfo.position val altText = mediaPreviewAdapter.getItem(position).alt_text executeAfterFragmentResumed { activity -> EditAltTextDialogFragment.show(activity.supportFragmentManager, position, altText) } } } return true } } return super.onContextItemSelected(item) } override fun handleKeyboardShortcutSingle(handler: KeyboardShortcutsHandler, keyCode: Int, event: KeyEvent, metaState: Int): Boolean { val action = handler.getKeyAction(KeyboardShortcutConstants.CONTEXT_TAG_NAVIGATION, keyCode, event, metaState) if (KeyboardShortcutConstants.ACTION_NAVIGATION_BACK == action) { if (editText.length() == 0 && !textChanged) { if (!navigateBackPressed) { Toast.makeText(this, getString(R.string.message_toast_press_again_to_close), Toast.LENGTH_SHORT).show() editText.removeCallbacks(backTimeoutRunnable) editText.postDelayed(backTimeoutRunnable, 2000) } else { onBackPressed() } navigateBackPressed = true } else { textChanged = false } return true } return super.handleKeyboardShortcutSingle(handler, keyCode, event, metaState) } override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { when (requestCode) { REQUEST_ATTACH_LOCATION_PERMISSION -> { if (checkAnySelfPermissionsGranted(AndroidPermission.ACCESS_FINE_LOCATION, AndroidPermission.ACCESS_COARSE_LOCATION)) { try { startLocationUpdateIfEnabled() } catch (e: SecurityException) { // That should not happen } } else { Toast.makeText(this, R.string.message_toast_cannot_get_location, Toast.LENGTH_SHORT).show() kPreferences.edit { this[attachLocationKey] = false this[attachPreciseLocationKey] = false } } } REQUEST_TAKE_PHOTO_PERMISSION -> { if (!checkAnySelfPermissionsGranted(AndroidPermission.WRITE_EXTERNAL_STORAGE)) { Toast.makeText(this, R.string.message_toast_compose_write_storage_no_permission, Toast.LENGTH_SHORT).show() } takePhoto() } REQUEST_CAPTURE_VIDEO_PERMISSION -> { if (!checkAnySelfPermissionsGranted(AndroidPermission.WRITE_EXTERNAL_STORAGE)) { Toast.makeText(this, R.string.message_toast_compose_write_storage_no_permission, Toast.LENGTH_SHORT).show() } captureVideo() } REQUEST_PICK_MEDIA_PERMISSION -> { if (!checkAnySelfPermissionsGranted(AndroidPermission.WRITE_EXTERNAL_STORAGE)) { Toast.makeText(this, R.string.message_toast_compose_write_storage_no_permission, Toast.LENGTH_SHORT).show() } pickMedia() } } } override fun onRequestPermissionCancelled(requestCode: Int) { when (requestCode) { REQUEST_ATTACH_LOCATION_PERMISSION -> { } } } override fun onSetAltText(position: Int, altText: String?) { mediaPreviewAdapter.setAltText(position, altText) } private fun locationMenuItemSelected(item: MenuItem) { item.isChecked = true var attachLocationChecked = false var attachPreciseLocationChecked = false when (item.itemId) { R.id.location_precise -> { attachLocationChecked = true attachPreciseLocationChecked = true locationLabel.tag = null } R.id.location_coarse -> { attachLocationChecked = true attachPreciseLocationChecked = false } } kPreferences.edit { this[attachLocationKey] = attachLocationChecked this[attachPreciseLocationKey] = attachPreciseLocationChecked } if (attachLocationChecked) { requestOrUpdateLocation() } else if (locationListener != null) { try { locationListener?.let { locationManager.removeUpdates(it) locationListener = null } } catch (e: SecurityException) { //Ignore } } updateLocationState() setMenu() updateTextCount() } private fun visibilityMenuItemSelected(item: MenuItem) { statusVisibility = when (item.itemId) { R.id.visibility_public, R.id.attachment_visibility_public -> StatusVisibility.PUBLIC R.id.visibility_unlisted, R.id.attachment_visibility_unlisted -> StatusVisibility.UNLISTED R.id.visibility_private, R.id.attachment_visibility_private -> StatusVisibility.PRIVATE R.id.visibility_direct, R.id.attachment_visibility_direct -> StatusVisibility.DIRECT else -> null } updateVisibilityState() updateSummaryTextState() setMenu() updateTextCount() } private fun extensionIntentItemSelected(item: MenuItem) { val intent = item.intent ?: return try { when (intent.action) { INTENT_ACTION_EXTENSION_COMPOSE -> { val accountKeys = accountsAdapter.selectedAccountKeys intent.putExtra(EXTRA_TEXT, ParseUtils.parseString(editText.text)) intent.putExtra(EXTRA_ACCOUNT_KEYS, accountKeys) if (accountKeys.isNotEmpty()) { val accountKey = accountKeys.first() intent.putExtra(EXTRA_NAME, DataStoreUtils.getAccountName(this, accountKey)) intent.putExtra(EXTRA_SCREEN_NAME, DataStoreUtils.getAccountScreenName(this, accountKey)) } inReplyToStatus?.let { intent.putExtra(EXTRA_IN_REPLY_TO_ID, it.id) intent.putExtra(EXTRA_IN_REPLY_TO_NAME, it.user_name) intent.putExtra(EXTRA_IN_REPLY_TO_SCREEN_NAME, it.user_screen_name) } startActivityForResult(intent, REQUEST_EXTENSION_COMPOSE) } else -> startActivity(intent) } } catch (e: ActivityNotFoundException) { Analyzer.logException(e) } } private fun addMedia(media: List) { mediaPreviewAdapter.addAll(media) updateMediaState() setMenu() } private fun removeMedia(list: List) { mediaPreviewAdapter.removeAll(list) } private fun clearMedia() { mediaPreviewAdapter.clear() updateMediaState() setMenu() } private fun displaySelectedAccountsIcon() { if (isFinishing) return val accounts = accountsAdapter.selectedAccounts val single = accounts.singleOrNull() val displayDoneIcon = isAccountSelectorVisible if (single != null) { accountsCount.setText(null) if (displayDoneIcon) { Glide.with(this).clear(accountProfileImage) accountProfileImage.setColorFilter(ThemeUtils.getColorFromAttribute(this, android.R.attr.colorForeground)) accountProfileImage.scaleType = ImageView.ScaleType.CENTER_INSIDE accountProfileImage.setImageResource(R.drawable.ic_action_confirm) } else { accountProfileImage.clearColorFilter() accountProfileImage.scaleType = ImageView.ScaleType.CENTER_CROP requestManager.loadProfileImage(this, single, accountProfileImage.style) .into(accountProfileImage) } accountProfileImage.setBorderColor(single.color) } else { accountsCount.setText(accounts.size.toString()) Glide.with(this).clear(accountProfileImage) if (displayDoneIcon) { accountProfileImage.setColorFilter(ThemeUtils.getColorFromAttribute(this, android.R.attr.colorForeground)) accountProfileImage.setImageResource(R.drawable.ic_action_confirm) accountProfileImage.scaleType = ImageView.ScaleType.CENTER_INSIDE } else { accountProfileImage.clearColorFilter() accountProfileImage.scaleType = ImageView.ScaleType.CENTER_CROP accountProfileImage.setImageDrawable(null) } accountProfileImage.setBorderColors(*IntArray(accounts.size) { accounts[it].color }) } if (displayDoneIcon) { accountsCount.visibility = View.GONE } else { accountsCount.visibility = View.VISIBLE } } private fun updateMediaState() { attachedMediaPreview.visibility = if (hasMedia) View.VISIBLE else View.GONE } private fun resetButtonsStates() { // Should be called first updateAccountSelectionState() updateLocationState() updateVisibilityState() updateSummaryTextState() updateUpdateStatusIcon() updateMediaState() setMenu() } // MARK: Begin intent handling private fun handleIntent(intent: Intent): Boolean { shouldSaveAccounts = false shouldSaveVisibility = false mentionUser = intent.getParcelableExtra(EXTRA_USER) inReplyToStatus = intent.getParcelableExtra(EXTRA_STATUS) when (intent.action) { INTENT_ACTION_REPLY -> { return handleReplyIntent(inReplyToStatus) } INTENT_ACTION_QUOTE -> { return handleQuoteIntent(inReplyToStatus) } INTENT_ACTION_EDIT_DRAFT -> { draft = intent.getParcelableExtra(EXTRA_DRAFT) return handleEditDraftIntent(draft) } INTENT_ACTION_MENTION -> { return handleMentionIntent(mentionUser) } INTENT_ACTION_REPLY_MULTIPLE -> { val screenNames: Array? = intent.getStringArrayExtra(EXTRA_SCREEN_NAMES) val accountKey: UserKey? = intent.getParcelableExtra(EXTRA_ACCOUNT_KEYS) val inReplyToStatus: ParcelableStatus? = intent.getParcelableExtra(EXTRA_IN_REPLY_TO_STATUS) return handleReplyMultipleIntent(screenNames, accountKey, inReplyToStatus) } INTENT_ACTION_COMPOSE_TAKE_PHOTO -> { requestOrTakePhoto() return true } INTENT_ACTION_COMPOSE_PICK_IMAGE -> { requestOrPickMedia() return true } } // Unknown action or no intent extras return false } @SuppressLint("SetTextI18n") private fun handleMentionIntent(user: ParcelableUser?): Boolean { if (user?.key == null) return false val accountKey = user.account_key ?: return false val accountScreenName = DataStoreUtils.getAccountScreenName(this, accountKey) if (TextUtils.isEmpty(accountScreenName)) return false editText.setText("@${user.acct} ") val selectionEnd = editText.length() editText.setSelection(selectionEnd) accountsAdapter.selectedAccountKeys = arrayOf(accountKey) return true } private fun handleQuoteIntent(status: ParcelableStatus?): Boolean { if (status == null) return false editText.setText(Utils.getQuoteStatus(this, status)) editText.setSelection(0) accountsAdapter.selectedAccountKeys = arrayOf(status.account_key) showQuoteLabelAndHint(status) return true } private fun handleReplyIntent(status: ParcelableStatus?): Boolean { if (status == null) return false val am = AccountManager.get(this) val statusAccount = AccountUtils.getAccountDetails(am, status.account_key, false) ?: return false val accountUser = statusAccount.user val mentions = ArrayList() val userAcct = status.user_acct if (accountUser.key != status.user_key) { editText.append("@$userAcct ") } var selectionStart = editText.length() if (status.is_retweet && !TextUtils.isEmpty(status.retweeted_by_user_screen_name)) { status.retweeted_by_user_acct?.addTo(mentions) } if (status.is_quote && !TextUtils.isEmpty(status.quoted_user_screen_name)) { status.quoted_user_acct?.addTo(mentions) } when (statusAccount.type) { AccountType.FANFOU -> { addFanfouHtmlToMentions(status.text_unescaped, status.spans, mentions) if (status.is_quote) { addFanfouHtmlToMentions(status.quoted_text_unescaped, status.quoted_spans, mentions) } } AccountType.MASTODON -> { addMastodonMentions(status.text_unescaped, status.spans, mentions) mentions.remove("${accountUser.screen_name}@${accountUser.key.host}") } else -> if (status.mentions.isNotNullOrEmpty()) { status.mentions.filterNot { it.key == status.account_key || it.screen_name.isNullOrEmpty() }.mapTo(mentions) { it.getAcct(statusAccount.key) } mentions.addAll(extractor.extractMentionedScreennames(status.quoted_text_plain)) } else { mentions.addAll(extractor.extractMentionedScreennames(status.text_plain)) if (status.is_quote) { mentions.addAll(extractor.extractMentionedScreennames(status.quoted_text_plain)) } } } mentions.distinctBy { it.toLowerCase(Locale.US) }.filterNot { return@filterNot it.equals(userAcct, ignoreCase = true) }.forEach { editText.append("@$it ") } // For non-Twitter instances, put current user mention at last if (statusAccount.type != AccountType.TWITTER && accountUser.key == status.user_key) { selectionStart = editText.length() editText.append("@$userAcct ") } val text = intent.getStringExtra(EXTRA_TEXT) if (text != null) { editText.append(text) } else { val selectionEnd = editText.length() editText.setSelection(selectionStart, selectionEnd) } editSummary.string = status.extras?.summary_text editSummaryEnabled = !editSummary.empty statusVisibility = intent.getStringExtra(EXTRA_VISIBILITY) ?: status.extras?.visibility possiblySensitive = intent.getBooleanExtra(EXTRA_IS_POSSIBLY_SENSITIVE, statusAccount.type == AccountType.MASTODON && status.is_possibly_sensitive) accountsAdapter.selectedAccountKeys = arrayOf(status.account_key) showReplyLabelAndHint(status) editText.requestFocus() return true } private fun handleEditDraftIntent(draft: Draft?): Boolean { if (draft == null) return false val extras = draft.action_extras as? UpdateStatusActionExtras val media = draft.media draftUniqueId = draft.unique_id_non_null recentLocation = draft.location accountsAdapter.selectedAccountKeys = draft.account_keys ?: emptyArray() if (media != null) { addMedia(Arrays.asList(*media)) } if (extras != null) { if (extras.summaryText?.isNotEmpty() == true) { editSummaryEnabled = true } editSummary.setText(extras.summaryText) statusVisibility = extras.visibility possiblySensitive = extras.isPossiblySensitive inReplyToStatus = extras.inReplyToStatus editText.setText(extras.editingText ?: draft.text) } else { editText.setText(draft.text) } editText.setSelection(editText.length()) val tag = Uri.withAppendedPath(Drafts.CONTENT_URI, draft._id.toString()).toString() notificationManager.cancel(tag, NOTIFICATION_ID_DRAFTS) return true } private fun handleDefaultIntent(intent: Intent?): Boolean { if (intent == null) return false val action = intent.action val hasVisibility = intent.hasExtra(EXTRA_VISIBILITY) val hasAccountKeys: Boolean = when { intent.hasExtra(EXTRA_ACCOUNT_KEYS) -> { val accountKeys = intent.getTypedArrayExtra(EXTRA_ACCOUNT_KEYS) accountsAdapter.selectedAccountKeys = accountKeys true } intent.hasExtra(EXTRA_ACCOUNT_KEY) -> { intent.getParcelableExtra(EXTRA_ACCOUNT_KEY)?.let { accountsAdapter.selectedAccountKeys = arrayOf(it) true } ?: false } else -> { false } } when (action) { Intent.ACTION_SEND, Intent.ACTION_SEND_MULTIPLE -> { shouldSaveAccounts = false shouldSaveVisibility = false val stream = intent.getStreamExtra() if (stream != null) { val src = stream.toTypedArray() TaskStarter.execute(AddMediaTask(this, src, null, copySrc = true, deleteSrc = false )) } } else -> { shouldSaveAccounts = !hasAccountKeys shouldSaveVisibility = !hasVisibility val data = intent.data if (data != null) { val src = arrayOf(data) TaskStarter.execute(AddMediaTask(this, src, null, copySrc = true, deleteSrc = false )) } } } val extraSubject = intent.getCharSequenceExtra(Intent.EXTRA_SUBJECT) val extraText = intent.getCharSequenceExtra(Intent.EXTRA_TEXT) editText.charSequence = when { extraSubject != null && extraText != null -> "$extraSubject - $extraText" extraSubject != null -> extraSubject else -> extraText } val selection = intent.getIntExtra(EXTRA_SELECTION, -1) if (selection < 0) { editText.setSelection(editText.length()) } else { editText.setSelection(selection.coerceIn(0..editText.length())) } if (intent.hasExtra(Intent.EXTRA_PROCESS_TEXT) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){ val charSequences = intent.getCharSequenceExtra(Intent.EXTRA_PROCESS_TEXT) charSequences?.let { editText.setText(it.toString()) } } editText.requestFocus() return true } // MARK: End intent handling // MARK: Begin label and hint handling private fun showLabelAndHint(intent: Intent): Boolean { when (intent.action) { INTENT_ACTION_REPLY -> { return showReplyLabelAndHint(intent.getParcelableExtra(EXTRA_STATUS)) } INTENT_ACTION_QUOTE -> { return showQuoteLabelAndHint(intent.getParcelableExtra(EXTRA_STATUS)) } INTENT_ACTION_EDIT_DRAFT -> { val draft: Draft? = intent.getParcelableExtra(EXTRA_DRAFT) return when (draft?.action_type) { Draft.Action.REPLY -> { showReplyLabelAndHint((draft.action_extras as? UpdateStatusActionExtras)?.inReplyToStatus) } Draft.Action.QUOTE -> { showQuoteLabelAndHint((draft.action_extras as? UpdateStatusActionExtras)?.inReplyToStatus) } else -> { showDefaultLabelAndHint() false } } } else -> { showDefaultLabelAndHint() return false } } } private fun showQuoteLabelAndHint(status: ParcelableStatus?): Boolean { if (status == null) { showDefaultLabelAndHint() return false } val replyToName = userColorNameManager.getDisplayName(status, nameFirst) replyLabel.spannable = getString(R.string.label_quote_name_text, replyToName, status.text_unescaped) replyLabel.visibility = View.VISIBLE editText.hint = getString(R.string.label_quote_name, replyToName) return true } private fun showReplyLabelAndHint(status: ParcelableStatus?): Boolean { if (status == null) { showDefaultLabelAndHint() return false } val replyToName = userColorNameManager.getDisplayName(status, nameFirst) replyLabel.spannable = getString(R.string.label_reply_name_text, replyToName, status.text_unescaped) replyLabel.visibility = View.VISIBLE editText.hint = getString(R.string.label_reply_name, replyToName) return true } private fun showDefaultLabelAndHint() { replyLabel.visibility = View.GONE editText.setHint(R.string.label_status_hint) } // MARK: End label and hint handling private fun addFanfouHtmlToMentions(text: String, spans: Array?, mentions: MutableCollection) { if (spans == null) return for (span in spans) { val start = span.start val end = span.end if (start <= 0 || end > text.length || start > end) continue val ch = text[start - 1] if (ch == '@' || ch == '\uff20') { mentions.add(text.substring(start, end)) } } } private fun addMastodonMentions(text: String, spans: Array?, mentions: MutableCollection) { if (spans != null && spans.isNotEmpty()) { spans.filter { if (it.type != SpanItem.SpanType.ACCT_MENTION) return@filter false return@filter true }.mapTo(mentions) { it.link } } else { mentions.addAll(extractor.extractMentionedScreennames(text)) } } private fun handleReplyMultipleIntent(screenNames: Array?, accountKey: UserKey?, inReplyToStatus: ParcelableStatus?): Boolean { if (screenNames == null || screenNames.isEmpty() || accountKey == null || inReplyToStatus == null) return false val myScreenName = DataStoreUtils.getAccountScreenName(this, accountKey) ?: return false screenNames.filterNot { it.equals(myScreenName, ignoreCase = true) } .forEach { editText.append("@$it ") } editText.setSelection(editText.length()) accountsAdapter.selectedAccountKeys = arrayOf(accountKey) this.inReplyToStatus = inReplyToStatus return true } private fun updateAccountSelectionState() { displaySelectedAccountsIcon() val accounts = accountsAdapter.selectedAccounts editText.account = accounts.firstOrNull() statusTextCount.maxLength = accounts.textLimit val singleAccount = accounts.singleOrNull() val allMastodon = accounts.isNotEmpty() && accounts.all { it.type == AccountType.MASTODON } val anyMastodon = accounts.any { it.type == AccountType.MASTODON } hasEditSummary = anyMastodon hasStatusVisibility = allMastodon hasAttachmentStatusVisibility = anyMastodon && !allMastodon hasLocationOption = !allMastodon ignoreMentions = singleAccount?.type == AccountType.TWITTER replyToSelf = singleAccount?.let { it.key == inReplyToStatus?.user_key } ?: false } private fun requestOrTakePhoto() { if (checkAnySelfPermissionsGranted(AndroidPermission.WRITE_EXTERNAL_STORAGE)) { takePhoto() return } ActivityCompat.requestPermissions(this, arrayOf(AndroidPermission.WRITE_EXTERNAL_STORAGE), REQUEST_TAKE_PHOTO_PERMISSION) } private fun requestOrCaptureVideo() { if (checkAnySelfPermissionsGranted(AndroidPermission.WRITE_EXTERNAL_STORAGE)) { captureVideo() return } ActivityCompat.requestPermissions(this, arrayOf(AndroidPermission.WRITE_EXTERNAL_STORAGE), REQUEST_CAPTURE_VIDEO_PERMISSION) } private fun requestOrPickMedia() { if (checkAnySelfPermissionsGranted(AndroidPermission.WRITE_EXTERNAL_STORAGE)) { pickMedia() return } ActivityCompat.requestPermissions(this, arrayOf(AndroidPermission.WRITE_EXTERNAL_STORAGE), REQUEST_PICK_MEDIA_PERMISSION) } private fun takePhoto(): Boolean { val builder = ThemedMediaPickerActivity.withThemed(this) builder.takePhoto() startActivityForResult(builder.build(), REQUEST_TAKE_PHOTO) return true } private fun captureVideo(): Boolean { val builder = ThemedMediaPickerActivity.withThemed(this) builder.captureVideo() startActivityForResult(builder.build(), REQUEST_TAKE_PHOTO) return true } private fun pickMedia(): Boolean { val intent = ThemedMediaPickerActivity.withThemed(this) .pickMedia() .containsVideo(true) .videoOnly(false) .allowMultiple(true) .build() startActivityForResult(intent, REQUEST_PICK_MEDIA) return true } private fun saveAccountSelection() { if (!shouldSaveAccounts) return preferences[composeAccountsKey] = accountsAdapter.selectedAccountKeys } private fun saveVisibility() { if (!shouldSaveVisibility) return preferences[composeStatusVisibilityKey] = statusVisibility } private fun setMenu() { if (menuBar == null) return val menu = menuBar.menu val hasMedia = this.hasMedia menu.setItemAvailability(R.id.edit_summary, hasEditSummary) menu.setItemAvailability(R.id.schedule, extraFeaturesService.isSupported( ExtraFeaturesService.FEATURE_SCHEDULE_STATUS)) menu.setItemAvailability(R.id.add_gif, extraFeaturesService.isSupported( ExtraFeaturesService.FEATURE_SHARE_GIF)) menu.setGroupAvailability(MENU_GROUP_IMAGE_EXTENSION, hasMedia) menu.setItemChecked(R.id.toggle_sensitive, possiblySensitive) val attachLocation = kPreferences[attachLocationKey] val attachPreciseLocation = kPreferences[attachPreciseLocationKey] if (!attachLocation) { menu.setItemChecked(R.id.location_off, true) menu.setItemIcon(R.id.location_submenu, R.drawable.ic_action_location_off) } else if (attachPreciseLocation) { menu.setItemChecked(R.id.location_precise, true) menu.setItemIcon(R.id.location_submenu, R.drawable.ic_action_location) } else { menu.setItemChecked(R.id.location_coarse, true) menu.setItemIcon(R.id.location_submenu, R.drawable.ic_action_location) } when (statusVisibility) { StatusVisibility.UNLISTED -> { menu.setItemChecked(R.id.visibility_unlisted, true) menu.setItemIcon(R.id.visibility_submenu, R.drawable.ic_action_web_lock) menu.setItemChecked(R.id.attachment_visibility_unlisted, true) menu.setItemIcon(R.id.attachment_visibility_submenu, R.drawable.ic_action_web_lock) } StatusVisibility.PRIVATE -> { menu.setItemChecked(R.id.visibility_private, true) menu.setItemIcon(R.id.visibility_submenu, R.drawable.ic_action_lock) menu.setItemChecked(R.id.attachment_visibility_private, true) menu.setItemIcon(R.id.attachment_visibility_submenu, R.drawable.ic_action_lock) } StatusVisibility.DIRECT -> { menu.setItemChecked(R.id.visibility_direct, true) menu.setItemIcon(R.id.visibility_submenu, R.drawable.ic_action_message) menu.setItemChecked(R.id.attachment_visibility_direct, true) menu.setItemIcon(R.id.attachment_visibility_submenu, R.drawable.ic_action_message) } else -> { // Default to public menu.setItemChecked(R.id.visibility_public, true) menu.setItemIcon(R.id.visibility_submenu, R.drawable.ic_action_web) menu.setItemChecked(R.id.attachment_visibility_public, true) menu.setItemIcon(R.id.attachment_visibility_submenu, R.drawable.ic_action_web) } } menu.setItemAvailability(R.id.visibility_submenu, hasStatusVisibility) menu.setItemAvailability(R.id.attachment_visibility_submenu, hasAttachmentStatusVisibility) menu.setItemAvailability(R.id.location_submenu, hasLocationOption) ThemeUtils.wrapMenuIcon(menuBar, excludeGroups = *intArrayOf(MENU_GROUP_IMAGE_EXTENSION)) ThemeUtils.resetCheatSheet(menuBar) } private fun setProgressVisible(visible: Boolean) { if (isFinishing) return composeProgress.visibility = if (visible) View.VISIBLE else View.GONE } private fun setRecentLocation(location: ParcelableLocation?) { if (location != null) { val attachPreciseLocation = kPreferences[attachPreciseLocationKey] if (attachPreciseLocation) { locationLabel.spannable = ParcelableLocationUtils.getHumanReadableString(location, 3) } else { if (locationLabel.tag == null || location != recentLocation) { val task = DisplayPlaceNameTask() task.params = location task.callback = this TaskStarter.execute(task) } } } else { locationLabel.setText(R.string.unknown_location) } recentLocation = location } /** * The Location Manager manages location providers. This code searches for * the best provider of data (GPS, WiFi/cell phone tower lookup, some other * mechanism) and finds the last known location. */ @Throws(SecurityException::class) private fun startLocationUpdateIfEnabled(): Boolean { if (locationListener != null) return true val attachLocation = kPreferences[attachLocationKey] if (!attachLocation) { return false } val attachPreciseLocation = kPreferences[attachPreciseLocationKey] val criteria = Criteria() if (attachPreciseLocation) { criteria.accuracy = Criteria.ACCURACY_FINE } else { criteria.accuracy = Criteria.ACCURACY_COARSE } val provider = locationManager.getBestProvider(criteria, true) if (provider != null) { locationLabel.setText(R.string.getting_location) locationListener = ComposeLocationListener(this).also { locationManager.requestLocationUpdates(provider, 0, 0f, it) } val location = locationManager.getCachedLocation() if (location != null) { locationListener?.onLocationChanged(location) } } else { Toast.makeText(this, R.string.message_toast_cannot_get_location, Toast.LENGTH_SHORT).show() } return provider != null } private fun requestOrUpdateLocation() { if (checkAnySelfPermissionsGranted(AndroidPermission.ACCESS_COARSE_LOCATION, AndroidPermission.ACCESS_FINE_LOCATION)) { try { startLocationUpdateIfEnabled() } catch (e: SecurityException) { Toast.makeText(this, R.string.message_toast_cannot_get_location, Toast.LENGTH_SHORT).show() } } else { val permissions = arrayOf(AndroidPermission.ACCESS_COARSE_LOCATION, AndroidPermission.ACCESS_FINE_LOCATION) PermissionRequestDialog.show(supportFragmentManager, getString(R.string.message_permission_request_compose_location), permissions, REQUEST_ATTACH_LOCATION_PERMISSION) } } private fun hasComposingStatus(): Boolean { if (intent.action == INTENT_ACTION_EDIT_DRAFT) return true if (hasMedia) return true val text = editText.text?.toString().orEmpty() if (text == originalText) return false val replyTextAndMentions = getTwitterReplyTextAndMentions(text) if (replyTextAndMentions != null) { return replyTextAndMentions.replyText.isNotEmpty() } return text.isNotEmpty() } private fun confirmAndUpdateStatus() { val matchResult = Regex("[DM] +([a-z0-9_]{1,20}) +[^ ]+").matchEntire(editText.text) if (matchResult != null) { val screenName = matchResult.groupValues[1] val df = DirectMessageConfirmFragment() df.arguments = Bundle { this[EXTRA_SCREEN_NAME] = screenName } df.show(supportFragmentManager, "send_direct_message_confirm") } else if (isQuotingProtectedStatus) { val df = RetweetProtectedStatusWarnFragment() df.show(supportFragmentManager, "retweet_protected_status_warning_message") } else if (scheduleInfo != null && !extraFeaturesService.isEnabled(ExtraFeaturesService.FEATURE_SCHEDULE_STATUS)) { ExtraFeaturesIntroductionDialogFragment.show(supportFragmentManager, feature = ExtraFeaturesService.FEATURE_SCHEDULE_STATUS, requestCode = REQUEST_PURCHASE_EXTRA_FEATURES) } else { updateStatus() } } private fun updateStatus() { if (isFinishing || editText == null) return val update = try { getStatusUpdate(true) } catch (e: NoAccountException) { editText.error = getString(R.string.message_toast_no_account_selected) editText.requestFocus() return } catch (e: NoContentException) { editText.error = getString(R.string.error_message_no_content) editText.requestFocus() return } catch (e: StatusTooLongException) { editText.error = getString(R.string.error_message_status_too_long) editSummary.string = e.summary editText.string = e.text if (e.textExceededStartIndex >= 0) { editText.setSelection(e.textExceededStartIndex, editText.length()) editText.requestFocus() } else if (e.summaryExceededStartIndex >= 0) { editSummary.setSelection(e.summaryExceededStartIndex, editSummary.length()) editSummary.requestFocus() } return } LengthyOperationsService.updateStatusesAsync(this, update.draft_action, statuses = *arrayOf(update), scheduleInfo = scheduleInfo) finishComposing() } private fun finishComposing() { if (preferences[noCloseAfterTweetSentKey] && inReplyToStatus == null) { possiblySensitive = false shouldSaveAccounts = true shouldSaveVisibility = true inReplyToStatus = null mentionUser = null draft = null originalText = null editText.text = null clearMedia() val intent = Intent(INTENT_ACTION_COMPOSE) setIntent(intent) handleIntent(intent) showLabelAndHint(intent) setMenu() updateTextCount() shouldSkipDraft = false } else { setResult(Activity.RESULT_OK) shouldSkipDraft = true finish() } } private fun discardTweet() { val context = applicationContext val media = mediaList task { media.forEach { Utils.deleteMedia(context, Uri.parse(it.uri)) } } } private fun getStatusUpdate(checkLength: Boolean): ParcelableStatusUpdate { val accounts = accountsAdapter.selectedAccounts if (accounts.isEmpty()) throw NoAccountException() val update = ParcelableStatusUpdate() val media = this.media val summary = editSummary.textIfVisible?.normalized(Normalizer.Form.NFC) val text = editText.text?.normalized(Normalizer.Form.NFC).orEmpty() val maxLength = statusTextCount.maxLength val inReplyTo = inReplyToStatus val replyTextAndMentions = getTwitterReplyTextAndMentions(text, accounts) if (inReplyTo != null && replyTextAndMentions != null) { val (replyStartIndex, replyText, _, excludedMentions, replyToOriginalUser) = replyTextAndMentions if (replyText.isEmpty() && media.isEmpty()) throw NoContentException() val totalLength = StatusTextValidator.calculateLength(accounts, summary, replyText) if (checkLength && !statusShortenerUsed && maxLength > 0 && totalLength > maxLength) { val summaryLength = StatusTextValidator.calculateSummaryLength(accounts, summary) if (summary != null && summaryLength > maxLength) { throw StatusTooLongException(summary.offsetByCodePoints(0, maxLength), if (text.isEmpty()) -1 else 0, summary, text) } throw StatusTooLongException(-1, replyStartIndex + replyText.offsetByCodePoints(0, maxLength - summaryLength), summary, text) } update.text = replyText update.extended_reply_mode = true update.excluded_reply_user_ids = excludedMentions.mapToArray { it.key.id } val replyToSelf = accounts.singleOrNull()?.key == inReplyTo.user_key // Fix status to at least make mentioned user know what status it is if (!replyToOriginalUser && !replyToSelf) { update.attachment_url = LinkCreator.getStatusWebLink(inReplyTo).toString() } } else { if (text.isEmpty() && media.isEmpty()) throw NoContentException() val totalLength = StatusTextValidator.calculateLength(accounts, summary, text) if (checkLength && !statusShortenerUsed && maxLength > 0 && totalLength > maxLength) { val summaryLength = StatusTextValidator.calculateSummaryLength(accounts, summary) if (summary != null && summaryLength > maxLength) { throw StatusTooLongException(summary.offsetByCodePoints(0, maxLength), if (text.isEmpty()) -1 else 0, summary, text) } throw StatusTooLongException(-1, text.offsetByCodePoints(0, maxLength - summaryLength), summary, text) } update.text = text update.extended_reply_mode = false } update.summary = summary val attachLocation = kPreferences[attachLocationKey] val attachPreciseLocation = kPreferences[attachPreciseLocationKey] update.draft_action = draftAction update.accounts = accounts if (attachLocation) { update.location = recentLocation update.display_coordinates = attachPreciseLocation } update.media = media update.in_reply_to_status = inReplyTo update.is_possibly_sensitive = possiblySensitive update.visibility = statusVisibility ?: StatusVisibility.PUBLIC update.draft_extras = update.updateStatusActionExtras().also { it.editingText = text } return update } private fun updateTextCount() { val editable = editText.editableText ?: return val summary = editSummary.textIfVisible?.toString() val accounts = accountsAdapter.selectedAccounts val text = editable.toString() val textAndMentions = getTwitterReplyTextAndMentions(text) if (textAndMentions == null) { hintLabel.visibility = View.GONE editable.clearSpans(MentionColorSpan::class.java) statusTextCount.textCount = StatusTextValidator.calculateLength(accounts, summary, text, false, null) } else if (textAndMentions.replyToOriginalUser || replyToSelf) { hintLabel.visibility = View.GONE val mentionColor = ThemeUtils.getTextColorSecondary(this) editable.clearSpans(MentionColorSpan::class.java) editable.setSpan(MentionColorSpan(mentionColor), 0, textAndMentions.replyStartIndex, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) statusTextCount.textCount = StatusTextValidator.calculateLength(accounts, summary, textAndMentions.replyText, false, null) } else { hintLabel.visibility = View.VISIBLE editable.clearSpans(MentionColorSpan::class.java) statusTextCount.textCount = StatusTextValidator.calculateLength(accounts, summary, textAndMentions.replyText, false, null) } } private fun updateUpdateStatusIcon() { if (scheduleInfo != null) { updateStatusIcon.setImageResource(R.drawable.ic_action_time) } else { updateStatusIcon.setImageResource(R.drawable.ic_action_send) } } private fun updateLocationState() { if (kPreferences[attachLocationKey]) { locationLabel.visibility = View.VISIBLE if (recentLocation != null) { setRecentLocation(recentLocation) } else { locationLabel.setText(R.string.getting_location) } } else { locationLabel.visibility = View.GONE } } private fun updateVisibilityState() { val hasVisibility = hasStatusVisibility || hasAttachmentStatusVisibility visibilityLabel.visibility = if (hasVisibility) View.VISIBLE else View.GONE when (statusVisibility) { StatusVisibility.UNLISTED -> { visibilityLabel.setText(R.string.label_status_visibility_unlisted) TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(visibilityLabel, R.drawable.ic_action_web_lock, 0, 0, 0) } StatusVisibility.PRIVATE -> { visibilityLabel.setText(R.string.label_status_visibility_private) TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(visibilityLabel, R.drawable.ic_action_lock, 0, 0, 0) } StatusVisibility.DIRECT -> { visibilityLabel.setText(R.string.label_status_visibility_direct) TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(visibilityLabel, R.drawable.ic_action_message, 0, 0, 0) } else -> { // Default to public visibilityLabel.setText(R.string.label_status_visibility_public) TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(visibilityLabel, R.drawable.ic_action_web, 0, 0, 0) } } visibilityLabel.refreshDrawableState() } private fun updateSummaryTextState() { editSummary.visibility = if (editSummaryEnabled && hasEditSummary) View.VISIBLE else View.GONE } private fun getTwitterReplyTextAndMentions(text: String = editText.text?.toString().orEmpty(), accounts: Array = accountsAdapter.selectedAccounts): ReplyTextAndMentions? { val inReplyTo = inReplyToStatus ?: return null if (!ignoreMentions) return null val account = accounts.singleOrNull() ?: return null return extractor.extractReplyTextAndMentions(text, inReplyTo, account.key) } private fun saveToDrafts(): Uri? { val statusUpdate = try { getStatusUpdate(false) } catch (e: ComposeException) { return null } val draft = UpdateStatusTask.createDraft(draftAction) { applyUpdateStatus(statusUpdate) } val values = ObjectCursor.valuesCreatorFrom(Draft::class.java).create(draft) val draftUri = contentResolver.insert(Drafts.CONTENT_URI, values)!! displayNewDraftNotification(draftUri) return draftUri } private fun displayNewDraftNotification(draftUri: Uri) { val notificationUri = Drafts.CONTENT_URI_NOTIFICATIONS.withAppendedPath(draftUri.lastPathSegment!!) contentResolver.insert(notificationUri, null) } private fun ViewAnimator.setupViews() { fun AnimatorSet.setup() { interpolator = DecelerateInterpolator() duration = 250 } addView(accountSelector) { view -> inAnimator = AnimatorSet().also { set -> set.playTogether( ObjectAnimator.ofFloat(view, ViewProperties.TRANSLATION_X_RELATIVE, -1f, 0f), ObjectAnimator.ofFloat(view, View.ALPHA, 0f, 1f) ) set.setup() } outAnimator = AnimatorSet().also { set -> set.playTogether( ObjectAnimator.ofFloat(view, ViewProperties.TRANSLATION_X_RELATIVE, 0f, -1f), ObjectAnimator.ofFloat(view, View.ALPHA, 1f, 0f) ) set.setup() } } addView(composeMenu) { view -> inAnimator = AnimatorSet().also { set -> set.playTogether( ObjectAnimator.ofFloat(view, ViewProperties.TRANSLATION_X_RELATIVE, 1f, 0f), ObjectAnimator.ofFloat(view, View.ALPHA, 0f, 1f) ) set.setup() } outAnimator = AnimatorSet().also { set -> set.playTogether( ObjectAnimator.ofFloat(view, ViewProperties.TRANSLATION_X_RELATIVE, 0f, 1f), ObjectAnimator.ofFloat(view, View.ALPHA, 1f, 0f) ) set.setup() } } } private fun updateViewStyle() { accountProfileImage.style = preferences[profileImageStyleKey] } private fun setupEditText() { val sendByEnter = preferences[quickSendKey] EditTextEnterHandler.attach(editText, ComposeEnterListener(this), sendByEnter) editSummary.addTextChangedListener(object : SimpleTextWatcher { override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { setMenu() updateTextCount() } }) editText.addTextChangedListener(object : SimpleTextWatcher { override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { setMenu() updateTextCount() if (s is Spannable && count == 1 && before == 0) { val imageSpans = s.getSpans(start, start + count, ImageSpan::class.java) val imageSources = ArrayList() for (imageSpan in imageSpans) { imageSources.add(imageSpan.source!!) s.setSpan(MarkForDeleteSpan(), start, start + count, Spanned.SPAN_INCLUSIVE_INCLUSIVE) } if (imageSources.isNotEmpty()) { val intent = ThemedMediaPickerActivity.withThemed(this@ComposeActivity) .getMedia(Uri.parse(imageSources[0])) .build() startActivityForResult(intent, REQUEST_PICK_MEDIA) } } } override fun afterTextChanged(s: Editable) { textChanged = s.isEmpty() val deletes = s.getSpans(0, s.length, MarkForDeleteSpan::class.java) for (delete in deletes) { s.delete(s.getSpanStart(delete), s.getSpanEnd(delete)) s.removeSpan(delete) } for (span in s.getSpans(0, s.length, UpdateAppearance::class.java)) { trimSpans(s, span) } } private fun trimSpans(s: Editable, span: Any) { if (span is EmojiSpan) return if (span is SuggestionSpan) return if (span is MetricAffectingSpan) { s.removeSpan(span) } } }) editText.customSelectionActionModeCallback = this editText.imageInputListener = { contentInfo -> val task = AddMediaTask(this, arrayOf(contentInfo.contentUri), null, copySrc = true, deleteSrc = false ) task.callback = { contentInfo.releasePermission() } TaskStarter.execute(task) } editTextContainer.touchDelegate = ComposeEditTextTouchDelegate(editTextContainer, editText) } class RetweetProtectedStatusWarnFragment : BaseDialogFragment(), DialogInterface.OnClickListener { override fun onClick(dialog: DialogInterface, which: Int) { val activity = activity when (which) { DialogInterface.BUTTON_POSITIVE -> { if (activity is ComposeActivity) { activity.updateStatus() } } } } override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { val context = requireActivity() val builder = AlertDialog.Builder(requireContext()) builder.setMessage(R.string.quote_protected_status_warning_message) builder.setPositiveButton(R.string.send_anyway, this) builder.setNegativeButton(android.R.string.cancel, null) val dialog = builder.create() dialog.applyOnShow { applyTheme() } return dialog } } class DirectMessageConfirmFragment : BaseDialogFragment(), DialogInterface.OnClickListener { private val screenName: String get() = arguments?.getString(EXTRA_SCREEN_NAME).orEmpty() override fun onClick(dialog: DialogInterface, which: Int) { val activity = activity when (which) { DialogInterface.BUTTON_POSITIVE -> { if (activity is ComposeActivity) { activity.updateStatus() } } DialogInterface.BUTTON_NEUTRAL -> { if (activity is ComposeActivity) { // Insert a ZWSP into status text activity.editText.text.insert(1, "\u200b") activity.updateStatus() } } } } override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { val context = requireActivity() val builder = AlertDialog.Builder(requireContext()) builder.setMessage(getString(R.string.message_format_compose_message_convert_to_status, "@$screenName")) builder.setPositiveButton(R.string.action_send, this) builder.setNeutralButton(R.string.action_compose_message_convert_to_status, this) builder.setNegativeButton(android.R.string.cancel, null) val dialog = builder.create() dialog.applyOnShow { applyTheme() } return dialog } } class AttachedMediaItemTouchHelperCallback(adapter: ItemTouchHelperAdapter) : SimpleItemTouchHelperCallback(adapter) { override fun isLongPressDragEnabled(): Boolean { return true } override fun isItemViewSwipeEnabled(): Boolean { return false } override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: ViewHolder): Int { // Set movement flags based on the layout manager val dragFlags = ItemTouchHelper.START or ItemTouchHelper.END val swipeFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN return ItemTouchHelper.Callback.makeMovementFlags(dragFlags, swipeFlags) } override fun onChildDraw(c: Canvas, recyclerView: RecyclerView, viewHolder: ViewHolder, dX: Float, dY: Float, actionState: Int, isCurrentlyActive: Boolean) { if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) { // Fade out the view as it is swiped out of the parent's bounds val alpha = ALPHA_FULL - abs(dY) / viewHolder.itemView.height.toFloat() viewHolder.itemView.alpha = alpha super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive) } else { super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive) } } override fun getSwipeThreshold(viewHolder: ViewHolder): Float { return 0.75f } override fun clearView(recyclerView: RecyclerView, viewHolder: ViewHolder) { super.clearView(recyclerView, viewHolder) viewHolder.itemView.alpha = ALPHA_FULL } companion object { const val ALPHA_FULL = 1.0f } } private class ComposeLocationListener(activity: ComposeActivity) : LocationListener { private val activityRef = WeakReference(activity) override fun onLocationChanged(location: Location) { val activity = activityRef.get() ?: return activity.setRecentLocation(ParcelableLocationUtils.fromLocation(location)) } override fun onStatusChanged(provider: String, status: Int, extras: Bundle) { } override fun onProviderEnabled(provider: String) { } override fun onProviderDisabled(provider: String) { } } private class AccountIconViewHolder(val adapter: AccountIconsAdapter, itemView: View) : ViewHolder(itemView) { private val iconView = itemView.findViewById(android.R.id.icon) init { itemView.setOnClickListener { (itemView as CheckableLinearLayout).toggle() adapter.toggleSelection(layoutPosition) } itemView.setOnLongClickListener { (itemView as CheckableLinearLayout).toggle() adapter.setSelection(layoutPosition) return@setOnLongClickListener true } iconView.style = adapter.profileImageStyle } fun showAccount(adapter: AccountIconsAdapter, account: AccountDetails, isSelected: Boolean) { itemView.alpha = if (isSelected) 1f else 0.33f val context = adapter.context adapter.requestManager.loadProfileImage(context, account, adapter.profileImageStyle).into(iconView) iconView.setBorderColor(account.color) } } private class AccountIconsAdapter( private val activity: ComposeActivity ) : BaseRecyclerViewAdapter(activity, activity.requestManager) { private val inflater: LayoutInflater = activity.layoutInflater private val selection: MutableMap = HashMap() private var accounts: Array? = null init { setHasStableIds(true) } var selectedAccountKeys: Array get() { val accounts = accounts ?: return emptyArray() return accounts.filter { selection[it.key] ?: false } .map { it.key } .toTypedArray() } set(value) { selection.clear() for (accountKey in value) { selection[accountKey] = true } notifyDataSetChanged() } val selectedAccounts: Array get() { val accounts = accounts ?: return emptyArray() return accounts.filter { selection[it.key] ?: false }.toTypedArray() } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AccountIconViewHolder { val view = inflater.inflate(R.layout.adapter_item_compose_account, parent, false) return AccountIconViewHolder(this, view) } override fun onBindViewHolder(holder: AccountIconViewHolder, position: Int) { val account = accounts!![position] val isSelected = selection[account.key] ?: false holder.showAccount(this, account, isSelected) } override fun getItemCount(): Int { return if (accounts != null) accounts!!.size else 0 } override fun getItemId(position: Int): Long { return accounts!![position].hashCode().toLong() } fun setAccounts(accounts: Array) { this.accounts = accounts notifyDataSetChanged() } fun toggleSelection(position: Int) { if (accounts == null || position < 0) return val account = accounts!![position] selection[account.key] = true != selection[account.key] activity.updateAccountSelectionState() activity.updateVisibilityState() activity.updateSummaryTextState() activity.setMenu() notifyDataSetChanged() } fun setSelection(position: Int) { if (accounts == null || position < 0) return val account = accounts!![position] selection.clear() selection[account.key] = true != selection[account.key] activity.updateAccountSelectionState() activity.updateVisibilityState() activity.updateSummaryTextState() activity.setMenu() notifyDataSetChanged() } } private class AddMediaTask(activity: ComposeActivity, sources: Array, types: IntArray?, copySrc: Boolean, deleteSrc: Boolean) : AbsAddMediaTask<((List?) -> Unit)?>( activity, sources, types, copySrc, deleteSrc) { override fun afterExecute(callback: ((List?) -> Unit)?, result: List?) { callback?.invoke(result) val activity = context as? ComposeActivity ?: return activity.setProgressVisible(false) if (result != null) { activity.addMedia(result) } activity.setMenu() activity.updateTextCount() } override fun beforeExecute() { val activity = context as? ComposeActivity ?: return activity.setProgressVisible(true) } } private class DeleteMediaTask( activity: ComposeActivity, media: Array ) : AbsDeleteMediaTask<((BooleanArray) -> Unit)?>(activity, media.mapToArray { Uri.parse(it.uri) }) { override fun beforeExecute() { val activity = context as? ComposeActivity ?: return activity.setProgressVisible(true) } override fun afterExecute(callback: ((BooleanArray) -> Unit)?, result: BooleanArray) { callback?.invoke(result) } } private class DisplayPlaceNameTask : AbstractTask, ComposeActivity>() { override fun doLongOperation(location: ParcelableLocation): List
? { return try { val activity = callback ?: throw IOException("Interrupted") val gcd = Geocoder(activity, Locale.getDefault()) gcd.getFromLocation(location.latitude, location.longitude, 1) } catch (e: IOException) { null } } override fun beforeExecute() { val location = params val activity = callback ?: return val textView = activity.locationLabel ?: return val preferences = activity.preferences val attachLocation = preferences[attachLocationKey] val attachPreciseLocation = preferences[attachPreciseLocationKey] if (attachLocation) { if (attachPreciseLocation) { textView.spannable = ParcelableLocationUtils.getHumanReadableString(location, 3) textView.tag = location } else { when (val tag = textView.tag) { is Address -> { textView.spannable = tag.locality } is NoAddress -> { textView.setText(R.string.label_location_your_coarse_location) } else -> { textView.setText(R.string.getting_location) } } } } else { textView.setText(R.string.no_location) } } override fun afterExecute(activity: ComposeActivity?, addresses: List
?) { if (activity == null) return val textView = activity.locationLabel ?: return val preferences = activity.preferences val attachLocation = preferences[attachLocationKey] val attachPreciseLocation = preferences[attachPreciseLocationKey] if (attachLocation) { if (attachPreciseLocation) { val location = params textView.spannable = ParcelableLocationUtils.getHumanReadableString(location, 3) textView.tag = location } else if (addresses == null || addresses.isEmpty()) { val tag = textView.tag if (tag is Address) { textView.spannable = tag.locality } else { textView.setText(R.string.label_location_your_coarse_location) textView.tag = NoAddress() } } else { val address = addresses[0] textView.spannable = address.locality textView.tag = address } } else { textView.setText(R.string.no_location) } } internal class NoAddress } private class ComposeEnterListener(private val activity: ComposeActivity?) : EnterListener { override fun shouldCallListener(): Boolean { return activity != null && activity.composeKeyMetaState == 0 } override fun onHitEnter(): Boolean { if (activity == null) return false activity.confirmAndUpdateStatus() return true } } private class ComposeEditTextTouchDelegate( val parentView: View, val delegateView: View ) : TouchDelegate(Rect(), delegateView) { private var delegateTargeted: Boolean = false override fun onTouchEvent(event: MotionEvent): Boolean { var sendToDelegate = false var handled = false when (event.action) { MotionEvent.ACTION_DOWN -> { if (TwidereViewUtils.hitView(event, delegateView)) { delegateTargeted = false sendToDelegate = false } else if (TwidereViewUtils.hitView(event, parentView)) { delegateTargeted = true sendToDelegate = true } } MotionEvent.ACTION_UP, MotionEvent.ACTION_MOVE -> { sendToDelegate = delegateTargeted } MotionEvent.ACTION_CANCEL -> { sendToDelegate = delegateTargeted delegateTargeted = false } } if (sendToDelegate) { handled = delegateView.dispatchTouchEvent(event) } return handled } } private class MentionColorSpan(color: Int) : ForegroundColorSpan(color) private open class ComposeException : Exception() private class StatusTooLongException( val summaryExceededStartIndex: Int, val textExceededStartIndex: Int, val summary: String?, val text: String ) : ComposeException() private class NoContentException : ComposeException() private class NoAccountException : ComposeException() companion object { // Constants private const val EXTRA_SHOULD_SAVE_ACCOUNTS = "should_save_accounts" private const val EXTRA_SHOULD_SAVE_VISIBILITY = "should_save_visibility" private const val EXTRA_ORIGINAL_TEXT = "original_text" private const val EXTRA_DRAFT_UNIQUE_ID = "draft_unique_id" private const val REQUEST_ATTACH_LOCATION_PERMISSION = 301 private const val REQUEST_PICK_MEDIA_PERMISSION = 302 private const val REQUEST_TAKE_PHOTO_PERMISSION = 303 private const val REQUEST_CAPTURE_VIDEO_PERMISSION = 304 private const val REQUEST_SET_SCHEDULE = 305 private const val REQUEST_ADD_GIF = 306 private fun ParcelableStatusUpdate.updateStatusActionExtras() = UpdateStatusActionExtras().also { it.inReplyToStatus = in_reply_to_status it.isPossiblySensitive = is_possibly_sensitive it.displayCoordinates = display_coordinates it.excludedReplyUserIds = excluded_reply_user_ids it.isExtendedReplyMode = extended_reply_mode it.summaryText = summary it.visibility = visibility } private fun Intent.getStreamExtra(): List? { val list = getParcelableArrayListExtra(Intent.EXTRA_STREAM) if (list != null) return list val item = getParcelableExtra(Intent.EXTRA_STREAM) if (item != null) return listOf(item) return null } } }