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

544 lines
22 KiB
Kotlin

/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2015 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
import android.accounts.AccountManager
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.database.Cursor
import android.graphics.PorterDuff.Mode
import android.graphics.Rect
import android.net.Uri
import android.os.Bundle
import android.support.v4.app.LoaderManager.LoaderCallbacks
import android.support.v4.content.CursorLoader
import android.support.v4.content.Loader
import android.support.v4.widget.CursorAdapter
import android.text.Editable
import android.text.TextUtils
import android.text.TextWatcher
import android.view.*
import android.view.View.OnClickListener
import android.view.inputmethod.InputMethodManager
import android.widget.*
import android.widget.AdapterView.OnItemClickListener
import android.widget.AdapterView.OnItemSelectedListener
import com.bumptech.glide.Glide
import jopt.csp.util.SortableIntList
import kotlinx.android.synthetic.main.activity_quick_search_bar.*
import org.mariotaku.kpreferences.get
import org.mariotaku.ktextension.empty
import org.mariotaku.ktextension.spannable
import org.mariotaku.twidere.BuildConfig
import org.mariotaku.twidere.R
import org.mariotaku.twidere.TwidereConstants.QUERY_PARAM_ACCOUNT_KEY
import org.mariotaku.twidere.TwidereConstants.QUERY_PARAM_QUERY
import org.mariotaku.twidere.adapter.AccountsSpinnerAdapter
import org.mariotaku.twidere.annotation.Referral
import org.mariotaku.twidere.constant.IntentConstants.EXTRA_ACCOUNT_KEY
import org.mariotaku.twidere.constant.IntentConstants.EXTRA_QUERY
import org.mariotaku.twidere.constant.KeyboardShortcutConstants.ACTION_NAVIGATION_BACK
import org.mariotaku.twidere.constant.KeyboardShortcutConstants.CONTEXT_TAG_NAVIGATION
import org.mariotaku.twidere.constant.newDocumentApiKey
import org.mariotaku.twidere.constant.profileImageStyleKey
import org.mariotaku.twidere.extension.loadProfileImage
import org.mariotaku.twidere.model.AccountDetails
import org.mariotaku.twidere.model.SuggestionItem
import org.mariotaku.twidere.model.UserKey
import org.mariotaku.twidere.model.util.AccountUtils
import org.mariotaku.twidere.provider.TwidereDataStore.SearchHistory
import org.mariotaku.twidere.provider.TwidereDataStore.Suggestions
import org.mariotaku.twidere.util.*
import org.mariotaku.twidere.util.EditTextEnterHandler.EnterListener
import org.mariotaku.twidere.util.content.ContentResolverUtils
import org.mariotaku.twidere.view.ProfileImageView
import org.mariotaku.twidere.view.iface.IExtendedView.OnFitSystemWindowsListener
/**
* Created by mariotaku on 15/1/6.
*/
class QuickSearchBarActivity : BaseActivity(), OnClickListener, LoaderCallbacks<Cursor?>,
OnItemSelectedListener, OnItemClickListener, OnFitSystemWindowsListener,
SwipeDismissListViewTouchListener.DismissCallbacks {
private val systemWindowsInsets = Rect()
private var textChanged: Boolean = false
private var hasQrScanner: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
hasQrScanner = run {
val scanIntent = Intent(ACTION_ZXING_SCAN)
scanIntent.putExtra(EXTRA_ZXING_SCAN_MODE, ZXING_SCAN_MODE_QR_CODE)
return@run scanIntent.resolveActivity(packageManager) != null
}
setContentView(R.layout.activity_quick_search_bar)
val am = AccountManager.get(this)
val accounts = AccountUtils.getAllAccountDetails(am, AccountUtils.getAccounts(am), true).toList()
val accountsSpinnerAdapter = AccountsSpinnerAdapter(this, R.layout.spinner_item_account_icon,
requestManager = Glide.with(this))
accountsSpinnerAdapter.setDropDownViewResource(R.layout.list_item_simple_user)
accountsSpinnerAdapter.addAll(accounts)
accountSpinner.adapter = accountsSpinnerAdapter
accountSpinner.onItemSelectedListener = this
if (savedInstanceState == null) {
val intent = intent
val accountKey = intent.getParcelableExtra<UserKey>(EXTRA_ACCOUNT_KEY)
var index = -1
if (accountKey != null) {
index = accountsSpinnerAdapter.findPositionByKey(accountKey)
}
if (index != -1) {
accountSpinner.setSelection(index)
}
}
mainContent.setOnFitSystemWindowsListener(this)
suggestionsList.adapter = SuggestionsAdapter(this)
suggestionsList.onItemClickListener = this
val listener = SwipeDismissListViewTouchListener(suggestionsList, this)
suggestionsList.setOnTouchListener(listener)
suggestionsList.setOnScrollListener(listener.makeScrollListener())
searchSubmit.setOnClickListener(this)
EditTextEnterHandler.attach(searchQuery, object : EnterListener {
override fun shouldCallListener(): Boolean {
return true
}
override fun onHitEnter(): Boolean {
doSearch()
return true
}
}, true)
searchQuery.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) {
textChanged = true
updateSubmitButton()
}
override fun afterTextChanged(s: Editable) {
supportLoaderManager.restartLoader(0, null, this@QuickSearchBarActivity)
}
})
if (savedInstanceState == null) {
searchQuery.setText(intent.getStringExtra(EXTRA_QUERY))
searchQuery.setSelection(searchQuery.length())
}
supportLoaderManager.initLoader(0, null, this)
updateSubmitButton()
}
override fun canDismiss(position: Int): Boolean {
val adapter = suggestionsList.adapter as SuggestionsAdapter
return adapter.getItemViewType(position) == SuggestionsAdapter.VIEW_TYPE_SEARCH_HISTORY
}
override fun onDismiss(listView: ListView, reverseSortedPositions: IntArray) {
val adapter = suggestionsList.adapter as SuggestionsAdapter
val ids = LongArray(reverseSortedPositions.size)
for (i in 0 until reverseSortedPositions.size) {
val position = reverseSortedPositions[i]
val item = adapter.getSuggestionItem(position) ?: return
ids[i] = item._id
}
adapter.addRemovedPositions(reverseSortedPositions)
ContentResolverUtils.bulkDelete(contentResolver, SearchHistory.CONTENT_URI, SearchHistory._ID,
false, ids, null, null)
supportLoaderManager.restartLoader(0, null, this)
}
override fun onClick(v: View) {
when (v) {
searchSubmit -> {
if (searchQuery.empty && hasQrScanner) {
val currentFocus = currentFocus
if (currentFocus === searchQuery) {
val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(currentFocus.windowToken, 0)
currentFocus.clearFocus()
}
val scanIntent = Intent(ACTION_ZXING_SCAN)
scanIntent.putExtra(EXTRA_ZXING_SCAN_MODE, ZXING_SCAN_MODE_QR_CODE)
try {
startActivityForResult(scanIntent, REQUEST_SCAN_QR)
} catch (e: ActivityNotFoundException) {
// Ignore
Toast.makeText(this, R.string.message_toast_qr_scanner_not_supported,
Toast.LENGTH_SHORT).show()
} catch (e: SecurityException) {
// Goddamned SAMSUNG again!!!
Toast.makeText(this, R.string.message_toast_qr_scanner_not_supported,
Toast.LENGTH_SHORT).show()
}
} else {
doSearch()
}
}
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when (requestCode) {
REQUEST_SCAN_QR -> {
if (resultCode == Activity.RESULT_OK && data != null) {
val scanResult = data.getStringExtra(EXTRA_ZXING_SCAN_RESULT) ?: run {
Toast.makeText(this, R.string.message_toast_qr_scanner_not_supported,
Toast.LENGTH_SHORT).show()
return
}
val viewIntent = Intent(Intent.ACTION_VIEW, Uri.parse(scanResult)).apply {
`package` = BuildConfig.APPLICATION_ID
putExtra(EXTRA_ACCOUNT_KEY, selectedAccountDetails?.key)
}
val componentName = viewIntent.resolveActivity(packageManager) ?: run {
Toast.makeText(this, R.string.message_toast_qr_scan_link_not_supported,
Toast.LENGTH_SHORT).show()
return
}
viewIntent.component = componentName
startActivity(viewIntent)
finish()
}
}
}
}
override fun onCreateLoader(id: Int, args: Bundle?): Loader<Cursor?> {
val accountKey = selectedAccountDetails?.key
val builder = Suggestions.Search.CONTENT_URI.buildUpon()
builder.appendQueryParameter(QUERY_PARAM_QUERY, ParseUtils.parseString(searchQuery.text))
if (accountKey != null) {
builder.appendQueryParameter(QUERY_PARAM_ACCOUNT_KEY, accountKey.toString())
}
return CursorLoader(this, builder.build(), Suggestions.Search.COLUMNS, null, null, null)
}
override fun onLoadFinished(loader: Loader<Cursor?>, data: Cursor?) {
val adapter = suggestionsList.adapter as SuggestionsAdapter
adapter.changeCursor(data)
}
override fun onLoaderReset(loader: Loader<Cursor?>) {
val adapter = suggestionsList.adapter as SuggestionsAdapter
adapter.changeCursor(null)
}
override fun onFitSystemWindows(insets: Rect) {
systemWindowsInsets.set(insets)
updateWindowAttributes()
}
override fun onItemClick(parent: AdapterView<*>, view: View, position: Int, id: Long) {
val adapter = suggestionsList.adapter as SuggestionsAdapter
val item = adapter.getSuggestionItem(position) ?: return
val details = selectedAccountDetails ?: return
when (adapter.getItemViewType(position)) {
SuggestionsAdapter.VIEW_TYPE_USER_SUGGESTION_ITEM -> {
IntentUtils.openUserProfile(this, details.key,
UserKey.valueOf(item.extra_id!!), item.summary, null,
preferences[newDocumentApiKey], Referral.DIRECT, null)
finish()
}
SuggestionsAdapter.VIEW_TYPE_USER_SCREEN_NAME -> {
IntentUtils.openUserProfile(this, details.key, null, item.title,
null, preferences[newDocumentApiKey], Referral.DIRECT, null)
finish()
}
SuggestionsAdapter.VIEW_TYPE_SAVED_SEARCH, SuggestionsAdapter.VIEW_TYPE_SEARCH_HISTORY -> {
val query = item.title ?: return
IntentUtils.openSearch(this, details.key, query)
setResult(RESULT_SEARCH_PERFORMED)
finish()
}
}
}
override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) {
supportLoaderManager.restartLoader(0, null, this)
}
override fun onNothingSelected(parent: AdapterView<*>) {
}
override fun handleKeyboardShortcutSingle(handler: KeyboardShortcutsHandler, keyCode: Int, event: KeyEvent, metaState: Int): Boolean {
val action = handler.getKeyAction(CONTEXT_TAG_NAVIGATION, keyCode, event, metaState)
if (ACTION_NAVIGATION_BACK == action && searchQuery.length() == 0) {
if (!textChanged) {
onBackPressed()
} else {
textChanged = false
}
return true
}
return super.handleKeyboardShortcutSingle(handler, keyCode, event, metaState)
}
override fun onResume() {
super.onResume()
updateWindowAttributes()
}
private fun doSearch() {
if (isFinishing) return
val query = ParseUtils.parseString(searchQuery.text)
if (TextUtils.isEmpty(query)) return
val details = selectedAccountDetails ?: return
IntentUtils.openSearch(this, details.key, query)
setResult(RESULT_SEARCH_PERFORMED)
finish()
}
private val selectedAccountDetails: AccountDetails?
get() {
return accountSpinner.selectedItem as? AccountDetails
}
private fun updateWindowAttributes() {
val window = window
val attributes = window.attributes
attributes.gravity = Gravity.TOP or Gravity.CENTER_HORIZONTAL
attributes.y = systemWindowsInsets.top
window.attributes = attributes
}
private fun setSearchQueryText(query: String?) {
searchQuery.setText(query)
if (query == null) return
searchQuery.setSelection(query.length)
}
private fun updateSubmitButton() {
if (searchQuery.empty && hasQrScanner) {
searchSubmit.setImageResource(R.drawable.ic_action_qr_scan)
} else {
searchSubmit.setImageResource(R.drawable.ic_action_search)
}
}
class SuggestionsAdapter internal constructor(
private val activity: QuickSearchBarActivity
) : CursorAdapter(activity, null, 0), OnClickListener {
private val profileImageStyle = activity.preferences[profileImageStyleKey]
private val profileImageSize = activity.getString(R.string.profile_image_size)
private val requestManager = Glide.with(activity)
private val inflater = LayoutInflater.from(activity)
private val userColorNameManager = activity.userColorNameManager
private val removedPositions = SortableIntList()
private var indices: SuggestionItem.Indices? = null
override fun newView(context: Context, cursor: Cursor, parent: ViewGroup): View {
when (getActualItemViewType(cursor.position)) {
VIEW_TYPE_SEARCH_HISTORY, VIEW_TYPE_SAVED_SEARCH -> {
val view = inflater.inflate(R.layout.list_item_suggestion_search, parent, false)
val holder = SearchViewHolder(view)
holder.edit_query.setOnClickListener(this)
view.tag = holder
return view
}
VIEW_TYPE_USER_SUGGESTION_ITEM, VIEW_TYPE_USER_SCREEN_NAME -> {
val view = inflater.inflate(R.layout.list_item_suggestion_user, parent, false)
view.tag = UserViewHolder(view).apply {
icon.style = profileImageStyle
}
return view
}
}
throw UnsupportedOperationException("Unknown viewType")
}
internal fun getSuggestionItem(position: Int): SuggestionItem? {
val cursor = (getItem(position) ?: return null) as Cursor
val indices = indices ?: return null
return SuggestionItem(cursor, indices)
}
override fun bindView(view: View, context: Context, cursor: Cursor) {
val indices = indices!!
when (getActualItemViewType(cursor.position)) {
VIEW_TYPE_SEARCH_HISTORY -> {
val holder = view.tag as SearchViewHolder
val title = cursor.getString(indices.title)
holder.edit_query.tag = title
holder.text1.spannable = title
holder.icon.setImageResource(R.drawable.ic_action_history)
}
VIEW_TYPE_SAVED_SEARCH -> {
val holder = view.tag as SearchViewHolder
val title = cursor.getString(indices.title)
holder.edit_query.tag = title
holder.text1.spannable = title
holder.icon.setImageResource(R.drawable.ic_action_save)
}
VIEW_TYPE_USER_SUGGESTION_ITEM -> {
val holder = view.tag as UserViewHolder
val userKey = UserKey.valueOf(cursor.getString(indices.extra_id))
holder.text1.spannable = userColorNameManager.getUserNickname(userKey,
cursor.getString(indices.title))
holder.text2.visibility = View.VISIBLE
holder.text2.spannable = "@${cursor.getString(indices.summary)}"
holder.icon.clearColorFilter()
requestManager.loadProfileImage(context, cursor.getString(indices.icon),
profileImageStyle, cornerRadius = holder.icon.cornerRadius,
cornerRadiusRatio = holder.icon.cornerRadiusRatio,
size = profileImageSize).into(holder.icon)
}
VIEW_TYPE_USER_SCREEN_NAME -> {
val holder = view.tag as UserViewHolder
holder.text1.spannable = "@${cursor.getString(indices.title)}"
holder.text2.visibility = View.GONE
holder.icon.setColorFilter(holder.text1.currentTextColor, Mode.SRC_ATOP)
//TODO cancel image load
holder.icon.setImageResource(R.drawable.ic_action_user)
}
}
}
override fun getItemViewType(position: Int): Int {
return getActualItemViewType(getActualPosition(position))
}
fun getActualItemViewType(position: Int): Int {
val cursor = super.getItem(position) as Cursor
if (indices == null) throw NullPointerException()
when (cursor.getString(indices!!.type)) {
Suggestions.Search.TYPE_SAVED_SEARCH -> {
return VIEW_TYPE_SAVED_SEARCH
}
Suggestions.Search.TYPE_SCREEN_NAME -> {
return VIEW_TYPE_USER_SCREEN_NAME
}
Suggestions.Search.TYPE_SEARCH_HISTORY -> {
return VIEW_TYPE_SEARCH_HISTORY
}
Suggestions.Search.TYPE_USER -> {
return VIEW_TYPE_USER_SUGGESTION_ITEM
}
}
return Adapter.IGNORE_ITEM_VIEW_TYPE
}
override fun getViewTypeCount(): Int {
return 4
}
override fun onClick(v: View) {
when (v.id) {
R.id.edit_query -> {
activity.setSearchQueryText(v.tag as String)
}
}
}
override fun swapCursor(newCursor: Cursor?): Cursor? {
indices = newCursor?.let(SuggestionItem::Indices)
return super.swapCursor(newCursor)
}
override fun getCount(): Int {
return super.getCount() - removedPositions.size()
}
override fun getItem(position: Int): Any? {
return super.getItem(getActualPosition(position))
}
override fun getItemId(position: Int): Long {
return super.getItemId(getActualPosition(position))
}
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
return super.getView(getActualPosition(position), convertView, parent)
}
override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View {
return super.getDropDownView(getActualPosition(position), convertView, parent)
}
private fun getActualPosition(position: Int): Int {
var skipped = 0
for (i in 0 until removedPositions.size()) {
if (position + skipped >= removedPositions.get(i)) {
skipped++
}
}
return position + skipped
}
fun addRemovedPositions(positions: IntArray) {
for (position in positions) {
removedPositions.add(getActualPosition(position))
}
removedPositions.sort()
notifyDataSetChanged()
}
internal class SearchViewHolder(view: View) {
internal val icon = view.findViewById(android.R.id.icon) as ImageView
internal val text1 = view.findViewById(android.R.id.text1) as TextView
internal val edit_query = view.findViewById(R.id.edit_query)
}
internal class UserViewHolder(view: View) {
internal val icon = view.findViewById(android.R.id.icon) as ProfileImageView
internal val text1 = view.findViewById(android.R.id.text1) as TextView
internal val text2 = view.findViewById(android.R.id.text2) as TextView
}
companion object {
internal val VIEW_TYPE_SEARCH_HISTORY = 0
internal val VIEW_TYPE_SAVED_SEARCH = 1
internal val VIEW_TYPE_USER_SUGGESTION_ITEM = 2
internal val VIEW_TYPE_USER_SCREEN_NAME = 3
}
}
companion object {
const val ACTION_ZXING_SCAN = "com.google.zxing.client.android.SCAN"
const val EXTRA_ZXING_SCAN_MODE = "SCAN_MODE"
const val EXTRA_ZXING_SCAN_RESULT = "SCAN_RESULT"
const val ZXING_SCAN_MODE_QR_CODE = "QR_CODE_MODE"
const val REQUEST_SCAN_QR = 101
const val RESULT_SEARCH_PERFORMED = 2
}
}