improved users selector

This commit is contained in:
Mariotaku Lee 2017-01-30 11:29:09 +08:00
parent 3129a25c55
commit 2c1b8b37f2
No known key found for this signature in database
GPG Key ID: 15C10F89D7C33535
11 changed files with 206 additions and 230 deletions

View File

@ -208,4 +208,5 @@ public interface IntentConstants {
String EXTRA_API_CONFIG = "api_config";
String EXTRA_COUNT = "count";
String EXTRA_REQUEST_CODE = "request_code";
String EXTRA_FROM_CACHE = "from_cache";
}

View File

@ -377,8 +377,9 @@
</activity>
<activity
android:name=".activity.UserSelectorActivity"
android:label="@string/select_user_list"
android:theme="@style/Theme.Twidere.Dialog">
android:label="@string/select_user"
android:theme="@style/Theme.Twidere.Dialog"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="org.mariotaku.twidere.SELECT_USER"/>
@ -387,8 +388,9 @@
</activity>
<activity
android:name=".activity.UserListSelectorActivity"
android:label="@string/select_user"
android:theme="@style/Theme.Twidere.Dialog">
android:label="@string/select_user_list"
android:theme="@style/Theme.Twidere.Dialog"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="org.mariotaku.twidere.SELECT_USER_LIST"/>

View File

@ -24,6 +24,7 @@ import android.content.Intent
import android.os.AsyncTask
import android.os.Bundle
import android.support.v4.app.DialogFragment
import android.support.v4.app.LoaderManager
import android.text.TextUtils
import android.text.TextUtils.isEmpty
import android.util.Log
@ -43,6 +44,7 @@ import org.mariotaku.twidere.adapter.UserAutoCompleteAdapter
import org.mariotaku.twidere.constant.IntentConstants.*
import org.mariotaku.twidere.fragment.CreateUserListDialogFragment
import org.mariotaku.twidere.fragment.ProgressDialogFragment
import org.mariotaku.twidere.model.ParcelableUser
import org.mariotaku.twidere.model.ParcelableUserList
import org.mariotaku.twidere.model.SingleResponse
import org.mariotaku.twidere.model.UserKey

View File

@ -21,83 +21,63 @@ package org.mariotaku.twidere.activity
import android.app.Activity
import android.content.Intent
import android.os.AsyncTask
import android.os.Bundle
import android.support.v4.app.DialogFragment
import android.support.v4.app.LoaderManager
import android.support.v4.content.Loader
import android.text.TextUtils.isEmpty
import android.util.Log
import android.view.View
import android.view.View.OnClickListener
import android.widget.AdapterView
import android.widget.AdapterView.OnItemClickListener
import android.widget.ListView
import kotlinx.android.synthetic.main.activity_user_selector.*
import org.mariotaku.microblog.library.MicroBlogException
import org.mariotaku.microblog.library.twitter.model.Paging
import kotlinx.android.synthetic.main.layout_list_with_empty_view.*
import org.mariotaku.ktextension.Bundle
import org.mariotaku.ktextension.isNotNullOrEmpty
import org.mariotaku.ktextension.set
import org.mariotaku.twidere.R
import org.mariotaku.twidere.TwidereConstants.LOGTAG
import org.mariotaku.twidere.adapter.SimpleParcelableUsersAdapter
import org.mariotaku.twidere.adapter.UserAutoCompleteAdapter
import org.mariotaku.twidere.constant.IntentConstants.*
import org.mariotaku.twidere.fragment.CreateUserListDialogFragment
import org.mariotaku.twidere.fragment.ProgressDialogFragment
import org.mariotaku.twidere.loader.CacheUserSearchLoader
import org.mariotaku.twidere.model.ParcelableUser
import org.mariotaku.twidere.model.SingleResponse
import org.mariotaku.twidere.model.UserKey
import org.mariotaku.twidere.model.util.ParcelableUserUtils
import org.mariotaku.twidere.util.AsyncTaskUtils
import org.mariotaku.twidere.util.MicroBlogAPIFactory
import org.mariotaku.twidere.util.ParseUtils
import java.util.*
import org.mariotaku.twidere.util.view.SimpleTextWatcher
class UserSelectorActivity : BaseActivity(), OnClickListener, OnItemClickListener {
class UserSelectorActivity : BaseActivity(), OnItemClickListener, LoaderManager.LoaderCallbacks<List<ParcelableUser>> {
private lateinit var usersAdapter: SimpleParcelableUsersAdapter
private var screenName: String? = null
private var loaderInitialized: Boolean = false
private val accountKey: UserKey?
get() = intent.getParcelableExtra<UserKey>(EXTRA_ACCOUNT_KEY)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val intent = intent
if (!intent.hasExtra(EXTRA_ACCOUNT_KEY)) {
val accountKey = this.accountKey ?: run {
finish()
return
}
setContentView(R.layout.activity_user_selector)
editScreenName.addTextChangedListener(object : SimpleTextWatcher() {
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
searchUser(accountKey, s.toString(), true)
}
})
screenNameConfirm.setOnClickListener {
val screenName = ParseUtils.parseString(editScreenName.text)
searchUser(accountKey, screenName, false)
}
if (savedInstanceState == null) {
screenName = intent.getStringExtra(EXTRA_SCREEN_NAME)
} else {
screenName = savedInstanceState.getString(EXTRA_SCREEN_NAME)
editScreenName.setText(intent.getStringExtra(EXTRA_SCREEN_NAME))
}
if (!isEmpty(screenName)) {
searchUser(screenName!!)
}
val adapter = UserAutoCompleteAdapter(this)
adapter.accountKey = accountKey
editScreenName.setAdapter(adapter)
editScreenName.setText(screenName)
usersAdapter = SimpleParcelableUsersAdapter(this)
usersList.adapter = usersAdapter
usersList.onItemClickListener = this
screenNameConfirm.setOnClickListener(this)
}
listView.adapter = usersAdapter
listView.onItemClickListener = this
override fun onClick(v: View) {
when (v.id) {
R.id.screenNameConfirm -> {
val screen_name = ParseUtils.parseString(editScreenName.text)
if (isEmpty(screen_name)) return
searchUser(screen_name)
}
R.id.createList -> {
val f = CreateUserListDialogFragment()
val args = Bundle()
args.putParcelable(EXTRA_ACCOUNT_KEY, accountKey)
f.arguments = args
f.show(supportFragmentManager, null)
}
}
showSearchHint()
}
override fun onItemClick(view: AdapterView<*>, child: View, position: Int, id: Long) {
@ -110,93 +90,79 @@ class UserSelectorActivity : BaseActivity(), OnClickListener, OnItemClickListene
finish()
}
fun setUsersData(data: List<ParcelableUser>) {
override fun onCreateLoader(id: Int, args: Bundle): Loader<List<ParcelableUser>> {
val accountKey = args.getParcelable<UserKey>(EXTRA_ACCOUNT_KEY)
val query = args.getString(EXTRA_QUERY)
val fromCache = args.getBoolean(EXTRA_FROM_CACHE)
if (!fromCache) {
showProgress()
}
return CacheUserSearchLoader(this, accountKey, query, fromCache, true)
}
override fun onLoaderReset(loader: Loader<List<ParcelableUser>>) {
usersAdapter.setData(null, true)
}
override fun onLoadFinished(loader: Loader<List<ParcelableUser>>, data: List<ParcelableUser>?) {
progressContainer.visibility = View.GONE
listContainer.visibility = View.VISIBLE
usersAdapter.setData(data, true)
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putString(EXTRA_SCREEN_NAME, screenName)
}
private val accountKey: UserKey
get() = intent.getParcelableExtra<UserKey>(EXTRA_ACCOUNT_KEY)
private fun searchUser(name: String) {
val task = SearchUsersTask(this, accountKey, name)
AsyncTaskUtils.executeTask(task)
}
private fun dismissDialogFragment(tag: String) {
executeAfterFragmentResumed { activity ->
val fm = activity.supportFragmentManager
val f = fm.findFragmentByTag(tag)
if (f is DialogFragment) {
f.dismiss()
}
loader as CacheUserSearchLoader
if (data.isNotNullOrEmpty()) {
showList()
} else if (loader.query.isEmpty()) {
showSearchHint()
} else {
showNotFound()
}
}
private fun showDialogFragment(df: DialogFragment, tag: String) {
executeAfterFragmentResumed { activity ->
df.show(activity.supportFragmentManager, tag)
private fun searchUser(accountKey: UserKey, query: String, fromCache: Boolean) {
if (isEmpty(query)) {
showSearchHint()
return
}
val args = Bundle {
this[EXTRA_ACCOUNT_KEY] = accountKey
this[EXTRA_QUERY] = query
this[EXTRA_FROM_CACHE] = fromCache
}
if (loaderInitialized) {
supportLoaderManager.initLoader(0, args, this)
loaderInitialized = true
} else {
supportLoaderManager.restartLoader(0, args, this)
}
}
override fun onStart() {
super.onStart()
bus.register(this)
private fun showProgress() {
progressContainer.visibility = View.VISIBLE
listContainer.visibility = View.GONE
}
override fun onStop() {
bus.unregister(this)
super.onStop()
private fun showSearchHint() {
progressContainer.visibility = View.GONE
listContainer.visibility = View.VISIBLE
emptyView.visibility = View.VISIBLE
listView.visibility = View.GONE
emptyIcon.setImageResource(R.drawable.ic_info_search)
emptyText.text = getText(R.string.search_hint_users)
}
private class SearchUsersTask(
private val activity: UserSelectorActivity,
private val accountKey: UserKey,
private val name: String
) : AsyncTask<Any, Any, SingleResponse<List<ParcelableUser>>>() {
override fun doInBackground(vararg params: Any): SingleResponse<List<ParcelableUser>> {
val twitter = MicroBlogAPIFactory.getInstance(activity, accountKey) ?: return SingleResponse.getInstance<List<ParcelableUser>>()
try {
val paging = Paging()
val lists = twitter.searchUsers(name, paging)
val data = ArrayList<ParcelableUser>()
for (item in lists) {
data.add(ParcelableUserUtils.fromUser(item, accountKey))
}
return SingleResponse.getInstance<List<ParcelableUser>>(data)
} catch (e: MicroBlogException) {
Log.w(LOGTAG, e)
return SingleResponse.getInstance<List<ParcelableUser>>(e)
}
}
override fun onPostExecute(result: SingleResponse<List<ParcelableUser>>) {
activity.dismissDialogFragment(FRAGMENT_TAG_SEARCH_USERS)
if (result.data != null) {
activity.setUsersData(result.data)
}
}
override fun onPreExecute() {
val df = ProgressDialogFragment()
df.isCancelable = false
activity.showDialogFragment(df, FRAGMENT_TAG_SEARCH_USERS)
}
companion object {
private const val FRAGMENT_TAG_SEARCH_USERS = "search_users"
}
private fun showNotFound() {
progressContainer.visibility = View.GONE
listContainer.visibility = View.VISIBLE
emptyView.visibility = View.VISIBLE
listView.visibility = View.GONE
emptyIcon.setImageResource(R.drawable.ic_info_search)
emptyText.text = getText(R.string.search_hint_users)
}
private fun showList() {
progressContainer.visibility = View.GONE
listContainer.visibility = View.VISIBLE
listView.visibility = View.VISIBLE
emptyView.visibility = View.GONE
}
}

View File

@ -53,7 +53,6 @@ import com.squareup.otto.Subscribe
import kotlinx.android.synthetic.main.fragment_messages_conversation.*
import kotlinx.android.synthetic.main.layout_actionbar_message_user_picker.view.*
import me.uucky.colorpicker.internal.EffectViewHelper
import org.mariotaku.sqliteqb.library.Columns.Column
import org.mariotaku.sqliteqb.library.Expression
import org.mariotaku.sqliteqb.library.OrderBy
import org.mariotaku.twidere.R
@ -67,12 +66,14 @@ import org.mariotaku.twidere.annotation.CustomTabType
import org.mariotaku.twidere.constant.KeyboardShortcutConstants.ACTION_NAVIGATION_BACK
import org.mariotaku.twidere.constant.KeyboardShortcutConstants.CONTEXT_TAG_NAVIGATION
import org.mariotaku.twidere.constant.SharedPreferenceConstants
import org.mariotaku.twidere.loader.UserSearchLoader
import org.mariotaku.twidere.model.*
import org.mariotaku.twidere.loader.CacheUserSearchLoader
import org.mariotaku.twidere.model.AccountDetails
import org.mariotaku.twidere.model.ParcelableDirectMessage
import org.mariotaku.twidere.model.ParcelableUser
import org.mariotaku.twidere.model.UserKey
import org.mariotaku.twidere.model.message.TaskStateChangedEvent
import org.mariotaku.twidere.model.util.AccountUtils
import org.mariotaku.twidere.provider.TwidereDataStore
import org.mariotaku.twidere.provider.TwidereDataStore.CachedUsers
import org.mariotaku.twidere.provider.TwidereDataStore.DirectMessages
import org.mariotaku.twidere.provider.TwidereDataStore.DirectMessages.Conversation
import org.mariotaku.twidere.provider.TwidereDataStore.DirectMessages.ConversationEntries
@ -99,8 +100,7 @@ class MessagesConversationFragment : BaseFragment(), LoaderCallbacks<Cursor?>, O
val query = args.getString(EXTRA_QUERY)
val fromCache = args.getBoolean(EXTRA_FROM_CACHE)
val fromUser = args.getBoolean(EXTRA_FROM_USER, false)
return CacheUserSearchLoader(this@MessagesConversationFragment, accountKey, query,
fromCache, fromUser)
return CacheUserSearchLoader(context, accountKey, query, fromCache, fromUser)
}
override fun onLoadFinished(loader: Loader<List<ParcelableUser>>, data: List<ParcelableUser>?) {
@ -706,49 +706,6 @@ class MessagesConversationFragment : BaseFragment(), LoaderCallbacks<Cursor?>, O
// }.executeTask();
// }
class CacheUserSearchLoader(
fragment: MessagesConversationFragment,
accountKey: UserKey,
query: String,
private val fromCache: Boolean,
fromUser: Boolean
) : UserSearchLoader(fragment.context, accountKey, query, 0, null, fromUser) {
private val userColorNameManager: UserColorNameManager
init {
userColorNameManager = fragment.userColorNameManager
}
override fun loadInBackground(): List<ParcelableUser> {
val query = query
if (TextUtils.isEmpty(query)) return emptyList()
if (fromCache) {
val cachedList = ArrayList<ParcelableUser>()
val queryEscaped = query.replace("_", "^_")
val nicknameKeys = Utils.getMatchedNicknameKeys(query, userColorNameManager)
val selection = Expression.or(Expression.likeRaw(Column(CachedUsers.SCREEN_NAME), "?||'%'", "^"),
Expression.likeRaw(Column(CachedUsers.NAME), "?||'%'", "^"),
Expression.inArgs(Column(CachedUsers.USER_KEY), nicknameKeys.size))
val selectionArgs = arrayOf(queryEscaped, queryEscaped, *nicknameKeys)
val order = arrayOf(CachedUsers.LAST_SEEN, CachedUsers.SCREEN_NAME, CachedUsers.NAME)
val ascending = booleanArrayOf(false, true, true)
val orderBy = OrderBy(order, ascending)
val c = context.contentResolver.query(CachedUsers.CONTENT_URI,
CachedUsers.BASIC_COLUMNS, selection?.sql,
selectionArgs, orderBy.sql)!!
val i = ParcelableUserCursorIndices(c)
c.moveToFirst()
while (!c.isAfterLast) {
cachedList.add(i.newObject(c))
c.moveToNext()
}
c.close()
return cachedList
}
return super.loadInBackground()
}
}
class DeleteConversationConfirmDialogFragment : BaseDialogFragment(), DialogInterface.OnClickListener {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val builder = AlertDialog.Builder(activity)
@ -861,6 +818,6 @@ class MessagesConversationFragment : BaseFragment(), LoaderCallbacks<Cursor?>, O
// Constants
private val LOADER_ID_SEARCH_USERS = 1
private val EXTRA_FROM_CACHE = "from_cache"
}
}

View File

@ -0,0 +1,59 @@
package org.mariotaku.twidere.loader
import android.content.Context
import android.text.TextUtils
import org.mariotaku.sqliteqb.library.Columns
import org.mariotaku.sqliteqb.library.Expression
import org.mariotaku.sqliteqb.library.OrderBy
import org.mariotaku.twidere.model.ParcelableUser
import org.mariotaku.twidere.model.ParcelableUserCursorIndices
import org.mariotaku.twidere.model.UserKey
import org.mariotaku.twidere.provider.TwidereDataStore
import org.mariotaku.twidere.util.UserColorNameManager
import org.mariotaku.twidere.util.Utils
import org.mariotaku.twidere.util.dagger.GeneralComponentHelper
import java.util.*
import javax.inject.Inject
class CacheUserSearchLoader(
context: Context,
accountKey: UserKey,
query: String,
private val fromCache: Boolean,
fromUser: Boolean
) : UserSearchLoader(context, accountKey, query, 0, null, fromUser) {
@Inject
internal lateinit var userColorNameManager: UserColorNameManager
init {
GeneralComponentHelper.build(context).inject(this)
}
override fun loadInBackground(): List<ParcelableUser> {
if (TextUtils.isEmpty(query)) return emptyList()
if (fromCache) {
val cachedList = ArrayList<ParcelableUser>()
val queryEscaped = query.replace("_", "^_")
val nicknameKeys = Utils.getMatchedNicknameKeys(query, userColorNameManager)
val selection = Expression.or(Expression.likeRaw(Columns.Column(TwidereDataStore.CachedUsers.SCREEN_NAME), "?||'%'", "^"),
Expression.likeRaw(Columns.Column(TwidereDataStore.CachedUsers.NAME), "?||'%'", "^"),
Expression.inArgs(Columns.Column(TwidereDataStore.CachedUsers.USER_KEY), nicknameKeys.size))
val selectionArgs = arrayOf(queryEscaped, queryEscaped, *nicknameKeys)
val order = arrayOf(TwidereDataStore.CachedUsers.LAST_SEEN, TwidereDataStore.CachedUsers.SCREEN_NAME, TwidereDataStore.CachedUsers.NAME)
val ascending = booleanArrayOf(false, true, true)
val orderBy = OrderBy(order, ascending)
val c = context.contentResolver.query(TwidereDataStore.CachedUsers.CONTENT_URI,
TwidereDataStore.CachedUsers.BASIC_COLUMNS, selection?.sql,
selectionArgs, orderBy.sql)!!
val i = ParcelableUserCursorIndices(c)
c.moveToFirst()
while (!c.isAfterLast) {
cachedList.add(i.newObject(c))
c.moveToNext()
}
c.close()
return cachedList
}
return super.loadInBackground()
}
}

View File

@ -22,7 +22,6 @@ package org.mariotaku.twidere.loader
import android.content.Context
import android.support.v4.content.AsyncTaskLoader
import android.text.TextUtils
import org.mariotaku.twidere.Constants
import org.mariotaku.twidere.loader.iface.IExtendedLoader
import org.mariotaku.twidere.model.ParcelableUser
import org.mariotaku.twidere.util.collection.NoDuplicatesArrayList
@ -32,7 +31,7 @@ abstract class ParcelableUsersLoader(
context: Context,
data: List<ParcelableUser>?,
override var fromUser: Boolean
) : AsyncTaskLoader<List<ParcelableUser>>(context), IExtendedLoader, Constants {
) : AsyncTaskLoader<List<ParcelableUser>>(context), IExtendedLoader {
protected val data: MutableList<ParcelableUser> = Collections.synchronizedList(NoDuplicatesArrayList<ParcelableUser>())

View File

@ -28,6 +28,7 @@ import org.mariotaku.twidere.adapter.*
import org.mariotaku.twidere.app.TwidereApplication
import org.mariotaku.twidere.fragment.*
import org.mariotaku.twidere.fragment.filter.FilteredUsersFragment
import org.mariotaku.twidere.loader.CacheUserSearchLoader
import org.mariotaku.twidere.loader.MicroBlogAPIStatusesLoader
import org.mariotaku.twidere.loader.ParcelableStatusLoader
import org.mariotaku.twidere.loader.ParcelableUserLoader
@ -150,4 +151,6 @@ interface GeneralComponent {
fun inject(provider: UrlFiltersSubscriptionProvider)
fun inject(preference: PremiumEntryPreference)
fun inject(loader: CacheUserSearchLoader)
}

View File

@ -18,65 +18,49 @@
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<merge
<RelativeLayout
android:id="@+id/usersListContainer"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".activity.UserSelectorActivity">
<LinearLayout
android:id="@+id/usersListContainer"
android:id="@+id/searchContainer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:divider="?android:dividerVertical"
android:orientation="vertical"
android:showDividers="middle">
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:gravity="center_vertical"
android:orientation="horizontal"
android:padding="@dimen/element_spacing_normal">
<LinearLayout
<android.support.v7.widget.AppCompatEditText
android:id="@+id/editScreenName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
android:layout_weight="1"
android:ems="10"
android:hint="@string/search_hint_users"
android:maxLines="1"
app:backgroundTint="?colorAccent"/>
<TextView
style="?android:listSeparatorTextViewStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/title_user"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:padding="@dimen/element_spacing_normal">
<AutoCompleteTextView
android:id="@+id/editScreenName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ems="10"
android:maxLines="1"/>
<org.mariotaku.twidere.view.IconActionButton
android:id="@+id/screenNameConfirm"
android:layout_width="@dimen/minimum_element_size"
android:layout_height="@dimen/minimum_element_size"
android:layout_weight="0"
android:background="?selectableItemBackground"
android:color="?menuIconColor"
android:contentDescription="@string/search"
android:src="@drawable/ic_action_search"
tools:tint="?menuIconColor"/>
</LinearLayout>
</LinearLayout>
<ListView
android:id="@+id/usersList"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<org.mariotaku.twidere.view.IconActionButton
android:id="@+id/screenNameConfirm"
android:layout_width="@dimen/minimum_element_size"
android:layout_height="@dimen/minimum_element_size"
android:layout_weight="0"
android:background="?selectableItemBackground"
android:color="?menuIconColor"
android:contentDescription="@string/search"
android:src="@drawable/ic_action_search"
tools:tint="?menuIconColor"/>
</LinearLayout>
</merge>
<include
layout="@layout/layout_list_with_empty_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/searchContainer"/>
</RelativeLayout>

View File

@ -39,6 +39,7 @@
android:id="@+id/emptyView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clickable="true"
android:gravity="center"
android:orientation="vertical"
tools:visibility="visible">
@ -63,7 +64,8 @@
<FrameLayout
android:id="@+id/progressContainer"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
android:clickable="true">
<ProgressBar
android:id="@+id/progressBar"

View File

@ -1014,6 +1014,7 @@
<string name="search">Search</string>
<string name="search_hint">Search tweets or users</string>
<string name="search_hint_users">Search users</string>
<string name="search_statuses">Search Tweets</string>
<string name="search_type_statuses">Tweets</string>
<string name="search_type_users">Users</string>