/* * Twidere - Twitter client for Android * * Copyright (C) 2012-2015 Mariotaku Lee * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package org.mariotaku.twidere.activity import android.content.Context import android.database.Cursor import android.graphics.PorterDuff.Mode import android.graphics.Rect 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.widget.* import android.widget.AdapterView.OnItemClickListener import android.widget.AdapterView.OnItemSelectedListener import jopt.csp.util.SortableIntList import kotlinx.android.synthetic.main.activity_quick_search_bar.* 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.KeyboardShortcutConstants.ACTION_NAVIGATION_BACK import org.mariotaku.twidere.constant.KeyboardShortcutConstants.CONTEXT_TAG_NAVIGATION import org.mariotaku.twidere.constant.SharedPreferenceConstants.KEY_NEW_DOCUMENT_API import org.mariotaku.twidere.model.ParcelableAccount import org.mariotaku.twidere.model.UserKey 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.iface.IExtendedView.OnFitSystemWindowsListener /** * Created by mariotaku on 15/1/6. */ class QuickSearchBarActivity : BaseActivity(), OnClickListener, LoaderCallbacks, OnItemSelectedListener, OnItemClickListener, OnFitSystemWindowsListener, SwipeDismissListViewTouchListener.DismissCallbacks { private val mSystemWindowsInsets = Rect() private var textChanged: Boolean = false 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) var i = 0 val j = reverseSortedPositions.size while (i < j) { val position = reverseSortedPositions[i] val item = adapter.getSuggestionItem(position) ?: return ids[i] = item._id i++ } adapter.addRemovedPositions(reverseSortedPositions) ContentResolverUtils.bulkDelete(contentResolver, SearchHistory.CONTENT_URI, SearchHistory._ID, ids, null) supportLoaderManager.restartLoader(0, null, this) } override fun onClick(v: View) { when (v) { searchSubmit -> { doSearch() } } } override fun onCreateLoader(id: Int, args: Bundle?): Loader { val accountId = selectedAccountKey val builder = Suggestions.Search.CONTENT_URI.buildUpon() builder.appendQueryParameter(QUERY_PARAM_QUERY, ParseUtils.parseString(searchQuery.text)) if (accountId != null) { builder.appendQueryParameter(QUERY_PARAM_ACCOUNT_KEY, accountId.toString()) } return CursorLoader(this, builder.build(), Suggestions.Search.COLUMNS, null, null, null) } override fun onLoadFinished(loader: Loader, data: Cursor?) { val adapter = suggestionsList.adapter as SuggestionsAdapter adapter.changeCursor(data) } override fun onLoaderReset(loader: Loader) { val adapter = suggestionsList.adapter as SuggestionsAdapter adapter.changeCursor(null) } override fun onFitSystemWindows(insets: Rect) { mSystemWindowsInsets.set(insets) updateWindowAttributes() } override fun onItemClick(parent: AdapterView<*>, view: View, position: Int, id: Long) { val adapter = (suggestionsList.adapter ?: return) as SuggestionsAdapter val item = adapter.getSuggestionItem(position) when (adapter.getItemViewType(position)) { SuggestionsAdapter.VIEW_TYPE_USER_SUGGESTION_ITEM -> { IntentUtils.openUserProfile(this, selectedAccountKey, UserKey.valueOf(item!!.extra_id), item.summary, null, preferences.getBoolean(KEY_NEW_DOCUMENT_API), Referral.DIRECT) finish() } SuggestionsAdapter.VIEW_TYPE_USER_SCREEN_NAME -> { IntentUtils.openUserProfile(this, selectedAccountKey, null, item!!.title, null, preferences.getBoolean(KEY_NEW_DOCUMENT_API), Referral.DIRECT) finish() } SuggestionsAdapter.VIEW_TYPE_SAVED_SEARCH, SuggestionsAdapter.VIEW_TYPE_SEARCH_HISTORY -> { IntentUtils.openSearch(this, selectedAccountKey, item!!.title) 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 onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_quick_search_bar) val accounts = DataStoreUtils.getCredentialsList(this, false) val accountsSpinnerAdapter = AccountsSpinnerAdapter(this, R.layout.spinner_item_account_icon) accountsSpinnerAdapter.setDropDownViewResource(R.layout.list_item_user) accountsSpinnerAdapter.addAll(accounts) accountSpinner.adapter = accountsSpinnerAdapter accountSpinner.onItemSelectedListener = this if (savedInstanceState == null) { val intent = intent val accountKey = intent.getParcelableExtra(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 } override fun afterTextChanged(s: Editable) { supportLoaderManager.restartLoader(0, null, this@QuickSearchBarActivity) } }) supportLoaderManager.initLoader(0, null, this) } override fun onResume() { super.onResume() updateWindowAttributes() } private fun doSearch() { if (isFinishing) return val query = ParseUtils.parseString(searchQuery.text) if (TextUtils.isEmpty(query)) return IntentUtils.openSearch(this, selectedAccountKey, query) finish() } private val selectedAccountKey: UserKey? get() { val account = accountSpinner.selectedItem as ParcelableAccount ?: return null return account.account_key } private fun updateWindowAttributes() { val window = window val attributes = window.attributes attributes.gravity = Gravity.TOP or Gravity.CENTER_HORIZONTAL attributes.y = mSystemWindowsInsets.top window.attributes = attributes } private fun setSearchQueryText(query: String?) { searchQuery.setText(query) if (query == null) return searchQuery.setSelection(query.length) } internal class SuggestionItem(cursor: Cursor, indices: SuggestionsAdapter.Indices) { val title: String val summary: String val _id: Long val extra_id: String init { _id = cursor.getLong(indices._id) title = cursor.getString(indices.title) summary = cursor.getString(indices.summary) extra_id = cursor.getString(indices.extra_id) } } class SuggestionsAdapter internal constructor( private val activity: QuickSearchBarActivity ) : CursorAdapter(activity, null, 0), OnClickListener { private val mInflater: LayoutInflater private val mMediaLoader: MediaLoaderWrapper private val mUserColorNameManager: UserColorNameManager private val removedPositions: SortableIntList? private var indices: Indices? = null init { removedPositions = SortableIntList() mMediaLoader = activity.mediaLoader mUserColorNameManager = activity.userColorNameManager mInflater = LayoutInflater.from(activity) } override fun newView(context: Context, cursor: Cursor, parent: ViewGroup): View { when (getActualItemViewType(cursor.position)) { VIEW_TYPE_SEARCH_HISTORY, VIEW_TYPE_SAVED_SEARCH -> { val view = mInflater.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 = mInflater.inflate(R.layout.list_item_suggestion_user, parent, false) view.tag = UserViewHolder(view) return view } } throw UnsupportedOperationException("Unknown viewType") } internal fun getSuggestionItem(position: Int): SuggestionItem? { val cursor = getItem(position) as Cursor? ?: return null 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.text = 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.text = 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.text = mUserColorNameManager.getUserNickname(userKey, cursor.getString(indices.title)) holder.text2.visibility = View.VISIBLE holder.text2.text = "@${cursor.getString(indices.summary)}" holder.icon.clearColorFilter() mMediaLoader.displayProfileImage(holder.icon, cursor.getString(indices.icon)) } VIEW_TYPE_USER_SCREEN_NAME -> { val holder = view.tag as UserViewHolder holder.text1.text = "@${cursor.getString(indices.title)}" holder.text2.visibility = View.GONE holder.icon.setColorFilter(holder.text1.currentTextColor, Mode.SRC_ATOP) mMediaLoader.cancelDisplayTask(holder.icon) 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 = if (newCursor != null) Indices(newCursor) else null removedPositions!!.clear() return super.swapCursor(newCursor) } override fun getCount(): Int { if (removedPositions == null) return super.getCount() 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 { if (removedPositions == null) return position var skipped = 0 var i = 0 val j = removedPositions.size() while (i < j) { if (position + skipped >= removedPositions.get(i)) { skipped++ } i++ } 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: ImageView internal val text1: TextView internal val edit_query: View init { icon = view.findViewById(android.R.id.icon) as ImageView text1 = view.findViewById(android.R.id.text1) as TextView edit_query = view.findViewById(R.id.edit_query) } } internal class UserViewHolder(view: View) { internal val icon: ImageView internal val text1: TextView internal val text2: TextView init { icon = view.findViewById(android.R.id.icon) as ImageView text1 = view.findViewById(android.R.id.text1) as TextView text2 = view.findViewById(android.R.id.text2) as TextView } } internal class Indices(cursor: Cursor) { internal val _id: Int internal val type: Int internal val title: Int internal val summary: Int internal val icon: Int internal val extra_id: Int init { _id = cursor.getColumnIndex(Suggestions._ID) type = cursor.getColumnIndex(Suggestions.TYPE) title = cursor.getColumnIndex(Suggestions.TITLE) summary = cursor.getColumnIndex(Suggestions.SUMMARY) icon = cursor.getColumnIndex(Suggestions.ICON) extra_id = cursor.getColumnIndex(Suggestions.EXTRA_ID) } } 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 } } }