Twidere-App-Android-Twitter.../twidere/src/main/kotlin/org/mariotaku/twidere/activity/ComposeActivity.kt

2037 lines
82 KiB
Kotlin
Raw Normal View History

2016-07-02 05:54:53 +02:00
/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2014 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
package org.mariotaku.twidere.activity
2016-12-04 04:58:03 +01:00
import android.accounts.AccountManager
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
2016-07-02 05:54:53 +02:00
import android.app.Activity
import android.app.Dialog
2017-02-14 13:32:15 +01:00
import android.content.ActivityNotFoundException
import android.content.DialogInterface
import android.content.Intent
2016-07-02 05:54:53 +02:00
import android.graphics.Canvas
import android.graphics.PorterDuff.Mode
2017-04-04 06:37:44 +02:00
import android.graphics.Rect
2016-07-02 05:54:53 +02:00
import android.location.*
import android.net.Uri
import android.os.Bundle
import android.os.Parcelable
2017-01-22 18:13:31 +01:00
import android.support.v4.app.ActivityCompat
2016-07-02 05:54:53 +02:00
import android.support.v4.app.DialogFragment
import android.support.v7.app.AlertDialog
import android.support.v7.view.SupportMenuInflater
import android.support.v7.widget.ActionMenuView.OnMenuItemClickListener
2016-12-18 06:21:24 +01:00
import android.support.v7.widget.FixedLinearLayoutManager
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
2017-02-14 13:32:15 +01:00
import android.support.v7.widget.RecyclerView.ViewHolder
2016-07-02 05:54:53 +02:00
import android.support.v7.widget.helper.ItemTouchHelper
import android.text.*
2017-04-03 18:20:59 +02:00
import android.text.method.LinkMovementMethod
import android.text.style.*
2016-07-02 05:54:53 +02:00
import android.view.*
import android.view.View.OnClickListener
import android.view.View.OnLongClickListener
import android.view.animation.DecelerateInterpolator
import android.widget.ImageView
2016-07-02 05:54:53 +02:00
import android.widget.Toast
2017-03-01 15:12:25 +01:00
import com.bumptech.glide.Glide
2016-07-02 05:54:53 +02:00
import com.twitter.Extractor
2017-02-28 08:34:00 +01:00
import com.twitter.Validator
2016-07-02 05:54:53 +02:00
import kotlinx.android.synthetic.main.activity_compose.*
2017-04-14 09:05:51 +02:00
import nl.komponents.kovenant.task
2016-07-02 05:54:53 +02:00
import org.mariotaku.abstask.library.AbstractTask
import org.mariotaku.abstask.library.TaskStarter
2016-12-13 04:06:07 +01:00
import org.mariotaku.kpreferences.get
2017-04-14 10:10:15 +02:00
import org.mariotaku.kpreferences.set
2017-02-28 08:58:15 +01:00
import org.mariotaku.ktextension.*
2017-03-05 09:08:09 +01:00
import org.mariotaku.library.objectcursor.ObjectCursor
2017-01-22 18:13:31 +01:00
import org.mariotaku.pickncrop.library.MediaPickerActivity
2016-07-02 05:54:53 +02:00
import org.mariotaku.twidere.Constants.*
import org.mariotaku.twidere.R
import org.mariotaku.twidere.adapter.BaseRecyclerViewAdapter
2017-02-14 13:32:15 +01:00
import org.mariotaku.twidere.adapter.MediaPreviewAdapter
import org.mariotaku.twidere.annotation.AccountType
2016-12-13 04:06:07 +01:00
import org.mariotaku.twidere.constant.*
2017-02-28 08:58:15 +01:00
import org.mariotaku.twidere.constant.IntentConstants.EXTRA_SCREEN_NAME
2017-02-05 14:42:20 +01:00
import org.mariotaku.twidere.extension.applyTheme
2017-03-02 02:08:54 +01:00
import org.mariotaku.twidere.extension.loadProfileImage
2017-04-14 09:05:51 +02:00
import org.mariotaku.twidere.extension.model.applyUpdateStatus
2017-02-28 08:34:00 +01:00
import org.mariotaku.twidere.extension.model.textLimit
import org.mariotaku.twidere.extension.model.unique_id_non_null
2017-04-14 06:19:21 +02:00
import org.mariotaku.twidere.extension.text.twitter.ReplyTextAndMentions
2017-04-03 18:20:59 +02:00
import org.mariotaku.twidere.extension.text.twitter.extractReplyTextAndMentions
2017-04-08 16:06:04 +02:00
import org.mariotaku.twidere.extension.withAppendedPath
import org.mariotaku.twidere.fragment.*
2016-12-13 04:06:07 +01:00
import org.mariotaku.twidere.fragment.PermissionRequestDialog.PermissionRequestCancelCallback
2016-07-02 05:54:53 +02:00
import org.mariotaku.twidere.model.*
import org.mariotaku.twidere.model.analyzer.PurchaseFinished
import org.mariotaku.twidere.model.draft.UpdateStatusActionExtras
2017-03-25 14:44:07 +01:00
import org.mariotaku.twidere.model.schedule.ScheduleInfo
2016-12-04 04:58:03 +01:00
import org.mariotaku.twidere.model.util.AccountUtils
2016-07-02 05:54:53 +02:00
import org.mariotaku.twidere.model.util.ParcelableLocationUtils
import org.mariotaku.twidere.preference.ServicePickerPreference
import org.mariotaku.twidere.provider.TwidereDataStore.Drafts
2017-01-05 14:08:49 +01:00
import org.mariotaku.twidere.service.LengthyOperationsService
2017-02-14 13:32:15 +01:00
import org.mariotaku.twidere.task.compose.AbsAddMediaTask
2017-02-15 06:32:45 +01:00
import org.mariotaku.twidere.task.compose.AbsDeleteMediaTask
2017-04-14 09:05:51 +02:00
import org.mariotaku.twidere.task.twitter.UpdateStatusTask
2016-07-02 05:54:53 +02:00
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.GeneralComponentHelper
2017-03-25 14:44:07 +01:00
import org.mariotaku.twidere.util.premium.ExtraFeaturesService
import org.mariotaku.twidere.util.view.ViewAnimator
import org.mariotaku.twidere.util.view.ViewProperties
2016-07-02 05:54:53 +02:00
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
2017-02-14 13:32:15 +01:00
import org.mariotaku.twidere.view.holder.compose.MediaPreviewViewHolder
import java.io.IOException
2016-07-02 05:54:53 +02:00
import java.lang.ref.WeakReference
import java.util.*
import javax.inject.Inject
2017-03-29 07:05:39 +02:00
import kotlin.collections.ArrayList
2016-12-13 04:06:07 +01:00
import android.Manifest.permission as AndroidPermission
2016-07-02 05:54:53 +02:00
2016-12-13 04:06:07 +01:00
class ComposeActivity : BaseActivity(), OnMenuItemClickListener, OnClickListener, OnLongClickListener,
2017-02-15 06:53:53 +01:00
ActionMode.Callback, PermissionRequestCancelCallback, EditAltTextDialogFragment.EditAltTextCallback {
2016-07-02 05:54:53 +02:00
// Utility classes
@Inject
lateinit var extractor: Extractor
@Inject
2017-02-28 08:34:00 +01:00
lateinit var validator: Validator
2016-09-09 05:58:26 +02:00
@Inject
2017-01-21 05:08:47 +01:00
lateinit var locationManager: LocationManager
2016-07-02 05:54:53 +02:00
private lateinit var itemTouchHelper: ItemTouchHelper
private lateinit var bottomMenuAnimator: ViewAnimator
2016-12-13 01:45:14 +01:00
private val supportMenuInflater by lazy { SupportMenuInflater(this) }
2016-07-02 05:54:53 +02:00
2016-07-08 03:44:43 +02:00
private val backTimeoutRunnable = Runnable { navigateBackPressed = false }
2016-07-02 05:54:53 +02:00
// Adapters
2016-12-06 06:15:22 +01:00
private lateinit var mediaPreviewAdapter: MediaPreviewAdapter
private lateinit var accountsAdapter: AccountIconsAdapter
2016-07-02 05:54:53 +02:00
// Data fields
2016-07-07 05:42:08 +02:00
private var recentLocation: ParcelableLocation? = null
2016-07-08 03:44:43 +02:00
private var inReplyToStatus: ParcelableStatus? = null
private var mentionUser: ParcelableUser? = null
private var originalText: String? = null
2016-07-04 03:31:17 +02:00
private var possiblySensitive: Boolean = false
private var shouldSaveAccounts: Boolean = false
2016-07-08 03:44:43 +02:00
private var imageUploaderUsed: Boolean = false
private var statusShortenerUsed: Boolean = false
private var navigateBackPressed: Boolean = false
2016-07-05 15:19:51 +02:00
private var textChanged: Boolean = false
2016-07-08 03:44:43 +02:00
private var composeKeyMetaState: Int = 0
private var draft: Draft? = null
private var nameFirst: Boolean = false
private var draftUniqueId: String? = null
private var shouldSkipDraft: Boolean = false
private var ignoreMentions: Boolean = false
2017-04-13 18:57:14 +02:00
private var replyToSelf: Boolean = false
2017-03-25 14:44:07 +01:00
private var scheduleInfo: ScheduleInfo? = null
set(value) {
field = value
updateUpdateStatusIcon()
}
2016-07-02 05:54:53 +02:00
// Listeners
private var locationListener: LocationListener? = null
2016-07-02 05:54:53 +02:00
2017-04-14 09:05:51 +02:00
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<ParcelableMediaUpdate>
get() = mediaList.toTypedArray()
private val mediaList: List<ParcelableMediaUpdate>
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.user_is_protected && status.account_key != status.user_key
}
2017-03-25 14:44:07 +01:00
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
GeneralComponentHelper.build(this).inject(this)
nameFirst = preferences[nameFirstKey]
setContentView(R.layout.activity_compose)
bottomMenuAnimator = ViewAnimator()
bottomMenuAnimator.setupViews()
2017-03-25 14:44:07 +01:00
mediaPreviewAdapter = MediaPreviewAdapter(this, Glide.with(this))
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)
updateAttachedMediaView()
}
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)
2017-04-12 14:58:08 +02:00
val defaultAccountKeys = accountDetails.map(AccountDetails::key).toTypedArray()
2017-03-25 14:44:07 +01:00
menuBar.setOnMenuItemClickListener(this)
setupEditText()
accountSelectorButton.setOnClickListener(this)
replyLabel.setOnClickListener(this)
2017-04-13 18:57:14 +02:00
hintLabel.text = 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)
}
}
2017-04-03 18:20:59 +02:00
hintLabel.movementMethod = LinkMovementMethod.getInstance()
hintLabel.linksClickable = true
2017-03-25 14:44:07 +01:00
accountSelector.layoutManager = FixedLinearLayoutManager(this).apply {
orientation = LinearLayoutManager.HORIZONTAL
reverseLayout = false
stackFromEnd = false
2017-03-25 14:44:07 +01:00
}
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) {
// Restore from previous saved state
val selected = savedInstanceState.getTypedArray(EXTRA_ACCOUNT_KEYS, UserKey.CREATOR)
2017-04-12 14:58:08 +02:00
accountsAdapter.setSelectedAccountKeys(*selected)
2017-03-25 14:44:07 +01:00
possiblySensitive = savedInstanceState.getBoolean(EXTRA_IS_POSSIBLY_SENSITIVE)
val mediaList = savedInstanceState.getParcelableArrayList<ParcelableMediaUpdate>(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)
originalText = savedInstanceState.getString(EXTRA_ORIGINAL_TEXT)
draftUniqueId = savedInstanceState.getString(EXTRA_DRAFT_UNIQUE_ID)
scheduleInfo = savedInstanceState.getParcelable(EXTRA_SCHEDULE_INFO)
2017-04-14 09:57:25 +02:00
showLabelAndHint(intent)
2017-03-25 14:44:07 +01:00
} else {
// The context was first created
val notificationId = intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
val notificationAccount = intent.getParcelableExtra<UserKey>(EXTRA_NOTIFICATION_ACCOUNT)
if (notificationId != -1) {
twitterWrapper.clearNotificationAsync(notificationId, notificationAccount)
}
if (!handleIntent(intent)) {
handleDefaultIntent(intent)
}
2017-04-14 09:57:25 +02:00
showLabelAndHint(intent)
2017-04-12 14:58:08 +02:00
val selectedAccountKeys = accountsAdapter.selectedAccountKeys
if (selectedAccountKeys.isNullOrEmpty()) {
2017-03-25 14:44:07 +01:00
val idsInPrefs: Array<UserKey> = kPreferences[composeAccountsKey] ?: emptyArray()
2017-04-12 14:58:08 +02:00
val intersection: Array<UserKey> = defaultAccountKeys.intersect(listOf(*idsInPrefs)).toTypedArray()
2017-03-25 14:44:07 +01:00
if (intersection.isEmpty()) {
2017-04-12 14:58:08 +02:00
accountsAdapter.setSelectedAccountKeys(*defaultAccountKeys)
2017-03-25 14:44:07 +01:00
} else {
2017-04-12 14:58:08 +02:00
accountsAdapter.setSelectedAccountKeys(*intersection)
2017-03-25 14:44:07 +01:00
}
}
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)
2017-04-03 18:20:59 +02:00
2017-03-25 14:44:07 +01:00
val composeExtensionsIntent = Intent(INTENT_ACTION_EXTENSION_COMPOSE)
val imageExtensionsIntent = Intent(INTENT_ACTION_EXTENSION_EDIT_IMAGE)
2017-03-29 14:48:17 +02:00
val mediaMenuItem = menu.findItem(R.id.status_attachment)
2017-03-25 14:44:07 +01:00
if (mediaMenuItem != null && mediaMenuItem.hasSubMenu()) {
2017-04-11 16:22:50 +02:00
val subMenu = mediaMenuItem.subMenu
MenuUtils.addIntentToMenu(this, subMenu, composeExtensionsIntent,
MENU_GROUP_COMPOSE_EXTENSION)
MenuUtils.addIntentToMenu(this, subMenu, imageExtensionsIntent,
2017-04-02 09:59:09 +02:00
MENU_GROUP_IMAGE_EXTENSION)
2017-03-25 14:44:07 +01:00
}
updateViewStyle()
setMenu()
updateLocationState()
notifyAccountSelectionChanged()
updateUpdateStatusIcon()
textChanged = false
bottomMenuAnimator.showView(composeMenu, false)
2017-03-25 14:44:07 +01:00
updateAttachedMediaView()
}
2017-04-14 09:05:51 +02:00
override fun onDestroy() {
if (!shouldSkipDraft && hasComposingStatus() && isFinishing) {
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 = !ServicePickerPreference.isNoneValue(kPreferences[mediaUploaderKey])
statusShortenerUsed = !ServicePickerPreference.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()
try {
if (locationListener != null) {
locationManager.removeUpdates(locationListener)
locationListener = null
}
} catch (ignore: SecurityException) {
// That should not happen
}
super.onStop()
}
2017-03-25 14:44:07 +01:00
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
2016-07-02 05:54:53 +02:00
when (requestCode) {
2017-01-22 18:13:31 +01:00
REQUEST_TAKE_PHOTO, REQUEST_PICK_MEDIA -> {
if (resultCode == Activity.RESULT_OK && data != null) {
val src = MediaPickerActivity.getMediaUris(data)
2017-02-15 06:32:45 +01:00
TaskStarter.execute(AddMediaTask(this, src, false, false))
val extras = data.getBundleExtra(MediaPickerActivity.EXTRA_EXTRAS)
if (extras?.getBoolean(EXTRA_IS_POSSIBLY_SENSITIVE) ?: false) {
possiblySensitive = true
}
2016-07-02 05:54:53 +02:00
}
}
REQUEST_EDIT_IMAGE -> {
if (resultCode == Activity.RESULT_OK && data != null) {
if (data.data != null) {
2016-07-02 05:54:53 +02:00
setMenu()
updateTextCount()
}
}
}
REQUEST_EXTENSION_COMPOSE -> {
if (resultCode == Activity.RESULT_OK && data != null) {
2017-04-12 16:09:53 +02:00
// 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)
2016-07-02 05:54:53 +02:00
if (text != null) {
2017-04-11 15:51:49 +02:00
val editable = editText.editableText
2017-04-12 16:09:53 +02:00
if (editable == null || isReplaceMode) {
2017-04-11 15:51:49 +02:00
editText.setText(text)
} else {
editable.replace(editText.selectionStart, editText.selectionEnd, text)
}
setMenu()
updateTextCount()
2016-07-02 05:54:53 +02:00
}
2017-04-11 15:51:49 +02:00
2017-04-12 16:09:53 +02:00
val src = MediaPickerActivity.getMediaUris(data)?.takeIf(Array<Uri>::isNotEmpty) ?:
data.getParcelableExtra<Uri>(EXTRA_IMAGE_URI)?.let { arrayOf(it) }
if (src != null) {
2017-04-11 16:22:50 +02:00
TaskStarter.execute(AddMediaTask(this, src, false, false))
}
2016-07-02 05:54:53 +02:00
}
}
REQUEST_PURCHASE_EXTRA_FEATURES -> {
if (resultCode == Activity.RESULT_OK) {
Analyzer.log(PurchaseFinished.create(data!!))
}
}
2017-03-25 14:44:07 +01:00
REQUEST_SET_SCHEDULE -> {
if (resultCode == Activity.RESULT_OK) {
scheduleInfo = data?.getParcelableExtra(EXTRA_SCHEDULE_INFO)
2017-03-25 14:44:07 +01:00
}
}
2017-03-29 17:15:28 +02:00
REQUEST_ADD_GIF -> {
2017-03-30 17:30:43 +02:00
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)
})
2017-03-30 17:30:43 +02:00
.build()
startActivityForResult(intent, REQUEST_PICK_MEDIA)
}
2017-03-29 17:15:28 +02:00
}
2016-07-02 05:54:53 +02:00
}
}
override fun onSaveInstanceState(outState: Bundle) {
2016-12-06 06:15:22 +01:00
outState.putParcelableArray(EXTRA_ACCOUNT_KEYS, accountsAdapter.selectedAccountKeys)
2016-07-02 05:54:53 +02:00
outState.putParcelableArrayList(EXTRA_MEDIA, ArrayList<Parcelable>(mediaList))
2016-07-04 03:31:17 +02:00
outState.putBoolean(EXTRA_IS_POSSIBLY_SENSITIVE, possiblySensitive)
2016-07-08 03:44:43 +02:00
outState.putParcelable(EXTRA_STATUS, inReplyToStatus)
outState.putParcelable(EXTRA_USER, mentionUser)
outState.putParcelable(EXTRA_DRAFT, draft)
2017-03-25 14:44:07 +01:00
outState.putParcelable(EXTRA_SCHEDULE_INFO, scheduleInfo)
2016-07-04 03:31:17 +02:00
outState.putBoolean(EXTRA_SHOULD_SAVE_ACCOUNTS, shouldSaveAccounts)
2016-07-08 03:44:43 +02:00
outState.putString(EXTRA_ORIGINAL_TEXT, originalText)
outState.putString(EXTRA_DRAFT_UNIQUE_ID, draftUniqueId)
2016-07-02 05:54:53 +02:00
super.onSaveInstanceState(outState)
}
override fun onClick(view: View) {
2016-07-08 03:44:43 +02:00
when (view) {
updateStatus -> {
2016-07-02 05:54:53 +02:00
confirmAndUpdateStatus()
}
2016-07-08 03:44:43 +02:00
accountSelectorButton -> {
2016-07-02 05:54:53 +02:00
isAccountSelectorVisible = !isAccountSelectorVisible
}
2016-07-08 03:44:43 +02:00
replyLabel -> {
2016-07-02 05:54:53 +02:00
if (replyLabel.visibility != View.VISIBLE) return
replyLabel.setSingleLine(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 {
2016-07-08 03:44:43 +02:00
when (v) {
updateStatus -> {
2016-12-28 08:30:50 +01:00
Utils.showMenuItemToast(v, getString(R.string.action_send), true)
2016-07-02 05:54:53 +02:00
return true
}
}
return false
}
override fun onMenuItemClick(item: MenuItem): Boolean {
when (item.itemId) {
2017-01-23 16:24:58 +01:00
R.id.take_photo -> {
2017-03-29 14:48:17 +02:00
requestOrTakePhoto()
}
R.id.record_video -> {
requestOrCaptureVideo()
2017-01-23 16:24:58 +01:00
}
2017-03-29 14:48:17 +02:00
R.id.add_media -> {
2017-01-22 18:13:31 +01:00
requestOrPickMedia()
2016-07-02 05:54:53 +02:00
}
R.id.drafts -> {
IntentUtils.openDrafts(this)
}
R.id.delete -> {
2017-02-15 06:32:45 +01:00
TaskStarter.execute(DeleteMediaTask(this, media))
2016-07-02 05:54:53 +02:00
}
R.id.toggle_sensitive -> {
2017-04-14 09:05:51 +02:00
if (!hasMedia) return true
2016-07-04 03:31:17 +02:00
possiblySensitive = !possiblySensitive
2016-07-02 05:54:53 +02:00
setMenu()
updateTextCount()
}
2017-03-25 14:44:07 +01:00
R.id.schedule -> {
2017-03-29 17:15:28 +02:00
val provider = statusScheduleProvider ?: return true
startActivityForResult(provider.createSetScheduleIntent(), REQUEST_SET_SCHEDULE)
}
R.id.add_gif -> {
val provider = gifShareProvider ?: return true
2017-03-30 17:30:43 +02:00
startActivityForResult(provider.createGifSelectorIntent(), REQUEST_ADD_GIF)
2017-03-25 14:44:07 +01:00
}
2016-07-02 05:54:53 +02:00
else -> {
2017-03-29 14:48:17 +02:00
when (item.groupId) {
R.id.location_option -> {
locationMenuItemSelected(item)
}
else -> {
2017-03-29 17:15:28 +02:00
extensionIntentItemSelected(item)
2016-07-02 05:54:53 +02:00
}
}
}
}
return true
}
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
when (ev.actionMasked) {
MotionEvent.ACTION_DOWN -> {
2017-04-04 06:37:44 +02:00
if (isAccountSelectorVisible && !TwidereViewUtils.hitView(ev, accountSelectorButton)) {
2016-07-08 03:44:43 +02:00
val layoutManager = accountSelector.layoutManager
val clickedItem = (0 until layoutManager.childCount).any {
TwidereViewUtils.hitView(ev, layoutManager.getChildAt(it))
2016-07-02 05:54:53 +02:00
}
2016-07-08 03:44:43 +02:00
if (!clickedItem) {
isAccountSelectorVisible = false
return true
}
2016-07-02 05:54:53 +02:00
}
}
}
return super.dispatchTouchEvent(ev)
}
override fun onTouchEvent(event: MotionEvent): Boolean {
if (event.action == MotionEvent.ACTION_DOWN) {
val window = window
2017-04-04 06:37:44 +02:00
if (!TwidereViewUtils.hitView(event, window.decorView)
2016-07-02 05:54:53 +02:00
&& window.peekDecorView() != null && !hasComposingStatus()) {
onBackPressed()
return true
}
}
return super.onTouchEvent(event)
}
override fun getMenuInflater(): MenuInflater {
2016-07-07 05:42:08 +02:00
return supportMenuInflater
2016-07-02 05:54:53 +02:00
}
2017-04-14 09:05:51 +02:00
override fun onActionModeStarted(mode: ActionMode) {
super.onActionModeStarted(mode)
ThemeUtils.applyColorFilterToMenuIcon(mode.menu,
ThemeUtils.getContrastActionBarItemColor(this), 0, 0, Mode.MULTIPLY)
2016-07-02 05:54:53 +02:00
}
2017-04-14 09:05:51 +02:00
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()
}
2016-12-05 12:28:17 +01:00
}
2017-04-14 09:05:51 +02:00
return super.dispatchKeyEvent(event)
2016-07-02 05:54:53 +02:00
}
2017-04-14 09:05:51 +02:00
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 handleKeyboardShortcutRepeat(handler: KeyboardShortcutsHandler, keyCode: Int,
repeatCount: Int, event: KeyEvent, metaState: Int): Boolean {
return super.handleKeyboardShortcutRepeat(handler, keyCode, repeatCount, event, metaState)
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, 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 onPermissionRequestCancelled(requestCode: Int) {
when (requestCode) {
REQUEST_ATTACH_LOCATION_PERMISSION -> {
}
}
}
override fun onSetAltText(position: Int, altText: String?) {
mediaPreviewAdapter.setAltText(position, altText)
}
private fun discardTweet() {
val context = applicationContext
val media = mediaList
task { media.forEach { media -> Utils.deleteMedia(context, Uri.parse(media.uri)) } }
}
private fun hasComposingStatus(): Boolean {
if (intent.action == INTENT_ACTION_EDIT_DRAFT) return true
if (hasMedia) return true
val text = editText.text?.toString().orEmpty()
val replyTextAndMentions = getTwitterReplyTextAndMentions(text)
if (replyTextAndMentions != null) {
return replyTextAndMentions.replyText.isNotEmpty()
}
return text.isNotEmpty() && text != originalText
}
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 displaySelectedAccountsIcon() {
val accounts = accountsAdapter.selectedAccounts
val account = accounts.singleOrNull()
val displayDoneIcon = isAccountSelectorVisible
if (account != null) {
2016-07-08 03:44:43 +02:00
accountsCount.setText(null)
if (displayDoneIcon) {
Glide.clear(accountProfileImage)
accountProfileImage.setColorFilter(ThemeUtils.getColorFromAttribute(this,
android.R.attr.colorForeground, 0))
accountProfileImage.scaleType = ImageView.ScaleType.CENTER_INSIDE
accountProfileImage.setImageResource(R.drawable.ic_action_confirm)
} else {
accountProfileImage.clearColorFilter()
accountProfileImage.scaleType = ImageView.ScaleType.CENTER_CROP
Glide.with(this).loadProfileImage(this, account, accountProfileImage.style)
.into(accountProfileImage)
}
2016-07-08 03:44:43 +02:00
accountProfileImage.setBorderColor(account.color)
2016-07-02 05:54:53 +02:00
} else {
2016-07-08 03:44:43 +02:00
accountsCount.setText(accounts.size.toString())
Glide.clear(accountProfileImage)
if (displayDoneIcon) {
accountProfileImage.setImageResource(R.drawable.ic_action_confirm)
accountProfileImage.scaleType = ImageView.ScaleType.CENTER_INSIDE
} else {
accountProfileImage.setImageDrawable(null)
}
2016-07-08 03:44:43 +02:00
accountProfileImage.setBorderColors(*Utils.getAccountColors(accounts))
2016-07-02 05:54:53 +02:00
}
if (displayDoneIcon) {
accountsCount.visibility = View.GONE
} else {
accountsCount.visibility = View.VISIBLE
}
2016-07-02 05:54:53 +02:00
}
2017-03-29 14:48:17 +02:00
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
2017-03-29 14:48:17 +02:00
}
R.id.location_coarse -> {
attachLocationChecked = true
attachPreciseLocationChecked = false
}
}
kPreferences.edit {
this[attachLocationKey] = attachLocationChecked
this[attachPreciseLocationKey] = attachPreciseLocationChecked
}
if (attachLocationChecked) {
requestOrUpdateLocation()
} else if (locationListener != null) {
try {
locationManager.removeUpdates(locationListener)
locationListener = null
} catch (e: SecurityException) {
//Ignore
}
}
updateLocationState()
setMenu()
updateTextCount()
}
2017-03-29 17:15:28 +02:00
private fun extensionIntentItemSelected(item: MenuItem) {
val intent = item.intent ?: return
try {
val action = intent.action
2017-04-11 16:22:50 +02:00
when (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)
2017-03-29 17:15:28 +02:00
}
2017-04-11 16:22:50 +02:00
else -> startActivity(intent)
2017-03-29 17:15:28 +02:00
}
} catch (e: ActivityNotFoundException) {
Analyzer.logException(e)
}
}
2017-01-07 15:45:33 +01:00
private fun updateViewStyle() {
accountProfileImage.style = preferences[profileImageStyleKey]
}
2016-07-02 05:54:53 +02:00
private fun setupEditText() {
val sendByEnter = preferences.getBoolean(KEY_QUICK_SEND)
EditTextEnterHandler.attach(editText, ComposeEnterListener(this), sendByEnter)
editText.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
}
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<String>()
for (imageSpan in imageSpans) {
imageSources.add(imageSpan.source)
s.setSpan(MarkForDeleteSpan(), start, start + count,
Spanned.SPAN_INCLUSIVE_INCLUSIVE)
}
if (!imageSources.isEmpty()) {
2017-01-22 18:13:31 +01:00
val intent = ThemedMediaPickerActivity.withThemed(this@ComposeActivity)
.getMedia(Uri.parse(imageSources[0]))
.build()
startActivityForResult(intent, REQUEST_PICK_MEDIA)
2016-07-02 05:54:53 +02:00
}
}
}
override fun afterTextChanged(s: Editable) {
2016-12-06 06:15:22 +01:00
textChanged = s.isEmpty()
2016-07-02 05:54:53 +02:00
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)
}
2016-07-02 05:54:53 +02:00
}
})
editText.customSelectionActionModeCallback = this
2017-04-04 06:37:44 +02:00
editTextContainer.touchDelegate = ComposeEditTextTouchDelegate(editTextContainer, editText)
2016-07-02 05:54:53 +02:00
}
2017-04-14 10:10:15 +02:00
2016-07-02 05:54:53 +02:00
private fun addMedia(media: List<ParcelableMediaUpdate>) {
2016-12-06 06:15:22 +01:00
mediaPreviewAdapter.addAll(media)
2016-07-02 05:54:53 +02:00
updateAttachedMediaView()
}
2017-04-14 10:10:15 +02:00
private fun removeMedia(list: List<ParcelableMediaUpdate>) {
mediaPreviewAdapter.removeAll(list)
}
2016-07-02 05:54:53 +02:00
private fun clearMedia() {
2016-12-06 06:15:22 +01:00
mediaPreviewAdapter.clear()
2016-07-02 05:54:53 +02:00
updateAttachedMediaView()
}
private fun updateAttachedMediaView() {
attachedMediaPreview.visibility = if (hasMedia) View.VISIBLE else View.GONE
setMenu()
}
2017-04-14 09:57:25 +02:00
// MARK: Begin intent handling
2016-07-02 05:54:53 +02:00
private fun handleIntent(intent: Intent): Boolean {
2016-07-04 03:31:17 +02:00
shouldSaveAccounts = false
2016-12-13 04:06:07 +01:00
mentionUser = intent.getParcelableExtra(EXTRA_USER)
inReplyToStatus = intent.getParcelableExtra(EXTRA_STATUS)
2017-04-14 09:57:25 +02:00
when (intent.action) {
2016-07-02 05:54:53 +02:00
INTENT_ACTION_REPLY -> {
2016-07-08 03:44:43 +02:00
return handleReplyIntent(inReplyToStatus)
2016-07-02 05:54:53 +02:00
}
INTENT_ACTION_QUOTE -> {
2016-07-08 03:44:43 +02:00
return handleQuoteIntent(inReplyToStatus)
2016-07-02 05:54:53 +02:00
}
INTENT_ACTION_EDIT_DRAFT -> {
2016-12-13 04:06:07 +01:00
draft = intent.getParcelableExtra(EXTRA_DRAFT)
2016-07-08 03:44:43 +02:00
return handleEditDraftIntent(draft)
2016-07-02 05:54:53 +02:00
}
INTENT_ACTION_MENTION -> {
2016-07-08 03:44:43 +02:00
return handleMentionIntent(mentionUser)
2016-07-02 05:54:53 +02:00
}
INTENT_ACTION_REPLY_MULTIPLE -> {
2017-04-14 09:57:25 +02:00
val screenNames: Array<String>? = intent.getStringArrayExtra(EXTRA_SCREEN_NAMES)
val accountKey: UserKey? = intent.getParcelableExtra(EXTRA_ACCOUNT_KEYS)
val inReplyToStatus: ParcelableStatus? = intent.getParcelableExtra(EXTRA_IN_REPLY_TO_STATUS)
2016-07-02 05:54:53 +02:00
return handleReplyMultipleIntent(screenNames, accountKey, inReplyToStatus)
}
INTENT_ACTION_COMPOSE_TAKE_PHOTO -> {
2017-03-29 14:48:17 +02:00
requestOrTakePhoto()
2017-01-22 18:13:31 +01:00
return true
2016-07-02 05:54:53 +02:00
}
INTENT_ACTION_COMPOSE_PICK_IMAGE -> {
2017-01-22 18:13:31 +01:00
requestOrPickMedia()
return true
2016-07-02 05:54:53 +02:00
}
}
// Unknown action or no intent extras
return false
}
private fun handleMentionIntent(user: ParcelableUser?): Boolean {
if (user == null || user.key == null) return false
val accountScreenName = DataStoreUtils.getAccountScreenName(this, user.account_key)
if (TextUtils.isEmpty(accountScreenName)) return false
editText.setText(String.format("@%s ", user.screen_name))
val selection_end = editText.length()
editText.setSelection(selection_end)
2017-04-12 14:58:08 +02:00
accountsAdapter.setSelectedAccountKeys(user.account_key)
2016-07-02 05:54:53 +02:00
return true
}
private fun handleQuoteIntent(status: ParcelableStatus?): Boolean {
if (status == null) return false
editText.setText(Utils.getQuoteStatus(this, status))
editText.setSelection(0)
2017-04-12 14:58:08 +02:00
accountsAdapter.setSelectedAccountKeys(status.account_key)
2017-04-14 09:57:25 +02:00
showQuoteLabelAndHint(status)
2016-07-02 05:54:53 +02:00
return true
}
private fun handleReplyIntent(status: ParcelableStatus?): Boolean {
if (status == null) return false
2016-12-15 06:11:32 +01:00
val am = AccountManager.get(this)
2017-04-02 09:12:37 +02:00
val details = AccountUtils.getAccountDetails(am, status.account_key, false) ?: return false
val accountUser = details.user
2017-03-29 07:05:39 +02:00
val mentions = ArrayList<String>()
2017-04-13 18:57:14 +02:00
if (accountUser.key != status.user_key) {
2017-04-02 09:12:37 +02:00
editText.append("@${status.user_screen_name} ")
2016-07-02 05:54:53 +02:00
}
2017-04-14 09:05:51 +02:00
var selectionStart = editText.length()
2016-07-02 05:54:53 +02:00
if (status.is_retweet && !TextUtils.isEmpty(status.retweeted_by_user_screen_name)) {
mentions.add(status.retweeted_by_user_screen_name)
}
if (status.is_quote && !TextUtils.isEmpty(status.quoted_user_screen_name)) {
mentions.add(status.quoted_user_screen_name)
}
2017-04-02 09:12:37 +02:00
if (status.mentions.isNotNullOrEmpty()) {
status.mentions.filterNot {
it.key == status.account_key || it.screen_name.isNullOrEmpty()
}.mapTo(mentions) { it.screen_name }
2016-07-04 03:31:17 +02:00
mentions.addAll(extractor.extractMentionedScreennames(status.quoted_text_plain))
2016-07-02 05:54:53 +02:00
} else if (USER_TYPE_FANFOU_COM == status.account_key.host) {
addFanfouHtmlToMentions(status.text_unescaped, status.spans, mentions)
if (status.is_quote) {
addFanfouHtmlToMentions(status.quoted_text_unescaped, status.quoted_spans, mentions)
}
} else {
2016-07-04 03:31:17 +02:00
mentions.addAll(extractor.extractMentionedScreennames(status.text_plain))
2016-07-02 05:54:53 +02:00
if (status.is_quote) {
2016-07-04 03:31:17 +02:00
mentions.addAll(extractor.extractMentionedScreennames(status.quoted_text_plain))
2016-07-02 05:54:53 +02:00
}
}
2017-03-29 07:05:39 +02:00
mentions.distinctBy { it.toLowerCase(Locale.US) }.filterNot {
2017-04-02 09:12:37 +02:00
return@filterNot it.equals(status.user_screen_name, ignoreCase = true)
2017-03-07 14:30:26 +01:00
}.forEach { editText.append("@$it ") }
2017-04-02 09:12:37 +02:00
// For non-Twitter instances, put current user mention at last
if (details.type != AccountType.TWITTER && accountUser.key == status.user_key) {
2017-03-07 14:30:26 +01:00
selectionStart = editText.length()
editText.append("@${status.user_screen_name} ")
2016-12-18 03:04:02 +01:00
}
2017-03-07 14:30:26 +01:00
2016-07-02 05:54:53 +02:00
val selectionEnd = editText.length()
editText.setSelection(selectionStart, selectionEnd)
2017-04-12 14:58:08 +02:00
accountsAdapter.setSelectedAccountKeys(status.account_key)
2017-04-14 09:57:25 +02:00
showReplyLabelAndHint(status)
2016-07-02 05:54:53 +02:00
return true
}
2017-04-14 09:57:25 +02:00
private fun handleEditDraftIntent(draft: Draft?): Boolean {
if (draft == null) return false
draftUniqueId = draft.unique_id_non_null
editText.setText(draft.text)
val selectionEnd = editText.length()
editText.setSelection(selectionEnd)
accountsAdapter.setSelectedAccountKeys(*draft.account_keys ?: emptyArray())
if (draft.media != null) {
addMedia(Arrays.asList(*draft.media))
}
recentLocation = draft.location
(draft.action_extras as? UpdateStatusActionExtras)?.let {
possiblySensitive = it.isPossiblySensitive
inReplyToStatus = it.inReplyToStatus
}
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 hasAccountKeys: Boolean
if (intent.hasExtra(EXTRA_ACCOUNT_KEYS)) {
val accountKeys = intent.getParcelableArrayExtra(EXTRA_ACCOUNT_KEYS).toTypedArray(UserKey.CREATOR)
accountsAdapter.setSelectedAccountKeys(*accountKeys)
hasAccountKeys = true
} else if (intent.hasExtra(EXTRA_ACCOUNT_KEY)) {
val accountKey = intent.getParcelableExtra<UserKey>(EXTRA_ACCOUNT_KEY)
accountsAdapter.setSelectedAccountKeys(accountKey)
hasAccountKeys = true
} else {
hasAccountKeys = false
}
if (Intent.ACTION_SEND == action) {
shouldSaveAccounts = false
val stream = intent.getParcelableExtra<Uri>(Intent.EXTRA_STREAM)
if (stream != null) {
val src = arrayOf(stream)
TaskStarter.execute(AddMediaTask(this, src, true, false))
}
} else if (Intent.ACTION_SEND_MULTIPLE == action) {
shouldSaveAccounts = false
val extraStream = intent.getParcelableArrayListExtra<Uri>(Intent.EXTRA_STREAM)
if (extraStream != null) {
val src = extraStream.toTypedArray()
TaskStarter.execute(AddMediaTask(this, src, true, false))
}
} else {
shouldSaveAccounts = !hasAccountKeys
val data = intent.data
if (data != null) {
val src = arrayOf(data)
TaskStarter.execute(AddMediaTask(this, src, true, false))
}
}
val extraSubject = intent.getCharSequenceExtra(Intent.EXTRA_SUBJECT)
val extraText = intent.getCharSequenceExtra(Intent.EXTRA_TEXT)
editText.setText(Utils.getShareStatus(this, extraSubject, extraText))
val selectionEnd = editText.length()
editText.setSelection(selectionEnd)
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)
when (draft?.action_type) {
Draft.Action.REPLY -> {
return showReplyLabelAndHint((draft.action_extras as? UpdateStatusActionExtras)?.inReplyToStatus)
}
Draft.Action.QUOTE -> {
return showQuoteLabelAndHint((draft.action_extras as? UpdateStatusActionExtras)?.inReplyToStatus)
}
else -> {
showDefaultLabelAndHint()
return false
}
}
}
else -> {
showDefaultLabelAndHint()
return false
}
}
}
private fun showQuoteLabelAndHint(status: ParcelableStatus?): Boolean {
if (status == null) {
showDefaultLabelAndHint()
return false
}
val replyToName = userColorNameManager.getDisplayName(status, nameFirst)
replyLabel.text = 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.text = 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
2016-07-02 05:54:53 +02:00
private fun addFanfouHtmlToMentions(text: String, spans: Array<SpanItem>?, mentions: MutableCollection<String>) {
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))
}
}
}
2017-04-12 14:58:08 +02:00
private fun handleReplyMultipleIntent(screenNames: Array<String>?, accountKey: UserKey?,
2017-04-14 09:57:25 +02:00
inReplyToStatus: ParcelableStatus?): Boolean {
if (screenNames == null || screenNames.isEmpty() || accountKey == null ||
inReplyToStatus == null) return false
val myScreenName = DataStoreUtils.getAccountScreenName(this, accountKey) ?: return false
2016-12-06 06:15:22 +01:00
screenNames.filterNot { it.equals(myScreenName, ignoreCase = true) }
.forEach { editText.append("@$it ") }
2016-07-02 05:54:53 +02:00
editText.setSelection(editText.length())
2017-04-12 14:58:08 +02:00
accountsAdapter.setSelectedAccountKeys(accountKey)
2016-07-08 03:44:43 +02:00
this.inReplyToStatus = inReplyToStatus
2016-07-02 05:54:53 +02:00
return true
}
private fun notifyAccountSelectionChanged() {
displaySelectedAccountsIcon()
2016-12-06 06:15:22 +01:00
val accounts = accountsAdapter.selectedAccounts
2017-04-07 06:12:34 +02:00
editText.accountKey = accounts.firstOrNull()?.key ?: Utils.getDefaultAccountKey(this)
2017-02-28 08:34:00 +01:00
statusTextCount.maxLength = accounts.textLimit
2017-04-14 06:19:21 +02:00
val singleAccount = accounts.singleOrNull()
ignoreMentions = singleAccount?.type == AccountType.TWITTER
replyToSelf = singleAccount?.let { it.key == inReplyToStatus?.user_key } ?: false
2016-07-02 05:54:53 +02:00
setMenu()
}
2017-03-29 14:48:17 +02:00
private fun requestOrTakePhoto() {
2017-01-22 18:13:31 +01:00
if (checkAnySelfPermissionsGranted(AndroidPermission.WRITE_EXTERNAL_STORAGE)) {
2017-03-29 14:48:17 +02:00
takePhoto()
2017-01-22 18:13:31 +01:00
return
}
ActivityCompat.requestPermissions(this, arrayOf(AndroidPermission.WRITE_EXTERNAL_STORAGE),
2017-03-29 14:48:17 +02:00
REQUEST_TAKE_PHOTO_PERMISSION)
2017-01-22 18:13:31 +01:00
}
2017-03-29 14:48:17 +02:00
private fun requestOrCaptureVideo() {
if (checkAnySelfPermissionsGranted(AndroidPermission.WRITE_EXTERNAL_STORAGE)) {
captureVideo()
return
2017-01-23 16:24:58 +01:00
}
2017-03-29 14:48:17 +02:00
ActivityCompat.requestPermissions(this, arrayOf(AndroidPermission.WRITE_EXTERNAL_STORAGE),
REQUEST_CAPTURE_VIDEO_PERMISSION)
2017-01-22 18:13:31 +01:00
}
private fun requestOrPickMedia() {
if (checkAnySelfPermissionsGranted(AndroidPermission.WRITE_EXTERNAL_STORAGE)) {
pickMedia()
return
}
ActivityCompat.requestPermissions(this, arrayOf(AndroidPermission.WRITE_EXTERNAL_STORAGE),
REQUEST_PICK_MEDIA_PERMISSION)
}
2017-03-29 14:48:17 +02:00
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
}
2017-01-22 18:13:31 +01:00
private fun pickMedia(): Boolean {
val intent = ThemedMediaPickerActivity.withThemed(this)
2017-03-29 14:48:17 +02:00
.pickMedia()
2017-01-22 18:13:31 +01:00
.containsVideo(true)
.videoOnly(false)
.allowMultiple(true)
.build()
startActivityForResult(intent, REQUEST_PICK_MEDIA)
2016-07-02 05:54:53 +02:00
return true
}
private fun saveAccountSelection() {
2016-07-04 03:31:17 +02:00
if (!shouldSaveAccounts) return
2017-04-14 10:10:15 +02:00
preferences[composeAccountsKey] = accountsAdapter.selectedAccountKeys
2016-07-02 05:54:53 +02:00
}
private fun setMenu() {
if (menuBar == null) return
2016-07-08 03:44:43 +02:00
val menu = menuBar.menu
2017-04-14 09:05:51 +02:00
val hasMedia = this.hasMedia
2016-07-02 05:54:53 +02:00
/*
* No media & Not reply: [Take photo][Add image][Attach location][Drafts]
* Has media & Not reply: [Take photo][Media menu][Attach location][Drafts]
* Is reply: [Media menu][View status][Attach location][Drafts]
*/
2017-03-29 17:15:28 +02:00
menu.setItemAvailability(R.id.toggle_sensitive, hasMedia)
menu.setItemAvailability(R.id.schedule, extraFeaturesService.isSupported(
2017-03-25 14:44:07 +01:00
ExtraFeaturesService.FEATURE_SCHEDULE_STATUS))
2017-03-29 17:15:28 +02:00
menu.setItemAvailability(R.id.add_gif, extraFeaturesService.isSupported(
ExtraFeaturesService.FEATURE_SHARE_GIF))
2016-07-02 05:54:53 +02:00
2017-04-02 09:59:09 +02:00
menu.setGroupAvailability(MENU_GROUP_IMAGE_EXTENSION, hasMedia)
2016-07-04 03:31:17 +02:00
menu.setItemChecked(R.id.toggle_sensitive, hasMedia && possiblySensitive)
2017-03-29 14:48:17 +02:00
val attachLocation = kPreferences[attachLocationKey]
val attachPreciseLocation = kPreferences[attachPreciseLocationKey]
if (!attachLocation) {
menu.setItemChecked(R.id.location_off, true)
menu.setMenuItemIcon(R.id.location_submenu, R.drawable.ic_action_location_off)
} else if (attachPreciseLocation) {
menu.setItemChecked(R.id.location_precise, true)
menu.setMenuItemIcon(R.id.location_submenu, R.drawable.ic_action_location)
} else {
menu.setItemChecked(R.id.location_coarse, true)
menu.setMenuItemIcon(R.id.location_submenu, R.drawable.ic_action_location)
}
2017-04-02 09:59:09 +02:00
ThemeUtils.wrapMenuIcon(menuBar, MENU_GROUP_IMAGE_EXTENSION)
2016-07-02 05:54:53 +02:00
ThemeUtils.resetCheatSheet(menuBar)
}
private fun setProgressVisible(visible: Boolean) {
if (isFinishing) return
2016-07-07 09:39:32 +02:00
executeAfterFragmentResumed { activity ->
val composeActivity = activity as ComposeActivity
val fm = composeActivity.supportFragmentManager
val f = fm.findFragmentByTag(DISCARD_STATUS_DIALOG_FRAGMENT_TAG)
if (!visible && f is DialogFragment) {
f.dismiss()
} else if (visible) {
val df = ProgressDialogFragment()
df.show(fm, DISCARD_STATUS_DIALOG_FRAGMENT_TAG)
df.isCancelable = false
}
2017-04-14 09:05:51 +02:00
}
2017-04-04 06:37:44 +02:00
}
2016-07-02 05:54:53 +02:00
private fun setRecentLocation(location: ParcelableLocation?) {
if (location != null) {
2016-12-13 04:06:07 +01:00
val attachPreciseLocation = kPreferences[attachPreciseLocationKey]
2016-07-02 05:54:53 +02:00
if (attachPreciseLocation) {
locationLabel.text = ParcelableLocationUtils.getHumanReadableString(location, 3)
2016-07-02 05:54:53 +02:00
} else {
if (locationLabel.tag == null || location != recentLocation) {
2017-04-03 18:53:12 +02:00
val task = DisplayPlaceNameTask()
2016-07-04 03:31:17 +02:00
task.params = location
2017-04-03 18:53:12 +02:00
task.callback = this
2016-07-02 05:54:53 +02:00
TaskStarter.execute(task)
}
}
} else {
locationLabel.setText(R.string.unknown_location)
2016-07-02 05:54:53 +02:00
}
2016-07-07 05:42:08 +02:00
recentLocation = location
2016-07-02 05:54:53 +02:00
}
/**
* 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
2016-12-13 04:06:07 +01:00
val attachLocation = kPreferences[attachLocationKey]
2016-07-02 05:54:53 +02:00
if (!attachLocation) {
return false
}
2016-12-13 04:06:07 +01:00
val attachPreciseLocation = kPreferences[attachPreciseLocationKey]
2016-07-02 05:54:53 +02:00
val criteria = Criteria()
if (attachPreciseLocation) {
criteria.accuracy = Criteria.ACCURACY_FINE
} else {
criteria.accuracy = Criteria.ACCURACY_COARSE
}
2016-12-13 01:45:14 +01:00
val provider = locationManager.getBestProvider(criteria, true)
2016-07-02 05:54:53 +02:00
if (provider != null) {
locationLabel.setText(R.string.getting_location)
locationListener = ComposeLocationListener(this)
2016-12-13 01:45:14 +01:00
locationManager.requestLocationUpdates(provider, 0, 0f, locationListener)
2016-07-02 05:54:53 +02:00
val location = Utils.getCachedLocation(this)
if (location != null) {
2016-12-13 01:45:14 +01:00
locationListener?.onLocationChanged(location)
2016-07-02 05:54:53 +02:00
}
} else {
2017-01-26 14:28:43 +01:00
Toast.makeText(this, R.string.message_toast_cannot_get_location, Toast.LENGTH_SHORT).show()
2016-07-02 05:54:53 +02:00
}
return provider != null
}
private fun requestOrUpdateLocation() {
2016-12-13 04:06:07 +01:00
if (checkAnySelfPermissionsGranted(AndroidPermission.ACCESS_COARSE_LOCATION, AndroidPermission.ACCESS_FINE_LOCATION)) {
2016-07-02 05:54:53 +02:00
try {
startLocationUpdateIfEnabled()
} catch (e: SecurityException) {
2017-01-26 14:28:43 +01:00
Toast.makeText(this, R.string.message_toast_cannot_get_location, Toast.LENGTH_SHORT).show()
2016-07-02 05:54:53 +02:00
}
} else {
2016-12-13 04:06:07 +01:00
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)
2016-07-02 05:54:53 +02:00
}
}
private fun updateStatus() {
2017-02-28 08:34:00 +01:00
if (isFinishing || editText == null) return
2017-04-13 18:57:14 +02:00
2017-04-14 06:19:21 +02:00
val update = try {
getStatusUpdate()
} catch(e: NoAccountException) {
2017-01-26 14:28:43 +01:00
editText.error = getString(R.string.message_toast_no_account_selected)
2016-07-02 05:54:53 +02:00
return
2017-04-14 06:19:21 +02:00
} catch(e: NoContentException) {
2016-07-02 05:54:53 +02:00
editText.error = getString(R.string.error_message_no_content)
return
2017-04-14 06:19:21 +02:00
} catch(e: StatusTooLongException) {
2016-07-02 05:54:53 +02:00
editText.error = getString(R.string.error_message_status_too_long)
2017-04-14 06:19:21 +02:00
editText.setSelection(e.exceededStartIndex, editText.length())
2016-07-02 05:54:53 +02:00
return
}
2017-04-14 06:19:21 +02:00
LengthyOperationsService.updateStatusesAsync(this, update.draft_action, statuses = update,
scheduleInfo = scheduleInfo)
finishComposing()
}
2017-04-14 10:10:15 +02:00
private fun finishComposing() {
if (preferences[noCloseAfterTweetSentKey] && inReplyToStatus == null) {
possiblySensitive = false
shouldSaveAccounts = 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()
}
}
2017-04-14 06:19:21 +02:00
private fun getStatusUpdate(): ParcelableStatusUpdate {
val accountKeys = accountsAdapter.selectedAccountKeys
if (accountKeys.isEmpty()) throw NoAccountException()
val update = ParcelableStatusUpdate()
val media = this.media
val text = editText.text?.toString().orEmpty()
val accounts = AccountUtils.getAllAccountDetails(AccountManager.get(this), accountKeys, true)
val maxLength = statusTextCount.maxLength
2017-04-14 09:05:51 +02:00
val inReplyTo = inReplyToStatus
2017-04-14 06:19:21 +02:00
val replyTextAndMentions = getTwitterReplyTextAndMentions(text)
2017-04-14 09:05:51 +02:00
if (inReplyTo != null && replyTextAndMentions != null) {
val (replyStartIndex, replyText, _, excludedMentions, replyToOriginalUser) =
replyTextAndMentions
2017-04-14 06:19:21 +02:00
if (replyText.isEmpty() && media.isEmpty()) throw NoContentException()
if (!statusShortenerUsed && validator.getTweetLength(replyText) > maxLength) {
throw StatusTooLongException(replyStartIndex + replyText.offsetByCodePoints(0, maxLength))
}
update.text = replyText
update.extended_reply_mode = true
update.excluded_reply_user_ids = excludedMentions.map { it.key.id }.toTypedArray()
2017-04-14 09:05:51 +02:00
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.getTwitterStatusLink(inReplyTo.user_screen_name,
inReplyTo.id).toString()
}
2017-04-14 06:19:21 +02:00
} else {
if (text.isEmpty() && media.isEmpty()) throw NoContentException()
if (!statusShortenerUsed && validator.getTweetLength(text) > maxLength) {
throw StatusTooLongException(text.offsetByCodePoints(0, maxLength))
}
update.text = text
update.extended_reply_mode = false
}
2016-12-13 04:06:07 +01:00
val attachLocation = kPreferences[attachLocationKey]
val attachPreciseLocation = kPreferences[attachPreciseLocationKey]
2017-04-14 09:05:51 +02:00
update.draft_action = draftAction
update.accounts = accounts
2016-07-02 05:54:53 +02:00
if (attachLocation) {
2016-07-07 05:42:08 +02:00
update.location = recentLocation
2016-07-02 05:54:53 +02:00
update.display_coordinates = attachPreciseLocation
}
update.media = media
2017-04-14 09:05:51 +02:00
update.in_reply_to_status = inReplyTo
2017-04-14 06:19:21 +02:00
update.is_possibly_sensitive = possiblySensitive
2017-04-14 09:05:51 +02:00
update.draft_extras = update.updateStatusActionExtras()
2017-04-14 06:19:21 +02:00
return update
}
2017-04-08 16:06:04 +02:00
2016-07-02 05:54:53 +02:00
private fun updateTextCount() {
2017-04-04 08:15:37 +02:00
val editable = editText.editableText ?: return
val text = editable.toString()
2017-04-14 06:19:21 +02:00
val textAndMentions = getTwitterReplyTextAndMentions(text)
if (textAndMentions == null) {
hintLabel.visibility = View.GONE
editable.clearSpans(MentionColorSpan::class.java)
statusTextCount.textCount = validator.getTweetLength(text)
} else if (textAndMentions.replyToOriginalUser || replyToSelf) {
hintLabel.visibility = View.GONE
val mentionColor = ThemeUtils.getColorFromAttribute(this,
android.R.attr.textColorSecondary, 0)
editable.clearSpans(MentionColorSpan::class.java)
editable.setSpan(MentionColorSpan(mentionColor), 0, textAndMentions.replyStartIndex,
2017-04-04 08:15:37 +02:00
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
2017-04-03 18:20:59 +02:00
statusTextCount.textCount = validator.getTweetLength(textAndMentions.replyText)
} else {
2017-04-14 06:19:21 +02:00
hintLabel.visibility = View.VISIBLE
2017-04-04 08:15:37 +02:00
editable.clearSpans(MentionColorSpan::class.java)
2017-04-14 06:19:21 +02:00
statusTextCount.textCount = validator.getTweetLength(textAndMentions.replyText)
2017-04-03 18:20:59 +02:00
}
2016-07-02 05:54:53 +02:00
}
2017-03-25 14:44:07 +01:00
private fun updateUpdateStatusIcon() {
if (scheduleInfo != null) {
updateStatusIcon.setImageResource(R.drawable.ic_action_time)
} else {
updateStatusIcon.setImageResource(R.drawable.ic_action_send)
}
}
2017-04-14 10:10:15 +02:00
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 getTwitterReplyTextAndMentions(text: String = editText.text?.toString().orEmpty()):
ReplyTextAndMentions? {
val inReplyTo = inReplyToStatus ?: return null
if (!ignoreMentions) return null
return extractor.extractReplyTextAndMentions(text, inReplyTo)
2017-04-14 09:05:51 +02:00
}
private fun saveToDrafts(): Uri? {
val statusUpdate = try {
getStatusUpdate()
} 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
}
2017-04-14 10:10:15 +02:00
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()
}
}
}
2017-04-14 09:05:51 +02:00
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 = activity
val builder = AlertDialog.Builder(context)
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.setOnShowListener {
it as AlertDialog
it.applyTheme()
}
return dialog
}
}
class DirectMessageConfirmFragment : BaseDialogFragment(), DialogInterface.OnClickListener {
private val screenName: String get() = arguments.getString(EXTRA_SCREEN_NAME)
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 = activity
val builder = AlertDialog.Builder(context)
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.setOnShowListener {
it as AlertDialog
it.applyTheme()
}
return dialog
}
}
class AttachedMediaItemTouchHelperCallback(adapter: SimpleItemTouchHelperCallback.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 - Math.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 {
val ALPHA_FULL = 1.0f
}
}
private class ComposeLocationListener(activity: ComposeActivity) : LocationListener {
2016-07-02 05:54:53 +02:00
2017-02-28 08:34:00 +01:00
private val activityRef = WeakReference(activity)
2016-07-02 05:54:53 +02:00
override fun onLocationChanged(location: Location) {
2016-07-04 03:31:17 +02:00
val activity = activityRef.get() ?: return
2016-07-02 05:54:53 +02:00
activity.setRecentLocation(ParcelableLocationUtils.fromLocation(location))
}
override fun onStatusChanged(provider: String, status: Int, extras: Bundle) {
}
override fun onProviderEnabled(provider: String) {
}
override fun onProviderDisabled(provider: String) {
}
}
2017-04-14 09:05:51 +02:00
private class AccountIconViewHolder(val adapter: AccountIconsAdapter, itemView: View) : ViewHolder(itemView) {
2017-02-28 08:34:00 +01:00
private val iconView = itemView.findViewById(android.R.id.icon) as ShapedImageView
2016-07-02 05:54:53 +02:00
init {
2017-04-01 10:04:01 +02:00
itemView.setOnClickListener {
(itemView as CheckableLinearLayout).toggle()
adapter.toggleSelection(layoutPosition)
}
itemView.setOnLongClickListener {
(itemView as CheckableLinearLayout).toggle()
adapter.setSelection(layoutPosition)
return@setOnLongClickListener true
}
2016-07-02 05:54:53 +02:00
}
2016-12-04 04:58:03 +01:00
fun showAccount(adapter: AccountIconsAdapter, account: AccountDetails, isSelected: Boolean) {
2016-07-02 05:54:53 +02:00
itemView.alpha = if (isSelected) 1f else 0.33f
2017-03-02 07:59:19 +01:00
val context = adapter.context
adapter.requestManager.loadProfileImage(context, account, adapter.profileImageStyle).into(iconView)
2016-07-02 05:54:53 +02:00
iconView.setBorderColor(account.color)
}
}
2017-04-14 09:05:51 +02:00
private class AccountIconsAdapter(
2017-03-01 15:12:25 +01:00
private val activity: ComposeActivity
2017-03-02 07:59:19 +01:00
) : BaseRecyclerViewAdapter<AccountIconViewHolder>(activity, Glide.with(activity)) {
2017-01-07 07:16:02 +01:00
private val inflater: LayoutInflater = activity.layoutInflater
private val selection: MutableMap<UserKey, Boolean> = HashMap()
2016-07-02 05:54:53 +02:00
2016-12-04 04:58:03 +01:00
private var accounts: Array<AccountDetails>? = null
2016-07-02 05:54:53 +02:00
init {
setHasStableIds(true)
}
override fun getItemId(position: Int): Long {
2016-08-17 15:46:18 +02:00
return accounts!![position].hashCode().toLong()
2016-07-02 05:54:53 +02:00
}
val selectedAccountKeys: Array<UserKey>
get() {
2016-08-17 15:46:18 +02:00
val accounts = accounts ?: return emptyArray()
2016-12-04 04:58:03 +01:00
return accounts.filter { selection[it.key] ?: false }
.map { it.key }
2016-07-02 05:54:53 +02:00
.toTypedArray()
}
2017-04-12 14:58:08 +02:00
fun setSelectedAccountKeys(vararg accountKeys: UserKey) {
2016-08-17 15:46:18 +02:00
selection.clear()
2016-07-02 05:54:53 +02:00
for (accountKey in accountKeys) {
2016-08-17 15:46:18 +02:00
selection.put(accountKey, true)
2016-07-02 05:54:53 +02:00
}
notifyDataSetChanged()
}
2016-12-04 04:58:03 +01:00
val selectedAccounts: Array<AccountDetails>
2016-07-02 05:54:53 +02:00
get() {
2016-08-17 15:46:18 +02:00
val accounts = accounts ?: return emptyArray()
2016-12-04 04:58:03 +01:00
return accounts.filter { selection[it.key] ?: false }.toTypedArray()
2016-07-02 05:54:53 +02:00
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AccountIconViewHolder {
2017-01-07 07:16:02 +01:00
val view = inflater.inflate(R.layout.adapter_item_compose_account, parent, false)
2016-07-02 05:54:53 +02:00
return AccountIconViewHolder(this, view)
}
override fun onBindViewHolder(holder: AccountIconViewHolder, position: Int) {
2016-08-17 15:46:18 +02:00
val account = accounts!![position]
2016-12-04 04:58:03 +01:00
val isSelected = selection[account.key] ?: false
2016-07-02 05:54:53 +02:00
holder.showAccount(this, account, isSelected)
}
override fun getItemCount(): Int {
2016-08-17 15:46:18 +02:00
return if (accounts != null) accounts!!.size else 0
2016-07-02 05:54:53 +02:00
}
2016-12-04 04:58:03 +01:00
fun setAccounts(accounts: Array<AccountDetails>) {
2016-08-17 15:46:18 +02:00
this.accounts = accounts
2016-07-02 05:54:53 +02:00
notifyDataSetChanged()
}
2017-04-14 09:05:51 +02:00
fun toggleSelection(position: Int) {
2016-08-17 15:46:18 +02:00
if (accounts == null || position < 0) return
val account = accounts!![position]
2016-12-04 04:58:03 +01:00
selection.put(account.key, true != selection[account.key])
2016-07-07 09:39:32 +02:00
activity.notifyAccountSelectionChanged()
2016-07-02 05:54:53 +02:00
notifyDataSetChanged()
}
2017-04-01 10:04:01 +02:00
2017-04-14 09:05:51 +02:00
fun setSelection(position: Int) {
2017-04-01 10:04:01 +02:00
if (accounts == null || position < 0) return
val account = accounts!![position]
selection.clear()
selection.put(account.key, true != selection[account.key])
activity.notifyAccountSelectionChanged()
notifyDataSetChanged()
}
2016-07-02 05:54:53 +02:00
}
2017-04-14 09:05:51 +02:00
private class AddMediaTask(activity: ComposeActivity, sources: Array<Uri>, copySrc: Boolean,
deleteSrc: Boolean) : AbsAddMediaTask<ComposeActivity>(activity, sources, copySrc, deleteSrc) {
2016-07-02 05:54:53 +02:00
2017-02-14 13:32:15 +01:00
init {
callback = activity
2016-07-02 05:54:53 +02:00
}
2017-02-14 13:32:15 +01:00
override fun afterExecute(activity: ComposeActivity?, result: List<ParcelableMediaUpdate>?) {
if (activity == null || result == null) return
2016-07-02 05:54:53 +02:00
activity.setProgressVisible(false)
2017-02-14 13:32:15 +01:00
activity.addMedia(result)
2016-07-02 05:54:53 +02:00
activity.setMenu()
activity.updateTextCount()
}
2017-02-14 13:32:15 +01:00
override fun beforeExecute() {
val activity = this.callback ?: return
2016-07-02 05:54:53 +02:00
activity.setProgressVisible(true)
}
2017-02-14 13:32:15 +01:00
2016-07-02 05:54:53 +02:00
}
2017-04-14 09:05:51 +02:00
private class DeleteMediaTask(activity: ComposeActivity, val media: Array<ParcelableMediaUpdate>) :
AbsDeleteMediaTask<ComposeActivity>(activity, media.mapToArray { Uri.parse(it.uri) }) {
2016-07-02 05:54:53 +02:00
init {
2017-02-15 06:32:45 +01:00
this.callback = activity
2016-07-02 05:54:53 +02:00
}
2017-02-15 06:32:45 +01:00
override fun beforeExecute() {
callback?.setProgressVisible(true)
2016-07-02 05:54:53 +02:00
}
2017-02-15 06:32:45 +01:00
override fun afterExecute(callback: ComposeActivity?, result: BooleanArray?) {
if (callback == null || result == null) return
callback.setProgressVisible(false)
2017-04-14 09:05:51 +02:00
callback.removeMedia(media.filterIndexed { i, _ -> result[i] })
2017-02-15 06:32:45 +01:00
callback.setMenu()
if (result.any { false }) {
Toast.makeText(callback, R.string.message_toast_error_occurred, Toast.LENGTH_SHORT).show()
2016-07-02 05:54:53 +02:00
}
}
}
2017-04-14 09:05:51 +02:00
private class DisplayPlaceNameTask : AbstractTask<ParcelableLocation, List<Address>, ComposeActivity>() {
2016-07-02 05:54:53 +02:00
override fun doLongOperation(location: ParcelableLocation): List<Address>? {
try {
2017-04-03 18:53:12 +02:00
val activity = callback ?: throw IOException("Interrupted")
val gcd = Geocoder(activity, Locale.getDefault())
2016-07-02 05:54:53 +02:00
return gcd.getFromLocation(location.latitude, location.longitude, 1)
} catch (e: IOException) {
return null
}
}
override fun beforeExecute() {
val location = params
2017-04-03 18:53:12 +02:00
val activity = callback ?: return
val textView = activity.locationLabel ?: return
2016-07-02 05:54:53 +02:00
2017-04-03 18:53:12 +02:00
val preferences = activity.preferences
2016-12-13 04:06:07 +01:00
val attachLocation = preferences[attachLocationKey]
val attachPreciseLocation = preferences[attachPreciseLocationKey]
2016-07-02 05:54:53 +02:00
if (attachLocation) {
if (attachPreciseLocation) {
textView.text = ParcelableLocationUtils.getHumanReadableString(location, 3)
textView.tag = location
} else {
val tag = textView.tag
if (tag is Address) {
textView.text = tag.locality
} else if (tag is NoAddress) {
2017-01-26 14:28:43 +01:00
textView.setText(R.string.label_location_your_coarse_location)
2016-07-02 05:54:53 +02:00
} else {
textView.setText(R.string.getting_location)
}
}
} else {
textView.setText(R.string.no_location)
}
}
2017-04-03 18:53:12 +02:00
override fun afterExecute(activity: ComposeActivity?, addresses: List<Address>?) {
if (activity == null) return
val textView = activity.locationLabel ?: return
val preferences = activity.preferences
2016-12-13 04:06:07 +01:00
val attachLocation = preferences[attachLocationKey]
val attachPreciseLocation = preferences[attachPreciseLocationKey]
2016-07-02 05:54:53 +02:00
if (attachLocation) {
if (attachPreciseLocation) {
val location = params
2016-12-13 01:45:14 +01:00
textView.text = ParcelableLocationUtils.getHumanReadableString(location, 3)
2016-07-02 05:54:53 +02:00
textView.tag = location
} else if (addresses == null || addresses.isEmpty()) {
2016-12-13 01:45:14 +01:00
val tag = textView.tag
2016-07-02 05:54:53 +02:00
if (tag is Address) {
textView.text = tag.locality
} else {
2017-01-26 14:28:43 +01:00
textView.setText(R.string.label_location_your_coarse_location)
2016-07-02 05:54:53 +02:00
textView.tag = NoAddress()
}
} else {
val address = addresses[0]
2016-12-13 01:45:14 +01:00
textView.tag = address
2016-07-02 05:54:53 +02:00
textView.text = address.locality
}
} else {
2016-12-13 01:45:14 +01:00
textView.setText(R.string.no_location)
2016-07-02 05:54:53 +02:00
}
}
internal class NoAddress
}
private class ComposeEnterListener(private val activity: ComposeActivity?) : EnterListener {
override fun shouldCallListener(): Boolean {
2016-07-08 03:44:43 +02:00
return activity != null && activity.composeKeyMetaState == 0
2016-07-02 05:54:53 +02:00
}
override fun onHitEnter(): Boolean {
if (activity == null) return false
activity.confirmAndUpdateStatus()
return true
}
}
2017-04-04 06:37:44 +02:00
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 -> {
2017-04-06 18:09:07 +02:00
if (TwidereViewUtils.hitView(event, delegateView)) {
delegateTargeted = false
sendToDelegate = false
} else if (TwidereViewUtils.hitView(event, parentView)) {
2017-04-04 06:37:44 +02:00
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
}
}
2017-04-04 08:15:37 +02:00
private class MentionColorSpan(color: Int) : ForegroundColorSpan(color)
2017-04-14 09:05:51 +02:00
private open class ComposeException : Exception()
private class StatusTooLongException(val exceededStartIndex: Int) : ComposeException()
private class NoContentException : ComposeException()
private class NoAccountException : ComposeException()
2017-04-14 06:19:21 +02:00
2016-07-02 05:54:53 +02:00
companion object {
// Constants
private const val EXTRA_SHOULD_SAVE_ACCOUNTS = "should_save_accounts"
private const val EXTRA_ORIGINAL_TEXT = "original_text"
private const val EXTRA_DRAFT_UNIQUE_ID = "draft_unique_id"
private const val DISCARD_STATUS_DIALOG_FRAGMENT_TAG = "discard_status"
2016-07-02 05:54:53 +02:00
2016-12-13 04:06:07 +01:00
private const val REQUEST_ATTACH_LOCATION_PERMISSION = 301
2017-01-22 18:13:31 +01:00
private const val REQUEST_PICK_MEDIA_PERMISSION = 302
2017-03-29 14:48:17 +02:00
private const val REQUEST_TAKE_PHOTO_PERMISSION = 303
private const val REQUEST_CAPTURE_VIDEO_PERMISSION = 304
2017-03-29 17:15:28 +02:00
private const val REQUEST_SET_SCHEDULE = 305
private const val REQUEST_ADD_GIF = 306
2016-12-13 04:06:07 +01:00
2017-04-14 09:05:51 +02:00
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
2016-07-02 05:54:53 +02:00
}
}
}