improved global search

This commit is contained in:
Mariotaku Lee 2015-11-08 15:12:35 +08:00
parent 984f7b9317
commit 8f19921243
28 changed files with 512 additions and 989 deletions

View File

@ -46,7 +46,7 @@ dependencies {
compile 'org.apache.commons:commons-lang3:3.4'
compile 'com.github.mariotaku:RestFu:0.9.2'
compile 'com.hannesdorfmann.parcelableplease:annotation:1.0.1'
compile 'com.github.mariotaku:SQLiteQB:901dd5e72f'
compile 'com.github.mariotaku:SQLiteQB:88291f3a28'
compile 'com.github.mariotaku.LoganSquareExtension:core:b6f53c9a4d'
compile fileTree(dir: 'libs', include: ['*.jar'])
}

View File

@ -202,6 +202,8 @@ public interface TwidereConstants extends SharedPreferenceConstants, IntentConst
int VIRTUAL_TABLE_ID_CACHED_USERS_WITH_SCORE = 122;
int VIRTUAL_TABLE_ID_DRAFTS_UNSENT = 131;
int VIRTUAL_TABLE_ID_DRAFTS_NOTIFICATIONS = 132;
int VIRTUAL_TABLE_ID_SUGGESTIONS_AUTO_COMPLETE = 141;
int VIRTUAL_TABLE_ID_SUGGESTIONS_SEARCH = 142;
int NOTIFICATION_ID_HOME_TIMELINE = 1;
int NOTIFICATION_ID_MENTIONS_TIMELINE = 2;

View File

@ -48,6 +48,10 @@ public class ProfileUpdate extends SimpleValueMap {
put("profile_link_color", String.format(Locale.ROOT, "%06X", 0xFFFFFF & profileLinkColor));
}
public void setBackgroundColor(int profileLinkColor) {
put("profile_background_color", String.format(Locale.ROOT, "%06X", 0xFFFFFF & profileLinkColor));
}
public ProfileUpdate name(String name) {
setName(name);
return this;
@ -72,4 +76,9 @@ public class ProfileUpdate extends SimpleValueMap {
setLinkColor(linkColor);
return this;
}
public ProfileUpdate backgroundColor(int linkColor) {
setBackgroundColor(linkColor);
return this;
}
}

View File

@ -28,6 +28,7 @@ import android.support.annotation.NonNull;
import com.bluelinelabs.logansquare.annotation.JsonField;
import com.bluelinelabs.logansquare.annotation.JsonObject;
import org.mariotaku.library.logansquare.extension.LoganSquareWrapper;
import org.mariotaku.twidere.api.twitter.model.DirectMessage;
import org.mariotaku.twidere.api.twitter.model.User;
import org.mariotaku.twidere.provider.TwidereDataStore.DirectMessages;
@ -108,25 +109,6 @@ public class ParcelableDirectMessage implements Parcelable, Comparable<Parcelabl
public ParcelableDirectMessage() {
}
public ParcelableDirectMessage(final ContentValues values) {
text_plain = values.getAsString(DirectMessages.TEXT_PLAIN);
text_html = values.getAsString(DirectMessages.TEXT_HTML);
text_unescaped = toPlainText(text_html);
sender_screen_name = values.getAsString(DirectMessages.SENDER_SCREEN_NAME);
sender_profile_image_url = values.getAsString(DirectMessages.SENDER_PROFILE_IMAGE_URL);
sender_name = values.getAsString(DirectMessages.SENDER_NAME);
sender_id = getAsLong(values, DirectMessages.SENDER_ID, -1);
recipient_screen_name = values.getAsString(DirectMessages.RECIPIENT_SCREEN_NAME);
recipient_profile_image_url = values.getAsString(DirectMessages.RECIPIENT_PROFILE_IMAGE_URL);
recipient_name = values.getAsString(DirectMessages.RECIPIENT_NAME);
recipient_id = getAsLong(values, DirectMessages.RECIPIENT_ID, -1);
timestamp = getAsLong(values, DirectMessages.MESSAGE_TIMESTAMP, -1);
id = getAsLong(values, DirectMessages.MESSAGE_ID, -1);
is_outgoing = getAsBoolean(values, DirectMessages.IS_OUTGOING, false);
account_id = getAsLong(values, DirectMessages.ACCOUNT_ID, -1);
media = ParcelableMedia.fromSerializedJson(values.getAsString(DirectMessages.MEDIA_JSON));
}
public ParcelableDirectMessage(final Cursor c, final CursorIndices idx) {
account_id = idx.account_id != -1 ? c.getLong(idx.account_id) : -1;
is_outgoing = idx.is_outgoing != -1 && c.getShort(idx.is_outgoing) == 1;

View File

@ -204,9 +204,9 @@ public interface TwidereDataStore {
String CONTENT_PATH = TABLE_NAME;
String CONTENT_PATH_WITH_RELATIONSHIP = TABLE_NAME + "/with_relationship";
String CONTENT_PATH_WITH_RELATIONSHIP = CONTENT_PATH + "/with_relationship";
String CONTENT_PATH_WITH_SCORE = TABLE_NAME + "/with_score";
String CONTENT_PATH_WITH_SCORE = CONTENT_PATH + "/with_score";
Uri CONTENT_URI = Uri.withAppendedPath(BASE_CONTENT_URI, CONTENT_PATH);
@ -288,11 +288,40 @@ public interface TwidereDataStore {
interface Suggestions extends BaseColumns {
String TYPE = "type";
String VALUE1 = "value1";
String VALUE2 = "value2";
String TITLE = "title";
String SUMMARY = "summary";
String ICON = "icon";
String EXTRA_ID = "extra_id";
String EXTRA = "extra";
interface Compose extends Suggestions {
String TABLE_NAME = "suggestions";
String CONTENT_PATH = TABLE_NAME;
Uri CONTENT_URI = Uri.withAppendedPath(BASE_CONTENT_URI, CONTENT_PATH);
String[] COLUMNS = {_ID, TYPE, TITLE, SUMMARY, ICON, EXTRA_ID, EXTRA};
String[] TYPES = {TYPE_PRIMARY_KEY, TYPE_TEXT_NOT_NULL, TYPE_TEXT, TYPE_TEXT, TYPE_TEXT,
TYPE_INT, TYPE_TEXT};
interface AutoComplete extends Suggestions {
String TYPE_USERS = "users";
String TYPE_HASHTAGS = "hashtags";
String CONTENT_PATH = Suggestions.CONTENT_PATH + "/auto_complete";
Uri CONTENT_URI = Uri.withAppendedPath(BASE_CONTENT_URI, CONTENT_PATH);
}
interface Search extends Suggestions {
String CONTENT_PATH = Suggestions.CONTENT_PATH + "/search";
Uri CONTENT_URI = Uri.withAppendedPath(BASE_CONTENT_URI, CONTENT_PATH);
String TYPE_SAVED_SEARCH = "saved_search";
String TYPE_USER = "user";
String TYPE_SEARCH_HISTORY = "search_history";
String TYPE_SCREEN_NAME = "screen_name";
}
}

View File

@ -22,6 +22,8 @@ package org.mariotaku.twidere.util;
import org.apache.commons.lang3.StringEscapeUtils;
import org.apache.commons.lang3.text.translate.AggregateTranslator;
import org.apache.commons.lang3.text.translate.CodePointTranslator;
import org.apache.commons.lang3.text.translate.EntityArrays;
import org.apache.commons.lang3.text.translate.LookupTranslator;
import java.io.IOException;
import java.io.Writer;
@ -30,6 +32,7 @@ public class HtmlEscapeHelper {
public static final AggregateTranslator ESCAPE_HTML = new AggregateTranslator(StringEscapeUtils.ESCAPE_HTML4,
new UnicodeControlCharacterToHtmlTranslator());
public static final LookupTranslator ESCAPE_BASIC = new LookupTranslator(EntityArrays.BASIC_ESCAPE());
public static String escape(final CharSequence text) {
if (text == null) return null;
@ -51,6 +54,10 @@ public class HtmlEscapeHelper {
return StringEscapeUtils.unescapeHtml4(string);
}
public static String escapeBasic(CharSequence text) {
return ESCAPE_BASIC.translate(text);
}
private static class UnicodeControlCharacterToHtmlTranslator extends CodePointTranslator {
@Override

View File

@ -186,6 +186,14 @@ public final class ParseUtils {
return parseString(object, null);
}
public static String parseString(final int object) {
return String.valueOf(object);
}
public static String parseString(final long object) {
return String.valueOf(object);
}
public static String parseString(final Object object, final String def) {
if (object == null) return def;
return String.valueOf(object);

View File

@ -19,7 +19,6 @@
package org.mariotaku.twidere.activity.support;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
@ -29,7 +28,7 @@ import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.app.LoaderManager.LoaderCallbacks;
import android.support.v4.content.AsyncTaskLoader;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.text.Editable;
import android.text.TextUtils;
@ -45,53 +44,38 @@ import android.view.WindowManager;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.BaseAdapter;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.Spinner;
import android.widget.TextView;
import org.apache.commons.lang3.ArrayUtils;
import org.mariotaku.sqliteqb.library.Columns.Column;
import org.mariotaku.sqliteqb.library.Expression;
import org.mariotaku.sqliteqb.library.OrderBy;
import org.mariotaku.sqliteqb.library.RawItemArray;
import com.mobeta.android.dslv.DragSortCursorAdapter;
import org.mariotaku.twidere.R;
import org.mariotaku.twidere.activity.support.QuickSearchBarActivity.SuggestionItem;
import org.mariotaku.twidere.adapter.AccountsSpinnerAdapter;
import org.mariotaku.twidere.model.ParcelableAccount;
import org.mariotaku.twidere.model.ParcelableCredentials;
import org.mariotaku.twidere.model.ParcelableUser;
import org.mariotaku.twidere.model.ParcelableUser.CachedIndices;
import org.mariotaku.twidere.provider.TwidereDataStore.CachedUsers;
import org.mariotaku.twidere.provider.TwidereDataStore.SavedSearches;
import org.mariotaku.twidere.provider.TwidereDataStore.SearchHistory;
import org.mariotaku.twidere.provider.TwidereDataStore.Suggestions;
import org.mariotaku.twidere.util.EditTextEnterHandler;
import org.mariotaku.twidere.util.EditTextEnterHandler.EnterListener;
import org.mariotaku.twidere.util.KeyboardShortcutsHandler;
import org.mariotaku.twidere.util.MediaLoaderWrapper;
import org.mariotaku.twidere.util.ParseUtils;
import org.mariotaku.twidere.util.SwipeDismissListViewTouchListener;
import org.mariotaku.twidere.util.SwipeDismissListViewTouchListener.DismissCallbacks;
import org.mariotaku.twidere.util.ThemeUtils;
import org.mariotaku.twidere.util.UserColorNameManager;
import org.mariotaku.twidere.util.Utils;
import org.mariotaku.twidere.util.content.ContentResolverUtils;
import org.mariotaku.twidere.view.ExtendedRelativeLayout;
import org.mariotaku.twidere.view.iface.IExtendedView.OnFitSystemWindowsListener;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Created by mariotaku on 15/1/6.
*/
public class QuickSearchBarActivity extends ThemedFragmentActivity implements OnClickListener,
LoaderCallbacks<List<SuggestionItem>>, OnItemSelectedListener, OnItemClickListener,
DismissCallbacks, OnFitSystemWindowsListener {
LoaderCallbacks<Cursor>, OnItemSelectedListener, OnItemClickListener,
OnFitSystemWindowsListener {
private Spinner mAccountSpinner;
private EditText mSearchQuery;
@ -102,25 +86,19 @@ public class QuickSearchBarActivity extends ThemedFragmentActivity implements On
private Rect mSystemWindowsInsets = new Rect();
private boolean mTextChanged;
@Override
public boolean canDismiss(int position) {
return mUsersSearchAdapter.canDismiss(position);
}
@Override
public void onDismiss(ListView listView, int[] reverseSortedPositions) {
final long[] ids = new long[reverseSortedPositions.length];
for (int i = 0, j = reverseSortedPositions.length; i < j; i++) {
final int position = reverseSortedPositions[i];
final SearchHistoryItem item = (SearchHistoryItem) mUsersSearchAdapter.getItem(position);
mUsersSearchAdapter.removeItemAt(position);
ids[i] = item.getCursorId();
}
final ContentResolver cr = getContentResolver();
final Long[] idsObject = ArrayUtils.toObject(ids);
ContentResolverUtils.bulkDelete(cr, SearchHistory.CONTENT_URI, SearchHistory._ID, idsObject,
null, false);
getSupportLoaderManager().restartLoader(0, null, this);
// final long[] ids = new long[reverseSortedPositions.length];
// for (int i = 0, j = reverseSortedPositions.length; i < j; i++) {
// final int position = reverseSortedPositions[i];
// final SearchHistoryItem item = (SearchHistoryItem) mUsersSearchAdapter.getItem(position);
// mUsersSearchAdapter.removeItemAt(position);
// ids[i] = item.getCursorId();
// }
// final ContentResolver cr = getContentResolver();
// final Long[] idsObject = ArrayUtils.toObject(ids);
// ContentResolverUtils.bulkDelete(cr, SearchHistory.CONTENT_URI, SearchHistory._ID, idsObject,
// null, false);
// getSupportLoaderManager().restartLoader(0, null, this);
}
@Override
@ -154,18 +132,22 @@ public class QuickSearchBarActivity extends ThemedFragmentActivity implements On
}
@Override
public Loader<List<SuggestionItem>> onCreateLoader(int id, Bundle args) {
return new SuggestionsLoader(this, mAccountSpinner.getSelectedItemId(), mSearchQuery.getText().toString());
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
final long accountId = getAccountId();
final Uri.Builder builder = Suggestions.Search.CONTENT_URI.buildUpon();
builder.appendQueryParameter(QUERY_PARAM_QUERY, ParseUtils.parseString(mSearchQuery.getText()));
builder.appendQueryParameter(QUERY_PARAM_ACCOUNT_ID, ParseUtils.parseString(accountId));
return new CursorLoader(this, builder.build(), Suggestions.Search.COLUMNS, null, null, null);
}
@Override
public void onLoadFinished(Loader<List<SuggestionItem>> loader, List<SuggestionItem> data) {
mUsersSearchAdapter.setData(data);
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
mUsersSearchAdapter.changeCursor(data);
}
@Override
public void onLoaderReset(Loader<List<SuggestionItem>> loader) {
mUsersSearchAdapter.setData(null);
public void onLoaderReset(Loader<Cursor> loader) {
mUsersSearchAdapter.changeCursor(null);
}
@Override
@ -176,8 +158,25 @@ public class QuickSearchBarActivity extends ThemedFragmentActivity implements On
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
final SuggestionItem item = mUsersSearchAdapter.getItem(position);
item.onItemClick(this, position);
final SuggestionItem item = mUsersSearchAdapter.getSuggestionItem(position);
switch (mUsersSearchAdapter.getItemViewType(position)) {
case SuggestionsAdapter.VIEW_TYPE_USER_SUGGESTION_ITEM: {
Utils.openUserProfile(this, getAccountId(), item.extra_id, item.summary, null);
finish();
break;
}
case SuggestionsAdapter.VIEW_TYPE_USER_SCREEN_NAME: {
Utils.openUserProfile(this, getAccountId(), -1, item.title, null);
finish();
break;
}
case SuggestionsAdapter.VIEW_TYPE_SAVED_SEARCH:
case SuggestionsAdapter.VIEW_TYPE_SEARCH_HISTORY: {
Utils.openSearch(this, getAccountId(), item.title);
finish();
break;
}
}
}
@Override
@ -225,9 +224,6 @@ public class QuickSearchBarActivity extends ThemedFragmentActivity implements On
mUsersSearchAdapter = new SuggestionsAdapter(this);
mSuggestionsList.setAdapter(mUsersSearchAdapter);
mSuggestionsList.setOnItemClickListener(this);
final SwipeDismissListViewTouchListener listener = new SwipeDismissListViewTouchListener(mSuggestionsList, this);
mSuggestionsList.setOnTouchListener(listener);
mSuggestionsList.setOnScrollListener(listener.makeScrollListener());
mSearchSubmit.setOnClickListener(this);
EditTextEnterHandler.attach(mSearchQuery, new EnterListener() {
@ -256,11 +252,7 @@ public class QuickSearchBarActivity extends ThemedFragmentActivity implements On
@Override
public void afterTextChanged(Editable s) {
if (Utils.removeLineBreaks(s)) {
doSearch();
} else {
getSupportLoaderManager().restartLoader(0, null, QuickSearchBarActivity.this);
}
getSupportLoaderManager().restartLoader(0, null, QuickSearchBarActivity.this);
}
});
@ -286,10 +278,6 @@ public class QuickSearchBarActivity extends ThemedFragmentActivity implements On
return mAccountSpinner.getSelectedItemId();
}
private static int getHistorySize(CharSequence query) {
return TextUtils.isEmpty(query) ? 3 : 2;
}
private void updateWindowAttributes() {
final Window window = getWindow();
final WindowManager.LayoutParams attributes = window.getAttributes();
@ -298,190 +286,136 @@ public class QuickSearchBarActivity extends ThemedFragmentActivity implements On
window.setAttributes(attributes);
}
interface SuggestionItem {
void bindView(SuggestionsAdapter adapter, View view, int position);
int getItemLayoutResource();
int getItemViewType();
boolean isEnabled();
void onItemClick(QuickSearchBarActivity activity, int position);
}
static abstract class BaseClickableItem implements SuggestionItem {
@Override
public final boolean isEnabled() {
return true;
}
}
static class SavedSearchItem extends BaseClickableItem {
static final int ITEM_VIEW_TYPE = 1;
private final String mQuery;
public SavedSearchItem(String query) {
mQuery = query;
}
@Override
public void bindView(SuggestionsAdapter adapter, View view, int position) {
final ImageView icon = (ImageView) view.findViewById(android.R.id.icon);
final TextView text1 = (TextView) view.findViewById(android.R.id.text1);
text1.setText(mQuery);
icon.setImageResource(R.drawable.ic_action_save);
final View editQuery = view.findViewById(R.id.edit_query);
editQuery.setTag(position);
editQuery.setOnClickListener(adapter);
}
@Override
public final int getItemLayoutResource() {
return R.layout.list_item_suggestion_search;
}
@Override
public int getItemViewType() {
return ITEM_VIEW_TYPE;
}
public String getQuery() {
return mQuery;
}
@Override
public void onItemClick(QuickSearchBarActivity activity, int position) {
Utils.openSearch(activity, activity.getAccountId(), mQuery);
activity.finish();
}
}
static class SearchHistoryItem extends BaseClickableItem {
static final int ITEM_VIEW_TYPE = 0;
private final long mCursorId;
private final String mQuery;
public SearchHistoryItem(long cursorId, String query) {
mCursorId = cursorId;
mQuery = query;
}
public long getCursorId() {
return mCursorId;
}
public String getQuery() {
return mQuery;
}
@Override
public void bindView(SuggestionsAdapter adapter, View view, int position) {
final ImageView icon = (ImageView) view.findViewById(android.R.id.icon);
final TextView text1 = (TextView) view.findViewById(android.R.id.text1);
text1.setText(mQuery);
icon.setImageResource(R.drawable.ic_action_history);
final View editQuery = view.findViewById(R.id.edit_query);
editQuery.setTag(position);
editQuery.setOnClickListener(adapter);
}
@Override
public final int getItemLayoutResource() {
return R.layout.list_item_suggestion_search;
}
@Override
public int getItemViewType() {
return ITEM_VIEW_TYPE;
}
@Override
public void onItemClick(QuickSearchBarActivity activity, int position) {
Utils.openSearch(activity, activity.getAccountId(), mQuery);
activity.finish();
}
}
private void setSearchQuery(String query) {
mSearchQuery.setText(query);
if (query == null) return;
mSearchQuery.setSelection(query.length());
}
public static class SuggestionsAdapter extends BaseAdapter implements OnClickListener {
static class SuggestionItem {
public final String title, summary;
private final long extra_id;
public SuggestionItem(Cursor cursor, SuggestionsAdapter.Indices indices) {
title = cursor.getString(indices.title);
summary = cursor.getString(indices.summary);
extra_id = cursor.getLong(indices.extra_id);
}
}
public static class SuggestionsAdapter extends DragSortCursorAdapter implements OnClickListener {
static final int VIEW_TYPE_SEARCH_HISTORY = 0;
static final int VIEW_TYPE_SAVED_SEARCH = 1;
static final int VIEW_TYPE_USER_SUGGESTION_ITEM = 2;
static final int VIEW_TYPE_USER_SCREEN_NAME = 3;
private final QuickSearchBarActivity mActivity;
private final LayoutInflater mInflater;
private final MediaLoaderWrapper mImageLoader;
private final UserColorNameManager mUserColorNameManager;
private List<SuggestionItem> mData;
private final QuickSearchBarActivity mActivity;
private Indices mIndices;
SuggestionsAdapter(QuickSearchBarActivity activity) {
super(activity, null, 0);
mActivity = activity;
mImageLoader = activity.mImageLoader;
mUserColorNameManager = activity.mUserColorNameManager;
mInflater = LayoutInflater.from(activity);
}
public boolean canDismiss(int position) {
return getItemViewType(position) == SearchHistoryItem.ITEM_VIEW_TYPE;
}
public Context getContext() {
return mActivity;
}
@Override
public int getCount() {
if (mData == null) return 0;
return mData.size();
}
@Override
public SuggestionItem getItem(int position) {
if (mData == null) return null;
return mData.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
final View view;
final SuggestionItem item = getItem(position);
if (convertView == null) {
view = mInflater.inflate(item.getItemLayoutResource(), parent, false);
} else {
view = convertView;
public View newView(Context context, Cursor cursor, ViewGroup parent) {
switch (getItemViewType(cursor.getPosition())) {
case VIEW_TYPE_SEARCH_HISTORY:
case VIEW_TYPE_SAVED_SEARCH: {
final View view = mInflater.inflate(R.layout.list_item_suggestion_search, parent, false);
final SearchViewHolder holder = new SearchViewHolder(view);
holder.edit_query.setOnClickListener(this);
view.setTag(holder);
return view;
}
case VIEW_TYPE_USER_SUGGESTION_ITEM:
case VIEW_TYPE_USER_SCREEN_NAME: {
final View view = mInflater.inflate(R.layout.list_item_suggestion_user, parent, false);
view.setTag(new UserViewHolder(view));
return view;
}
}
item.bindView(this, view, position);
return view;
throw new UnsupportedOperationException("Unknown viewType");
}
public MediaLoaderWrapper getImageLoader() {
return mImageLoader;
public SuggestionItem getSuggestionItem(int position) {
final Cursor cursor = (Cursor) super.getItem(position);
return new SuggestionItem(cursor, mIndices);
}
@Override
public boolean isEnabled(int position) {
return getItem(position).isEnabled();
public void bindView(View view, Context context, Cursor cursor) {
switch (getItemViewType(cursor.getPosition())) {
case VIEW_TYPE_SEARCH_HISTORY: {
final SearchViewHolder holder = (SearchViewHolder) view.getTag();
final String title = cursor.getString(mIndices.title);
holder.edit_query.setTag(title);
holder.text1.setText(title);
holder.icon.setImageResource(R.drawable.ic_action_history);
break;
}
case VIEW_TYPE_SAVED_SEARCH: {
final SearchViewHolder holder = (SearchViewHolder) view.getTag();
final String title = cursor.getString(mIndices.title);
holder.edit_query.setTag(title);
holder.text1.setText(title);
holder.icon.setImageResource(R.drawable.ic_action_save);
break;
}
case VIEW_TYPE_USER_SUGGESTION_ITEM: {
final UserViewHolder holder = (UserViewHolder) view.getTag();
holder.text1.setText(mUserColorNameManager.getUserNickname(cursor.getLong(mIndices.extra_id),
cursor.getString(mIndices.title), false));
holder.text2.setVisibility(View.VISIBLE);
holder.text2.setText(String.format("@%s", cursor.getString(mIndices.summary)));
holder.icon.clearColorFilter();
mImageLoader.displayProfileImage(holder.icon, cursor.getString(mIndices.icon));
break;
}
case VIEW_TYPE_USER_SCREEN_NAME: {
final UserViewHolder holder = (UserViewHolder) view.getTag();
holder.text1.setText(String.format("@%s", cursor.getString(mIndices.title)));
holder.text2.setVisibility(View.GONE);
holder.icon.setColorFilter(holder.text1.getCurrentTextColor(), Mode.SRC_ATOP);
mImageLoader.cancelDisplayTask(holder.icon);
holder.icon.setImageResource(R.drawable.ic_action_user);
break;
}
}
}
@Override
public int getItemViewType(int position) {
if (mData == null) return IGNORE_ITEM_VIEW_TYPE;
return mData.get(position).getItemViewType();
final Cursor cursor = (Cursor) getItem(position);
switch (cursor.getString(mIndices.type)) {
case Suggestions.Search.TYPE_SAVED_SEARCH: {
return VIEW_TYPE_SAVED_SEARCH;
}
case Suggestions.Search.TYPE_SCREEN_NAME: {
return VIEW_TYPE_USER_SCREEN_NAME;
}
case Suggestions.Search.TYPE_SEARCH_HISTORY: {
return VIEW_TYPE_SEARCH_HISTORY;
}
case Suggestions.Search.TYPE_USER: {
return VIEW_TYPE_USER_SUGGESTION_ITEM;
}
}
return IGNORE_ITEM_VIEW_TYPE;
}
@Override
@ -489,206 +423,67 @@ public class QuickSearchBarActivity extends ThemedFragmentActivity implements On
return 4;
}
public void removeItemAt(int position) {
if (mData == null) return;
mData.remove(position);
notifyDataSetChanged();
}
public void setData(List<SuggestionItem> data) {
mData = data;
notifyDataSetChanged();
}
public UserColorNameManager getUserColorNameManager() {
return mUserColorNameManager;
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.edit_query: {
final SuggestionItem item = getItem((Integer) v.getTag());
if (item instanceof SearchHistoryItem) {
mActivity.setSearchQuery(((SearchHistoryItem) item).getQuery());
} else if (item instanceof SavedSearchItem) {
mActivity.setSearchQuery(((SavedSearchItem) item).getQuery());
}
mActivity.setSearchQuery((String) v.getTag());
break;
}
}
}
}
public static class SuggestionsLoader extends AsyncTaskLoader<List<SuggestionItem>> {
private static final Pattern PATTERN_SCREEN_NAME = Pattern.compile("(?i)[@\uFF20]?([a-z0-9_]{1,20})");
private final UserColorNameManager mUserColorNameManager;
private final long mAccountId;
private final String mQuery;
public SuggestionsLoader(QuickSearchBarActivity context, long accountId, String query) {
super(context);
mUserColorNameManager = context.mUserColorNameManager;
mAccountId = accountId;
mQuery = query;
}
@Override
public List<SuggestionItem> loadInBackground() {
final boolean emptyQuery = TextUtils.isEmpty(mQuery);
final Context context = getContext();
final ContentResolver resolver = context.getContentResolver();
final List<SuggestionItem> result = new ArrayList<>();
final String[] historyProjection = {SearchHistory._ID, SearchHistory.QUERY};
final Cursor historyCursor = resolver.query(SearchHistory.CONTENT_URI,
historyProjection, null, null, SearchHistory.DEFAULT_SORT_ORDER);
for (int i = 0, j = Math.min(getHistorySize(mQuery), historyCursor.getCount()); i < j; i++) {
historyCursor.moveToPosition(i);
result.add(new SearchHistoryItem(historyCursor.getLong(0), historyCursor.getString(1)));
}
historyCursor.close();
if (!emptyQuery) {
final String queryEscaped = mQuery.replace("_", "^_");
final long[] nicknameIds = Utils.getMatchedNicknameIds(mQuery, mUserColorNameManager);
final Expression selection = Expression.or(
Expression.likeRaw(new Column(CachedUsers.SCREEN_NAME), "?||'%'", "^"),
Expression.likeRaw(new Column(CachedUsers.NAME), "?||'%'", "^"),
Expression.in(new Column(CachedUsers.USER_ID), new RawItemArray(nicknameIds)));
final String[] selectionArgs = new String[]{queryEscaped, queryEscaped};
final String[] order = {CachedUsers.LAST_SEEN, "score", CachedUsers.SCREEN_NAME, CachedUsers.NAME};
final boolean[] ascending = {false, false, true, true};
final OrderBy orderBy = new OrderBy(order, ascending);
final Uri uri = Uri.withAppendedPath(CachedUsers.CONTENT_URI_WITH_SCORE, String.valueOf(mAccountId));
final Cursor usersCursor = context.getContentResolver().query(uri, CachedUsers.COLUMNS,
selection.getSQL(), selectionArgs, orderBy.getSQL());
final CachedIndices usersIndices = new CachedIndices(usersCursor);
final int screenNamePos = result.size();
boolean hasName = false;
for (int i = 0, j = Math.min(5, usersCursor.getCount()); i < j; i++) {
usersCursor.moveToPosition(i);
final UserSuggestionItem userSuggestionItem = new UserSuggestionItem(usersCursor, usersIndices, mAccountId);
final ParcelableUser user = userSuggestionItem.getUser();
result.add(userSuggestionItem);
if (user.screen_name.equalsIgnoreCase(mQuery)) {
hasName = true;
}
}
if (!hasName) {
final Matcher m = PATTERN_SCREEN_NAME.matcher(mQuery);
if (m.matches()) {
result.add(screenNamePos, new UserScreenNameItem(m.group(1), mAccountId));
}
}
usersCursor.close();
public Cursor swapCursor(Cursor newCursor) {
if (newCursor != null) {
mIndices = new Indices(newCursor);
} else {
final String[] savedSearchesProjection = {SavedSearches.QUERY};
final Expression savedSearchesWhere = Expression.equals(SavedSearches.ACCOUNT_ID, mAccountId);
final Cursor savedSearchesCursor = resolver.query(SavedSearches.CONTENT_URI,
savedSearchesProjection, savedSearchesWhere.getSQL(), null,
SavedSearches.DEFAULT_SORT_ORDER);
savedSearchesCursor.moveToFirst();
while (!savedSearchesCursor.isAfterLast()) {
result.add(new SavedSearchItem(savedSearchesCursor.getString(0)));
savedSearchesCursor.moveToNext();
}
savedSearchesCursor.close();
mIndices = null;
}
return result;
return super.swapCursor(newCursor);
}
@Override
protected void onStartLoading() {
forceLoad();
}
}
static class SearchViewHolder {
static class UserScreenNameItem extends BaseClickableItem {
private final ImageView icon;
private final TextView text1;
private final View edit_query;
static final int ITEM_VIEW_TYPE = 3;
private final String mScreenName;
private final long mAccountId;
SearchViewHolder(View view) {
icon = (ImageView) view.findViewById(android.R.id.icon);
text1 = (TextView) view.findViewById(android.R.id.text1);
edit_query = view.findViewById(R.id.edit_query);
}
public UserScreenNameItem(String screenName, long accountId) {
mScreenName = screenName;
mAccountId = accountId;
}
@Override
public int getItemViewType() {
return ITEM_VIEW_TYPE;
static class UserViewHolder {
private final ImageView icon;
private final TextView text1;
private final TextView text2;
UserViewHolder(View view) {
icon = (ImageView) view.findViewById(android.R.id.icon);
text1 = (TextView) view.findViewById(android.R.id.text1);
text2 = (TextView) view.findViewById(android.R.id.text2);
}
}
@Override
public void onItemClick(QuickSearchBarActivity activity, int position) {
Utils.openUserProfile(activity, mAccountId, -1, mScreenName, null);
activity.finish();
}
private static class Indices {
private final int type;
private final int title;
private final int summary;
private final int icon;
private final int extra_id;
@Override
public final int getItemLayoutResource() {
return R.layout.list_item_suggestion_user;
}
@Override
public void bindView(SuggestionsAdapter adapter, View view, int position) {
final MediaLoaderWrapper loader = adapter.getImageLoader();
final ImageView icon = (ImageView) view.findViewById(android.R.id.icon);
final TextView text1 = (TextView) view.findViewById(android.R.id.text1);
final TextView text2 = (TextView) view.findViewById(android.R.id.text2);
text1.setText('@' + mScreenName);
text2.setVisibility(View.GONE);
icon.setColorFilter(text1.getCurrentTextColor(), Mode.SRC_ATOP);
loader.cancelDisplayTask(icon);
icon.setImageResource(R.drawable.ic_action_user);
}
}
static class UserSuggestionItem extends BaseClickableItem {
static final int ITEM_VIEW_TYPE = 2;
private final ParcelableUser mUser;
public UserSuggestionItem(Cursor c, CachedIndices i, long accountId) {
mUser = new ParcelableUser(c, i, accountId);
}
public ParcelableUser getUser() {
return mUser;
}
@Override
public int getItemViewType() {
return ITEM_VIEW_TYPE;
}
@Override
public void onItemClick(QuickSearchBarActivity activity, int position) {
Utils.openUserProfile(activity, mUser, null);
activity.finish();
}
@Override
public final int getItemLayoutResource() {
return R.layout.list_item_suggestion_user;
}
@Override
public void bindView(SuggestionsAdapter adapter, View view, int position) {
final ParcelableUser user = mUser;
final MediaLoaderWrapper loader = adapter.getImageLoader();
final UserColorNameManager manager = adapter.getUserColorNameManager();
final ImageView icon = (ImageView) view.findViewById(android.R.id.icon);
final TextView text1 = (TextView) view.findViewById(android.R.id.text1);
final TextView text2 = (TextView) view.findViewById(android.R.id.text2);
text1.setText(manager.getUserNickname(user.id, user.name, false));
text2.setVisibility(View.VISIBLE);
text2.setText("@" + user.screen_name);
icon.clearColorFilter();
loader.displayProfileImage(icon, user.profile_image_url);
public Indices(Cursor cursor) {
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);
}
}
}

View File

@ -33,6 +33,7 @@ import android.widget.TextView;
import org.mariotaku.twidere.R;
import org.mariotaku.twidere.util.HtmlEscapeHelper;
import org.mariotaku.twidere.util.HtmlSpanBuilder;
import org.mariotaku.twidere.util.PermissionsManager;
import static android.text.TextUtils.isEmpty;
@ -144,7 +145,7 @@ public class RequestPermissionsActivity extends BaseSupportDialogActivity implem
} else {
appendPermission(builder, getString(R.string.permission_description_none), false);
}
mMessageView.setText(Html.fromHtml(builder.toString()));
mMessageView.setText(HtmlSpanBuilder.fromHtml(builder.toString()));
} catch (final NameNotFoundException e) {
setResult(RESULT_CANCELED);
finish();

View File

@ -19,35 +19,22 @@
package org.mariotaku.twidere.adapter;
import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.graphics.PorterDuff.Mode;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.v4.widget.SimpleCursorAdapter;
import android.text.TextUtils;
import android.view.View;
import android.widget.EditText;
import android.widget.FilterQueryProvider;
import android.widget.TextView;
import org.mariotaku.sqliteqb.library.Columns.Column;
import org.mariotaku.sqliteqb.library.Expression;
import org.mariotaku.sqliteqb.library.OrderBy;
import org.mariotaku.sqliteqb.library.RawItemArray;
import org.mariotaku.twidere.BuildConfig;
import org.mariotaku.twidere.Constants;
import org.mariotaku.twidere.R;
import org.mariotaku.twidere.app.TwidereApplication;
import org.mariotaku.twidere.provider.TwidereDataStore.CachedHashtags;
import org.mariotaku.twidere.provider.TwidereDataStore.CachedUsers;
import org.mariotaku.twidere.provider.TwidereDataStore.CachedValues;
import org.mariotaku.twidere.provider.TwidereDataStore.Suggestions;
import org.mariotaku.twidere.util.MediaLoaderWrapper;
import org.mariotaku.twidere.util.ParseUtils;
import org.mariotaku.twidere.util.SharedPreferencesWrapper;
import org.mariotaku.twidere.util.UserColorNameManager;
import org.mariotaku.twidere.util.Utils;
import org.mariotaku.twidere.util.dagger.ApplicationModule;
import org.mariotaku.twidere.util.dagger.DaggerGeneralComponent;
import org.mariotaku.twidere.view.ProfileImageView;
@ -60,10 +47,6 @@ public class UserHashtagAutoCompleteAdapter extends SimpleCursorAdapter implemen
private static final String[] FROM = new String[0];
private static final int[] TO = new int[0];
@NonNull
private final ContentResolver mResolver;
@NonNull
private final SQLiteDatabase mDatabase;
@Inject
MediaLoaderWrapper mProfileImageLoader;
@Inject
@ -71,32 +54,18 @@ public class UserHashtagAutoCompleteAdapter extends SimpleCursorAdapter implemen
@Inject
UserColorNameManager mUserColorNameManager;
private final EditText mEditText;
private final boolean mDisplayProfileImage;
private int mProfileImageUrlIdx, mNameIdx, mScreenNameIdx, mUserIdIdx;
private char mToken = '@';
private int mTypeIdx, mIconIdx, mTitleIdx, mSummaryIdx, mExtraIdIdx;
private long mAccountId;
private char mToken;
public UserHashtagAutoCompleteAdapter(final Context context) {
this(context, null);
}
public UserHashtagAutoCompleteAdapter(final Context context, final EditText view) {
super(context, R.layout.list_item_auto_complete, null, FROM, TO, 0);
DaggerGeneralComponent.builder().applicationModule(ApplicationModule.get(context)).build().inject(this);
mEditText = view;
final TwidereApplication app = TwidereApplication.getInstance(context);
mResolver = context.getContentResolver();
mDatabase = app.getSQLiteDatabase();
mDisplayProfileImage = mPreferences.getBoolean(KEY_DISPLAY_PROFILE_IMAGE, true);
}
public UserHashtagAutoCompleteAdapter(final EditText view) {
this(view.getContext(), view);
}
@Override
public void bindView(final View view, final Context context, final Cursor cursor) {
if (isCursorClosed()) return;
@ -107,26 +76,25 @@ public class UserHashtagAutoCompleteAdapter extends SimpleCursorAdapter implemen
// Clear images in order to prevent images in recycled view shown.
icon.setImageDrawable(null);
if (mScreenNameIdx != -1 && mNameIdx != -1 && mUserIdIdx != -1) {
text1.setText(mUserColorNameManager.getUserNickname(cursor.getLong(mUserIdIdx), cursor.getString(mNameIdx)));
text2.setText("@" + cursor.getString(mScreenNameIdx));
} else {
text1.setText("#" + cursor.getString(mNameIdx));
text2.setText(R.string.hashtag);
}
icon.setVisibility(mDisplayProfileImage ? View.VISIBLE : View.GONE);
if (mProfileImageUrlIdx != -1) {
if (Suggestions.AutoComplete.TYPE_USERS.equals(cursor.getString(mTypeIdx))) {
text1.setText(mUserColorNameManager.getUserNickname(cursor.getLong(mExtraIdIdx), cursor.getString(mTitleIdx)));
text2.setText("@" + cursor.getString(mSummaryIdx));
if (mDisplayProfileImage) {
final String profileImageUrl = cursor.getString(mProfileImageUrlIdx);
final String profileImageUrl = cursor.getString(mIconIdx);
mProfileImageLoader.displayProfileImage(icon, profileImageUrl);
} else {
mProfileImageLoader.cancelDisplayTask(icon);
}
icon.clearColorFilter();
} else {
text1.setText("#" + cursor.getString(mTitleIdx));
text2.setText(R.string.hashtag);
icon.setImageResource(R.drawable.ic_action_hashtag);
icon.setColorFilter(text1.getCurrentTextColor(), Mode.SRC_ATOP);
}
icon.setVisibility(mDisplayProfileImage ? View.VISIBLE : View.GONE);
super.bindView(view, context, cursor);
}
@ -141,7 +109,7 @@ public class UserHashtagAutoCompleteAdapter extends SimpleCursorAdapter implemen
@Override
public CharSequence convertToString(final Cursor cursor) {
if (isCursorClosed()) return null;
return cursor.getString(mScreenNameIdx != -1 ? mScreenNameIdx : mNameIdx);
return cursor.getString(mSummaryIdx != -1 ? mSummaryIdx : mTitleIdx);
}
public boolean isCursorClosed() {
@ -151,45 +119,31 @@ public class UserHashtagAutoCompleteAdapter extends SimpleCursorAdapter implemen
@Override
public Cursor runQueryOnBackgroundThread(final CharSequence constraint) {
char token = mToken;
if (mEditText != null && constraint != null) {
final CharSequence text = mEditText.getText();
token = text.charAt(mEditText.getSelectionEnd() - constraint.length() - 1);
}
if (isAtSymbol(token) == isAtSymbol(mToken)) {
if (TextUtils.isEmpty(constraint)) return null;
char token = constraint.charAt(0);
if (getNormalizedSymbol(token) == getNormalizedSymbol(mToken)) {
final FilterQueryProvider filter = getFilterQueryProvider();
if (filter != null) return filter.runQuery(constraint);
}
mToken = token;
final String constraintEscaped = constraint != null ? constraint.toString().replaceAll("_", "^_") : null;
if (isAtSymbol(token)) {
final Expression selection;
final String[] selectionArgs;
if (constraintEscaped != null) {
final long[] nicknameIds = Utils.getMatchedNicknameIds(ParseUtils.parseString(constraint), mUserColorNameManager);
selection = Expression.or(Expression.likeRaw(new Column(CachedUsers.SCREEN_NAME), "?||'%'", "^"),
Expression.likeRaw(new Column(CachedUsers.NAME), "?||'%'", "^"),
Expression.in(new Column(CachedUsers.USER_ID), new RawItemArray(nicknameIds)));
selectionArgs = new String[]{constraintEscaped, constraintEscaped};
} else {
selection = null;
selectionArgs = null;
final Uri.Builder builder = Suggestions.AutoComplete.CONTENT_URI.buildUpon();
builder.appendQueryParameter(QUERY_PARAM_QUERY, String.valueOf(constraint.subSequence(1, constraint.length())));
switch (getNormalizedSymbol(token)) {
case '#': {
builder.appendQueryParameter(QUERY_PARAM_TYPE, Suggestions.AutoComplete.TYPE_HASHTAGS);
break;
}
case '@': {
builder.appendQueryParameter(QUERY_PARAM_TYPE, Suggestions.AutoComplete.TYPE_USERS);
break;
}
default: {
return null;
}
final OrderBy orderBy = new OrderBy(new String[]{CachedUsers.LAST_SEEN, "score", CachedUsers.SCREEN_NAME,
CachedUsers.NAME}, new boolean[]{false, false, true, true});
final Cursor cursor = mResolver.query(Uri.withAppendedPath(CachedUsers.CONTENT_URI_WITH_SCORE, String.valueOf(mAccountId)),
CachedUsers.BASIC_COLUMNS, selection != null ? selection.getSQL() : null, selectionArgs, orderBy.getSQL());
if (BuildConfig.DEBUG && cursor == null) throw new NullPointerException();
return cursor;
} else {
final String selection = constraintEscaped != null ? CachedHashtags.NAME + " LIKE ?||'%' ESCAPE '^'"
: null;
final String[] selectionArgs = constraintEscaped != null ? new String[]{constraintEscaped} : null;
final Cursor cursor = mDatabase.query(true, CachedHashtags.TABLE_NAME, CachedHashtags.COLUMNS, selection, selectionArgs,
null, null, CachedHashtags.NAME, null);
if (BuildConfig.DEBUG && cursor == null) throw new NullPointerException();
return cursor;
}
builder.appendQueryParameter(QUERY_PARAM_ACCOUNT_ID, String.valueOf(mAccountId));
return mContext.getContentResolver().query(builder.build(), Suggestions.AutoComplete.COLUMNS,
null, null, null);
}
@ -200,22 +154,26 @@ public class UserHashtagAutoCompleteAdapter extends SimpleCursorAdapter implemen
@Override
public Cursor swapCursor(final Cursor cursor) {
if (cursor != null) {
mNameIdx = cursor.getColumnIndex(CachedValues.NAME);
mScreenNameIdx = cursor.getColumnIndex(CachedUsers.SCREEN_NAME);
mUserIdIdx = cursor.getColumnIndex(CachedUsers.USER_ID);
mProfileImageUrlIdx = cursor.getColumnIndex(CachedUsers.PROFILE_IMAGE_URL);
mTypeIdx = cursor.getColumnIndex(Suggestions.AutoComplete.TYPE);
mTitleIdx = cursor.getColumnIndex(Suggestions.AutoComplete.TITLE);
mSummaryIdx = cursor.getColumnIndex(Suggestions.AutoComplete.SUMMARY);
mExtraIdIdx = cursor.getColumnIndex(Suggestions.AutoComplete.EXTRA_ID);
mIconIdx = cursor.getColumnIndex(Suggestions.AutoComplete.ICON);
}
return super.swapCursor(cursor);
}
private static boolean isAtSymbol(final char character) {
private static char getNormalizedSymbol(final char character) {
switch (character) {
case '\uff20':
case '@':
return true;
return '@';
case '\uff03':
case '#':
return '#';
}
return false;
return '\0';
}
}

View File

@ -690,7 +690,7 @@ public class MessagesConversationFragment extends BaseSupportFragment implements
@Override
public void afterTextChanged(Editable s) {
Utils.removeLineBreaks(s);
// Utils.removeLineBreaks(s);
mQueryTextChanged = s.length() == 0;
}
});
@ -713,7 +713,6 @@ public class MessagesConversationFragment extends BaseSupportFragment implements
@Override
public void afterTextChanged(final Editable s) {
Utils.removeLineBreaks(s);
mTextChanged = s.length() == 0;
}

View File

@ -195,7 +195,7 @@ public class StatusFragment extends BaseSupportFragment implements LoaderCallbac
if (Utils.hasOfficialAPIAccess(loader.getContext(), mPreferences, account)) {
mStatusAdapter.setReplyError(null);
} else {
final SpannableStringBuilder error = SpannableStringBuilder.valueOf(Html.fromHtml(getString(R.string.cant_load_all_replies_message)));
final SpannableStringBuilder error = SpannableStringBuilder.valueOf(HtmlSpanBuilder.fromHtml(getString(R.string.cant_load_all_replies_message)));
ClickableSpan dialogSpan = null;
for (URLSpan span : error.getSpans(0, error.length(), URLSpan.class)) {
if ("#dialog".equals(span.getURL())) {
@ -860,7 +860,7 @@ public class StatusFragment extends BaseSupportFragment implements LoaderCallbac
quotedNameView.setText(manager.getUserNickname(status.quoted_user_id, status.quoted_user_name, false));
quotedScreenNameView.setText("@" + status.quoted_user_screen_name);
quotedTextView.setText(Html.fromHtml(status.quoted_text_html));
quotedTextView.setText(HtmlSpanBuilder.fromHtml(status.quoted_text_html));
linkify.applyAllLinks(quotedTextView, status.account_id, layoutPosition, status.is_possibly_sensitive);
ThemeUtils.applyParagraphSpacing(quotedTextView, 1.1f);
@ -903,9 +903,9 @@ public class StatusFragment extends BaseSupportFragment implements LoaderCallbac
final String timeString = Utils.formatToLongTimeString(context, timestamp);
if (!TextUtils.isEmpty(timeString) && !TextUtils.isEmpty(status.source)) {
timeSourceView.setText(Html.fromHtml(context.getString(R.string.time_source, timeString, status.source)));
timeSourceView.setText(HtmlSpanBuilder.fromHtml(context.getString(R.string.time_source, timeString, status.source)));
} else if (TextUtils.isEmpty(timeString) && !TextUtils.isEmpty(status.source)) {
timeSourceView.setText(Html.fromHtml(context.getString(R.string.source, status.source)));
timeSourceView.setText(HtmlSpanBuilder.fromHtml(context.getString(R.string.source, status.source)));
} else if (!TextUtils.isEmpty(timeString) && TextUtils.isEmpty(status.source)) {
timeSourceView.setText(timeString);
}

View File

@ -115,6 +115,7 @@ import org.mariotaku.twidere.provider.TwidereDataStore.CachedUsers;
import org.mariotaku.twidere.provider.TwidereDataStore.Filters;
import org.mariotaku.twidere.util.AsyncTwitterWrapper;
import org.mariotaku.twidere.util.ContentValuesCreator;
import org.mariotaku.twidere.util.HtmlSpanBuilder;
import org.mariotaku.twidere.util.KeyboardShortcutsHandler;
import org.mariotaku.twidere.util.KeyboardShortcutsHandler.KeyboardShortcutCallback;
import org.mariotaku.twidere.util.LinkCreator;
@ -506,7 +507,7 @@ public class UserFragment extends BaseSupportFragment implements OnClickListener
}
mScreenNameView.setText("@" + user.screen_name);
mDescriptionContainer.setVisibility(TextUtils.isEmpty(user.description_html) ? View.GONE : View.VISIBLE);
mDescriptionView.setText(user.description_html != null ? Html.fromHtml(user.description_html) : user.description_plain);
mDescriptionView.setText(user.description_html != null ? HtmlSpanBuilder.fromHtml(user.description_html) : user.description_plain);
final TwidereLinkify linkify = new TwidereLinkify(this);
linkify.applyAllLinks(mDescriptionView, user.account_id, false);
mDescriptionView.setMovementMethod(null);

View File

@ -63,6 +63,7 @@ import org.mariotaku.twidere.util.AsyncTaskManager;
import org.mariotaku.twidere.util.AsyncTaskUtils;
import org.mariotaku.twidere.util.AsyncTwitterWrapper.UpdateProfileBannerImageTask;
import org.mariotaku.twidere.util.AsyncTwitterWrapper.UpdateProfileImageTask;
import org.mariotaku.twidere.util.HtmlEscapeHelper;
import org.mariotaku.twidere.util.KeyboardShortcutsHandler;
import org.mariotaku.twidere.util.ParseUtils;
import org.mariotaku.twidere.util.TwitterAPIFactory;
@ -237,6 +238,7 @@ public class UserProfileEditorFragment extends BaseSupportFragment implements On
if (savedInstanceState != null && savedInstanceState.getParcelable(EXTRA_USER) != null) {
final ParcelableUser user = savedInstanceState.getParcelable(EXTRA_USER);
assert user != null;
displayUser(user);
mEditName.setText(savedInstanceState.getString(EXTRA_NAME, user.name));
mEditLocation.setText(savedInstanceState.getString(EXTRA_LOCATION, user.location));
@ -431,7 +433,10 @@ public class UserProfileEditorFragment extends BaseSupportFragment implements On
private static final String DIALOG_FRAGMENT_TAG = "updating_user_profile";
private final UserProfileEditorFragment mFragment;
private final FragmentActivity mActivity;
private final Handler mHandler;
// Data fields
private final long mAccountId;
private final ParcelableUser mOriginal;
private final String mName;
@ -439,7 +444,7 @@ public class UserProfileEditorFragment extends BaseSupportFragment implements On
private final String mLocation;
private final String mDescription;
private final int mLinkColor;
private final FragmentActivity mActivity;
private final int mBackgroundColor;
public UpdateProfileTaskInternal(final UserProfileEditorFragment fragment,
final long accountId, final ParcelableUser original,
@ -456,6 +461,7 @@ public class UserProfileEditorFragment extends BaseSupportFragment implements On
mLocation = location;
mDescription = description;
mLinkColor = linkColor;
mBackgroundColor = backgroundColor;
}
@Override
@ -465,11 +471,12 @@ public class UserProfileEditorFragment extends BaseSupportFragment implements On
User user = null;
if (isProfileChanged()) {
final ProfileUpdate profileUpdate = new ProfileUpdate();
profileUpdate.name(mName);
profileUpdate.name(HtmlEscapeHelper.escapeBasic(mName));
profileUpdate.location(HtmlEscapeHelper.escapeBasic(mLocation));
profileUpdate.description(HtmlEscapeHelper.escapeBasic(mDescription));
profileUpdate.url(mUrl);
profileUpdate.location(mLocation);
profileUpdate.description(mDescription);
profileUpdate.linkColor(mLinkColor);
profileUpdate.backgroundColor(mBackgroundColor);
user = twitter.updateProfile(profileUpdate);
}
if (user == null) {
@ -486,6 +493,7 @@ public class UserProfileEditorFragment extends BaseSupportFragment implements On
final ParcelableUser orig = mOriginal;
if (orig == null) return true;
if (mLinkColor != orig.link_color) return true;
if (mBackgroundColor != orig.background_color) return true;
if (!stringEquals(mName, orig.name)) return true;
if (!stringEquals(mDescription, isEmpty(orig.description_expanded) ? orig.description_plain : orig.description_expanded))
return true;

View File

@ -19,6 +19,7 @@
package org.mariotaku.twidere.provider;
import android.annotation.SuppressLint;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
@ -33,6 +34,7 @@ import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.content.res.Resources;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.database.MergeCursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteFullException;
@ -42,7 +44,6 @@ import android.graphics.BitmapFactory;
import android.graphics.Typeface;
import android.media.AudioManager;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Binder;
import android.os.Handler;
import android.os.Looper;
@ -55,6 +56,7 @@ import android.support.v4.app.NotificationCompat.InboxStyle;
import android.support.v4.util.LongSparseArray;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.style.StyleSpan;
import android.util.Log;
@ -66,8 +68,11 @@ import org.apache.commons.lang3.ArrayUtils;
import org.mariotaku.sqliteqb.library.Columns.Column;
import org.mariotaku.sqliteqb.library.Expression;
import org.mariotaku.sqliteqb.library.OnConflict;
import org.mariotaku.sqliteqb.library.OrderBy;
import org.mariotaku.sqliteqb.library.RawItemArray;
import org.mariotaku.sqliteqb.library.RawSQLLang;
import org.mariotaku.sqliteqb.library.SQLConstants;
import org.mariotaku.sqliteqb.library.SQLFunctions;
import org.mariotaku.sqliteqb.library.SQLQueryBuilder;
import org.mariotaku.sqliteqb.library.SetValue;
import org.mariotaku.sqliteqb.library.query.SQLInsertQuery;
@ -93,8 +98,10 @@ import org.mariotaku.twidere.provider.TwidereDataStore.Drafts;
import org.mariotaku.twidere.provider.TwidereDataStore.Mentions;
import org.mariotaku.twidere.provider.TwidereDataStore.NetworkUsages;
import org.mariotaku.twidere.provider.TwidereDataStore.Preferences;
import org.mariotaku.twidere.provider.TwidereDataStore.SavedSearches;
import org.mariotaku.twidere.provider.TwidereDataStore.SearchHistory;
import org.mariotaku.twidere.provider.TwidereDataStore.Statuses;
import org.mariotaku.twidere.provider.TwidereDataStore.Suggestions;
import org.mariotaku.twidere.provider.TwidereDataStore.UnreadCounts;
import org.mariotaku.twidere.receiver.NotificationReceiver;
import org.mariotaku.twidere.service.BackgroundOperationService;
@ -129,6 +136,8 @@ import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.inject.Inject;
@ -720,7 +729,7 @@ public final class TwidereDataProvider extends ContentProvider implements Consta
case VIRTUAL_TABLE_ID_CACHED_USERS_WITH_SCORE: {
final long accountId = ParseUtils.parseLong(uri.getLastPathSegment(), -1);
final SQLSelectQuery query = CachedUsersQueryBuilder.withScore(projection,
selection, sortOrder, accountId);
selection, sortOrder, accountId, 0);
final Cursor c = mDatabaseWrapper.rawQuery(query.getSQL(), selectionArgs);
setNotificationUri(c, CachedUsers.CONTENT_URI);
return c;
@ -740,6 +749,12 @@ public final class TwidereDataProvider extends ContentProvider implements Consta
setNotificationUri(c, getNotificationUri(tableId, uri));
return c;
}
case VIRTUAL_TABLE_ID_SUGGESTIONS_AUTO_COMPLETE: {
return getAutoCompleteSuggestionsCursor(uri);
}
case VIRTUAL_TABLE_ID_SUGGESTIONS_SEARCH: {
return getSearchSuggestionCursor(uri);
}
}
if (table == null) return null;
final Cursor c = mDatabaseWrapper.query(table, projection, selection, selectionArgs, null, null, sortOrder);
@ -750,6 +765,131 @@ public final class TwidereDataProvider extends ContentProvider implements Consta
}
}
private static final Pattern PATTERN_SCREEN_NAME = Pattern.compile("(?i)[@\uFF20]?([a-z0-9_]{1,20})");
private Cursor getSearchSuggestionCursor(Uri uri) {
final String query = uri.getQueryParameter(QUERY_PARAM_QUERY);
final long accountId = ParseUtils.parseLong(uri.getQueryParameter(QUERY_PARAM_ACCOUNT_ID), -1);
if (query == null || accountId <= 0) return null;
final ContentResolver resolver = getContentResolver();
final boolean emptyQuery = TextUtils.isEmpty(query);
final String queryEscaped = query.replace("_", "^_");
final Cursor[] cursors;
final String[] historyProjection = {
new Column(SearchHistory._ID, Suggestions.Search._ID).getSQL(),
new Column("'" + Suggestions.Search.TYPE_SEARCH_HISTORY + "'", Suggestions.Search.TYPE).getSQL(),
new Column(SearchHistory.QUERY, Suggestions.Search.TITLE).getSQL(),
new Column(SQLConstants.NULL, Suggestions.Search.SUMMARY).getSQL(),
new Column(SQLConstants.NULL, Suggestions.Search.ICON).getSQL(),
new Column("0", Suggestions.Search.EXTRA_ID).getSQL(),
new Column(SQLConstants.NULL, Suggestions.Search.EXTRA).getSQL()
};
final Expression historySelection = Expression.likeRaw(new Column(SearchHistory.QUERY), "?||'%'", "^");
@SuppressLint("Recycle") final Cursor historyCursor = mDatabaseWrapper.query(true,
SearchHistory.TABLE_NAME, historyProjection, historySelection.getSQL(),
new String[]{queryEscaped}, null, null, SearchHistory.DEFAULT_SORT_ORDER,
TextUtils.isEmpty(query) ? "3" : "2");
if (emptyQuery) {
final String[] savedSearchesProjection = {
new Column(SavedSearches._ID, Suggestions.Search._ID).getSQL(),
new Column("'" + Suggestions.Search.TYPE_SAVED_SEARCH + "'", Suggestions.Search.TYPE).getSQL(),
new Column(SavedSearches.QUERY, Suggestions.Search.TITLE).getSQL(),
new Column(SQLConstants.NULL, Suggestions.Search.SUMMARY).getSQL(),
new Column(SQLConstants.NULL, Suggestions.Search.ICON).getSQL(),
new Column("0", Suggestions.Search.EXTRA_ID).getSQL(),
new Column(SQLConstants.NULL, Suggestions.Search.EXTRA).getSQL()
};
final Expression savedSearchesWhere = Expression.equals(SavedSearches.ACCOUNT_ID, accountId);
@SuppressLint("Recycle") final Cursor savedSearchesCursor = mDatabaseWrapper.query(true,
SavedSearches.TABLE_NAME, savedSearchesProjection, savedSearchesWhere.getSQL(),
null, null, null, SavedSearches.DEFAULT_SORT_ORDER, null);
cursors = new Cursor[2];
cursors[1] = savedSearchesCursor;
} else {
final String[] usersProjection = {
new Column(CachedUsers._ID, Suggestions.Search._ID).getSQL(),
new Column("'" + Suggestions.Search.TYPE_USER + "'", Suggestions.Search.TYPE).getSQL(),
new Column(CachedUsers.NAME, Suggestions.Search.TITLE).getSQL(),
new Column(CachedUsers.SCREEN_NAME, Suggestions.Search.SUMMARY).getSQL(),
new Column(CachedUsers.PROFILE_IMAGE_URL, Suggestions.Search.ICON).getSQL(),
new Column(CachedUsers.USER_ID, Suggestions.Search.EXTRA_ID).getSQL(),
new Column(SQLConstants.NULL, Suggestions.Search.EXTRA).getSQL()
};
final long[] nicknameIds = Utils.getMatchedNicknameIds(query, mUserColorNameManager);
final Expression usersSelection = Expression.or(
Expression.likeRaw(new Column(CachedUsers.SCREEN_NAME), "?||'%'", "^"),
Expression.likeRaw(new Column(CachedUsers.NAME), "?||'%'", "^"),
Expression.in(new Column(CachedUsers.USER_ID), new RawItemArray(nicknameIds)));
final String[] selectionArgs = new String[]{queryEscaped, queryEscaped};
final String[] order = {CachedUsers.LAST_SEEN, "score", CachedUsers.SCREEN_NAME, CachedUsers.NAME};
final boolean[] ascending = {false, false, true, true};
final OrderBy orderBy = new OrderBy(order, ascending);
final SQLSelectQuery usersQuery = CachedUsersQueryBuilder.withScore(usersProjection,
usersSelection.getSQL(), orderBy.getSQL(), accountId, 0);
@SuppressLint("Recycle") final Cursor usersCursor = mDatabaseWrapper.rawQuery(usersQuery.getSQL(), selectionArgs);
final Expression exactUserSelection = Expression.or(Expression.likeRaw(new Column(CachedUsers.SCREEN_NAME), "?", "^"));
final Cursor exactUserCursor = mDatabaseWrapper.query(CachedUsers.TABLE_NAME,
new String[]{SQLFunctions.COUNT()}, exactUserSelection.getSQL(),
new String[]{queryEscaped}, null, null, null, "1");
final boolean hasName = exactUserCursor.moveToPosition(0) && exactUserCursor.getInt(0) > 0;
exactUserCursor.close();
final MatrixCursor screenNameCursor = new MatrixCursor(Suggestions.Search.COLUMNS);
if (!hasName) {
final Matcher m = PATTERN_SCREEN_NAME.matcher(query);
if (m.matches()) {
screenNameCursor.addRow(new Object[]{0, Suggestions.Search.TYPE_SCREEN_NAME,
query, null, null, 0, null});
}
}
cursors = new Cursor[3];
cursors[1] = screenNameCursor;
cursors[2] = usersCursor;
}
cursors[0] = historyCursor;
return new MergeCursor(cursors);
}
private Cursor getAutoCompleteSuggestionsCursor(@NonNull Uri uri) {
final String query = uri.getQueryParameter(QUERY_PARAM_QUERY);
final String type = uri.getQueryParameter(QUERY_PARAM_TYPE);
final String accountId = uri.getQueryParameter(QUERY_PARAM_ACCOUNT_ID);
if (query == null || type == null) return null;
final String queryEscaped = query.replace("_", "^_");
if (Suggestions.AutoComplete.TYPE_USERS.equals(type)) {
final long[] nicknameIds = Utils.getMatchedNicknameIds(query, mUserColorNameManager);
final Expression where = Expression.or(Expression.likeRaw(new Column(CachedUsers.SCREEN_NAME), "?||'%'", "^"),
Expression.likeRaw(new Column(CachedUsers.NAME), "?||'%'", "^"),
Expression.in(new Column(CachedUsers.USER_ID), new RawItemArray(nicknameIds)));
final String[] whereArgs = {queryEscaped, queryEscaped};
final String[] mappedProjection = {
new Column(CachedUsers._ID, Suggestions._ID).getSQL(),
new Column("'" + Suggestions.AutoComplete.TYPE_USERS + "'", Suggestions.TYPE).getSQL(),
new Column(CachedUsers.NAME, Suggestions.TITLE).getSQL(),
new Column(CachedUsers.SCREEN_NAME, Suggestions.SUMMARY).getSQL(),
new Column(CachedUsers.USER_ID, Suggestions.EXTRA_ID).getSQL(),
new Column(CachedUsers.PROFILE_IMAGE_URL, Suggestions.ICON).getSQL(),
};
return query(Uri.withAppendedPath(CachedUsers.CONTENT_URI_WITH_SCORE, accountId),
mappedProjection, where.getSQL(), whereArgs, new OrderBy(new String[]{"score", CachedUsers.LAST_SEEN},
new boolean[]{false, false}).getSQL());
} else if (Suggestions.AutoComplete.TYPE_HASHTAGS.equals(type)) {
final Expression where = Expression.likeRaw(new Column(CachedHashtags.NAME), "?||'%'", "^");
final String[] whereArgs = new String[]{queryEscaped};
final String[] mappedProjection = {
new Column(CachedHashtags._ID, Suggestions._ID).getSQL(),
new Column("'" + Suggestions.AutoComplete.TYPE_HASHTAGS + "'", Suggestions.TYPE).getSQL(),
new Column(CachedHashtags.NAME, Suggestions.TITLE).getSQL(),
new Column("NULL", Suggestions.SUMMARY).getSQL(),
new Column("0", Suggestions.EXTRA_ID).getSQL(),
new Column("NULL", Suggestions.ICON).getSQL(),
};
return query(CachedHashtags.CONTENT_URI, mappedProjection, where.getSQL(),
whereArgs, null);
}
return null;
}
@Override
public int update(@NonNull final Uri uri, final ContentValues values, final String selection, final String[] selectionArgs) {
try {
@ -929,6 +1069,7 @@ public final class TwidereDataProvider extends ContentProvider implements Consta
private ParcelFileDescriptor getCacheFileFd(final String name) throws FileNotFoundException {
if (name == null) return null;
final Context context = getContext();
assert context != null;
final File cacheDir = context.getCacheDir();
final File file = new File(cacheDir, name);
if (!file.exists()) return null;
@ -938,6 +1079,7 @@ public final class TwidereDataProvider extends ContentProvider implements Consta
private ContentResolver getContentResolver() {
if (mContentResolver != null) return mContentResolver;
final Context context = getContext();
assert context != null;
return mContentResolver = context.getContentResolver();
}
@ -959,6 +1101,7 @@ public final class TwidereDataProvider extends ContentProvider implements Consta
private NotificationManager getNotificationManager() {
if (mNotificationManager != null) return mNotificationManager;
final Context context = getContext();
assert context != null;
return mNotificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
}
@ -974,6 +1117,7 @@ public final class TwidereDataProvider extends ContentProvider implements Consta
private Bitmap getProfileImageForNotification(final String profile_image_url) {
final Context context = getContext();
assert context != null;
final Resources res = context.getResources();
final int w = res.getDimensionPixelSize(android.R.dimen.notification_large_icon_width);
final int h = res.getDimensionPixelSize(android.R.dimen.notification_large_icon_height);
@ -1025,7 +1169,6 @@ public final class TwidereDataProvider extends ContentProvider implements Consta
}
private void notifyUnreadCountChanged(final int position) {
final Context context = getContext();
mHandler.post(new Runnable() {
@Override
public void run() {
@ -1409,6 +1552,8 @@ public final class TwidereDataProvider extends ContentProvider implements Consta
}
private void showMessagesNotification(AccountPreferences pref, StringLongPair[] pairs, ContentValues[] valuesArray) {
final Context context = getContext();
assert context != null;
final long accountId = pref.getAccountId();
final long prevOldestId = mReadStateManager.getPosition(TAG_OLDEST_MESSAGES, String.valueOf(accountId));
long oldestId = -1;
@ -1418,7 +1563,6 @@ public final class TwidereDataProvider extends ContentProvider implements Consta
if (messageId <= prevOldestId) return;
}
mReadStateManager.setPosition(TAG_OLDEST_MESSAGES, String.valueOf(accountId), oldestId, false);
final Context context = getContext();
final Resources resources = context.getResources();
final NotificationManager nm = getNotificationManager();
final ArrayList<Expression> orExpressions = new ArrayList<>();
@ -1574,31 +1718,4 @@ public final class TwidereDataProvider extends ContentProvider implements Consta
mNameFirst = mPreferences.getBoolean(KEY_NAME_FIRST, false);
}
@SuppressWarnings("unused")
private static class GetWritableDatabaseTask extends AsyncTask<Object, Object, SQLiteDatabase> {
private final Context mContext;
private final SQLiteOpenHelper mHelper;
private final SQLiteDatabaseWrapper mWrapper;
GetWritableDatabaseTask(final Context context, final SQLiteOpenHelper helper,
final SQLiteDatabaseWrapper wrapper) {
mContext = context;
mHelper = helper;
mWrapper = wrapper;
}
@Override
protected SQLiteDatabase doInBackground(final Object... params) {
return mHelper.getWritableDatabase();
}
@Override
protected void onPostExecute(final SQLiteDatabase result) {
mWrapper.setSQLiteDatabase(result);
if (result != null) {
mContext.sendBroadcast(new Intent(BROADCAST_DATABASE_READY));
}
}
}
}

View File

@ -78,9 +78,13 @@ public class CacheUsersStatusesTask extends AsyncTask<TwitterListResponse<Status
values.put(CachedHashtags.NAME, hashtag);
hashTagValues.add(values);
}
usersValues.add(createCachedUser(status.getUser()));
final ContentValues cachedUser = createCachedUser(status.getUser());
cachedUser.put(CachedUsers.LAST_SEEN, System.currentTimeMillis());
usersValues.add(cachedUser);
if (status.isRetweet()) {
usersValues.add(createCachedUser(status.getRetweetedStatus().getUser()));
final ContentValues cachedRetweetedUser = createCachedUser(status.getRetweetedStatus().getUser());
cachedRetweetedUser.put(CachedUsers.LAST_SEEN, System.currentTimeMillis());
usersValues.add(cachedRetweetedUser);
}
bulkInsert(resolver, CachedStatuses.CONTENT_URI, statusesValues);

View File

@ -19,8 +19,10 @@
package org.mariotaku.twidere.util;
import android.graphics.Typeface;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.style.StyleSpan;
import android.text.style.URLSpan;
import org.apache.commons.lang3.StringUtils;
@ -33,6 +35,7 @@ import org.attoparser.markup.html.elements.IHtmlElement;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
/**
@ -60,10 +63,20 @@ public class HtmlSpanBuilder {
}
private static Object createSpan(TagInfo info) {
switch (info.name) {
switch (info.name.toLowerCase(Locale.US)) {
case "a": {
return new URLSpan(info.getAttribute("href"));
}
case "b":
case "strong": {
return new StyleSpan(Typeface.BOLD);
}
case "em":
case "cite":
case "dfn":
case "i": {
return new StyleSpan(Typeface.ITALIC);
}
}
return null;
}

View File

@ -1,400 +0,0 @@
/*
* Copyright 2013 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.mariotaku.twidere.util;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.graphics.Rect;
import android.os.SystemClock;
import android.support.annotation.NonNull;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.ViewPropertyAnimator;
import android.widget.AbsListView;
import android.widget.ListView;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* A {@link View.OnTouchListener} that makes the list items in a {@link ListView}
* dismissable. {@link ListView} is given special treatment because by default it handles touches
* for its list items... i.e. it's in charge of drawing the pressed state (the list selector),
* handling list item clicks, etc.
* <p/>
* <p>After creating the listener, the caller should also call
* {@link ListView#setOnScrollListener(AbsListView.OnScrollListener)}, passing
* in the scroll listener returned by {@link #makeScrollListener()}. If a scroll listener is
* already assigned, the caller should still pass scroll changes through to this listener. This will
* ensure that this {@link SwipeDismissListViewTouchListener} is paused during list view
* scrolling.</p>
* <p/>
* <p>Example usage:</p>
* <p/>
* <pre>
* SwipeDismissListViewTouchListener touchListener =
* new SwipeDismissListViewTouchListener(
* listView,
* new SwipeDismissListViewTouchListener.OnDismissCallback() {
* public void onDismiss(ListView listView, int[] reverseSortedPositions) {
* for (int position : reverseSortedPositions) {
* adapter.remove(adapter.getItem(position));
* }
* adapter.notifyDataSetChanged();
* }
* });
* listView.setOnTouchListener(touchListener);
* listView.setOnScrollListener(touchListener.makeScrollListener());
* </pre>
* <p/>
* <p>This class Requires API level 12 or later due to use of {@link
* ViewPropertyAnimator}.</p>
*/
public class SwipeDismissListViewTouchListener implements View.OnTouchListener {
// Cached ViewConfiguration and system-wide constant values
private int mSlop;
private int mMinFlingVelocity;
private int mMaxFlingVelocity;
private long mAnimationTime;
// Fixed properties
private ListView mListView;
private DismissCallbacks mCallbacks;
private int mViewWidth = 1; // 1 and not 0 to prevent dividing by zero
// Transient properties
private List<PendingDismissData> mPendingDismisses = new ArrayList<>();
private int mDismissAnimationRefCount = 0;
private float mDownX;
private float mDownY;
private boolean mSwiping;
private int mSwipingSlop;
private VelocityTracker mVelocityTracker;
private int mDownPosition;
private View mDownView;
private boolean mPaused;
/**
* The callback interface used by {@link SwipeDismissListViewTouchListener} to inform its client
* about a successful dismissal of one or more list item positions.
*/
public interface DismissCallbacks {
/**
* Called to determine whether the given position can be dismissed.
*/
boolean canDismiss(int position);
/**
* Called when the user has indicated they she would like to dismiss one or more list item
* positions.
*
* @param listView The originating {@link ListView}.
* @param reverseSortedPositions An array of positions to dismiss, sorted in descending
* order for convenience.
*/
void onDismiss(ListView listView, int[] reverseSortedPositions);
}
/**
* Constructs a new swipe-to-dismiss touch listener for the given list view.
*
* @param listView The list view whose items should be dismissable.
* @param callbacks The callback to trigger when the user has indicated that she would like to
* dismiss one or more list items.
*/
public SwipeDismissListViewTouchListener(ListView listView, DismissCallbacks callbacks) {
ViewConfiguration vc = ViewConfiguration.get(listView.getContext());
mSlop = vc.getScaledTouchSlop();
mMinFlingVelocity = vc.getScaledMinimumFlingVelocity() * 16;
mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity();
mAnimationTime = listView.getContext().getResources().getInteger(
android.R.integer.config_shortAnimTime);
mListView = listView;
mCallbacks = callbacks;
}
/**
* Enables or disables (pauses or resumes) watching for swipe-to-dismiss gestures.
*
* @param enabled Whether or not to watch for gestures.
*/
public void setEnabled(boolean enabled) {
mPaused = !enabled;
}
/**
* Returns an {@link AbsListView.OnScrollListener} to be added to the {@link
* ListView} using {@link ListView#setOnScrollListener(AbsListView.OnScrollListener)}.
* If a scroll listener is already assigned, the caller should still pass scroll changes through
* to this listener. This will ensure that this {@link SwipeDismissListViewTouchListener} is
* paused during list view scrolling.</p>
*
* @see SwipeDismissListViewTouchListener
*/
public AbsListView.OnScrollListener makeScrollListener() {
return new AbsListView.OnScrollListener() {
@Override
public void onScrollStateChanged(AbsListView absListView, int scrollState) {
setEnabled(scrollState != AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
}
@Override
public void onScroll(AbsListView absListView, int i, int i1, int i2) {
}
};
}
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
if (mViewWidth < 2) {
mViewWidth = mListView.getWidth();
}
switch (motionEvent.getActionMasked()) {
case MotionEvent.ACTION_DOWN: {
if (mPaused) {
return false;
}
// TODO: ensure this is a finger, and set a flag
// Find the child view that was touched (perform a hit test)
Rect rect = new Rect();
int childCount = mListView.getChildCount();
int[] listViewCoords = new int[2];
mListView.getLocationOnScreen(listViewCoords);
int x = (int) motionEvent.getRawX() - listViewCoords[0];
int y = (int) motionEvent.getRawY() - listViewCoords[1];
View child;
for (int i = 0; i < childCount; i++) {
child = mListView.getChildAt(i);
child.getHitRect(rect);
if (rect.contains(x, y)) {
mDownView = child;
break;
}
}
if (mDownView != null) {
mDownX = motionEvent.getRawX();
mDownY = motionEvent.getRawY();
mDownPosition = mListView.getPositionForView(mDownView);
if (mCallbacks.canDismiss(mDownPosition)) {
mVelocityTracker = VelocityTracker.obtain();
mVelocityTracker.addMovement(motionEvent);
} else {
mDownView = null;
}
}
return false;
}
case MotionEvent.ACTION_CANCEL: {
if (mVelocityTracker == null) {
break;
}
if (mDownView != null && mSwiping) {
// cancel
mDownView.animate()
.translationX(0)
.alpha(1)
.setDuration(mAnimationTime)
.setListener(null);
}
mVelocityTracker.recycle();
mVelocityTracker = null;
mDownX = 0;
mDownY = 0;
mDownView = null;
mDownPosition = ListView.INVALID_POSITION;
mSwiping = false;
break;
}
case MotionEvent.ACTION_UP: {
if (mVelocityTracker == null) {
break;
}
float deltaX = motionEvent.getRawX() - mDownX;
mVelocityTracker.addMovement(motionEvent);
mVelocityTracker.computeCurrentVelocity(1000);
float velocityX = mVelocityTracker.getXVelocity();
float absVelocityX = Math.abs(velocityX);
float absVelocityY = Math.abs(mVelocityTracker.getYVelocity());
boolean dismiss = false;
boolean dismissRight = false;
if (Math.abs(deltaX) > mViewWidth / 2 && mSwiping) {
dismiss = true;
dismissRight = deltaX > 0;
} else if (mMinFlingVelocity <= absVelocityX && absVelocityX <= mMaxFlingVelocity
&& absVelocityY < absVelocityX && mSwiping) {
// dismiss only if flinging in the same direction as dragging
dismiss = (velocityX < 0) == (deltaX < 0);
dismissRight = mVelocityTracker.getXVelocity() > 0;
}
if (dismiss && mDownPosition != ListView.INVALID_POSITION) {
// dismiss
final View downView = mDownView; // mDownView gets null'd before animation ends
final int downPosition = mDownPosition;
++mDismissAnimationRefCount;
mDownView.animate()
.translationX(dismissRight ? mViewWidth : -mViewWidth)
.alpha(0)
.setDuration(mAnimationTime)
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
performDismiss(downView, downPosition);
}
});
} else {
// cancel
mDownView.animate()
.translationX(0)
.alpha(1)
.setDuration(mAnimationTime)
.setListener(null);
}
mVelocityTracker.recycle();
mVelocityTracker = null;
mDownX = 0;
mDownY = 0;
mDownView = null;
mDownPosition = ListView.INVALID_POSITION;
mSwiping = false;
break;
}
case MotionEvent.ACTION_MOVE: {
if (mVelocityTracker == null || mPaused) {
break;
}
mVelocityTracker.addMovement(motionEvent);
float deltaX = motionEvent.getRawX() - mDownX;
float deltaY = motionEvent.getRawY() - mDownY;
if (Math.abs(deltaX) > mSlop && Math.abs(deltaY) < Math.abs(deltaX) / 2) {
mSwiping = true;
mSwipingSlop = (deltaX > 0 ? mSlop : -mSlop);
mListView.requestDisallowInterceptTouchEvent(true);
// Cancel ListView's touch (un-highlighting the item)
MotionEvent cancelEvent = MotionEvent.obtain(motionEvent);
cancelEvent.setAction(MotionEvent.ACTION_CANCEL |
(motionEvent.getActionIndex()
<< MotionEvent.ACTION_POINTER_INDEX_SHIFT));
mListView.onTouchEvent(cancelEvent);
cancelEvent.recycle();
}
if (mSwiping) {
mDownView.setTranslationX(deltaX - mSwipingSlop);
mDownView.setAlpha(Math.max(0f, Math.min(1f,
1f - 2f * Math.abs(deltaX) / mViewWidth)));
return true;
}
break;
}
}
return false;
}
class PendingDismissData implements Comparable<PendingDismissData> {
public int position;
public View view;
public PendingDismissData(int position, View view) {
this.position = position;
this.view = view;
}
@Override
public int compareTo(@NonNull PendingDismissData other) {
// Sort by descending position
return other.position - position;
}
}
private void performDismiss(final View dismissView, final int dismissPosition) {
// Animate the dismissed list item to zero-height and fire the dismiss callback when
// all dismissed list item animations have completed. This triggers layout on each animation
// frame; in the future we may want to do something smarter and more performant.
final ViewGroup.LayoutParams lp = dismissView.getLayoutParams();
final int originalHeight = dismissView.getHeight();
ValueAnimator animator = ValueAnimator.ofInt(originalHeight, 1).setDuration(mAnimationTime);
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
--mDismissAnimationRefCount;
if (mDismissAnimationRefCount == 0) {
// No active animations, process all pending dismisses.
// Sort by descending position
Collections.sort(mPendingDismisses);
int[] dismissPositions = new int[mPendingDismisses.size()];
for (int i = mPendingDismisses.size() - 1; i >= 0; i--) {
dismissPositions[i] = mPendingDismisses.get(i).position;
}
mCallbacks.onDismiss(mListView, dismissPositions);
// Reset mDownPosition to avoid MotionEvent.ACTION_UP trying to start a dismiss
// animation with a stale position
mDownPosition = ListView.INVALID_POSITION;
ViewGroup.LayoutParams lp;
for (PendingDismissData pendingDismiss : mPendingDismisses) {
// Reset view presentation
pendingDismiss.view.setAlpha(1f);
pendingDismiss.view.setTranslationX(0);
lp = pendingDismiss.view.getLayoutParams();
lp.height = originalHeight;
pendingDismiss.view.setLayoutParams(lp);
}
// Send a cancel event
long time = SystemClock.uptimeMillis();
MotionEvent cancelEvent = MotionEvent.obtain(time, time,
MotionEvent.ACTION_CANCEL, 0, 0, 0);
mListView.dispatchTouchEvent(cancelEvent);
mPendingDismisses.clear();
}
}
});
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
lp.height = (Integer) valueAnimator.getAnimatedValue();
dismissView.setLayoutParams(lp);
}
});
mPendingDismisses.add(new PendingDismissData(dismissPosition, dismissView));
animator.start();
}
}

View File

@ -101,16 +101,6 @@ public class TwidereColorUtils {
return getContrastYIQ(color, 128, colorDark, colorLight);
}
@Deprecated
public static int getContrastYIQ(int color) {
return getContrastYIQ(color, 128);
}
@Deprecated
public static int getContrastYIQ(int color, int threshold) {
return getContrastYIQ(color, threshold, Color.BLACK, Color.WHITE);
}
/**
* Get most contrasting color
*

View File

@ -78,10 +78,8 @@ public class TwidereQueryBuilder {
return qb.build();
}
public static SQLSelectQuery withScore(final String[] projection,
final String selection,
final String sortOrder,
final long accountId) {
public static SQLSelectQuery withScore(final String[] projection, final String selection,
final String sortOrder, final long accountId, final int limit) {
final SQLSelectQuery.Builder qb = new SQLSelectQuery.Builder();
final Selectable select = Utils.getColumnsFromProjection(projection);
final Column[] columns = new Column[CachedUsers.COLUMNS.length + 1];
@ -106,6 +104,9 @@ public class TwidereQueryBuilder {
if (sortOrder != null) {
qb.orderBy(new OrderBy(sortOrder));
}
if (limit > 0) {
qb.limit(limit);
}
return qb.build();
}
@ -231,7 +232,7 @@ public class TwidereQueryBuilder {
}
qb.where(where);
qb.groupBy(Utils.getColumnsFromProjection(ConversationEntries.CONVERSATION_ID, DirectMessages.ACCOUNT_ID));
qb.orderBy(new OrderBy(ConversationEntries.MESSAGE_TIMESTAMP ,false));
qb.orderBy(new OrderBy(ConversationEntries.MESSAGE_TIMESTAMP, false));
return qb.build();
}

View File

@ -28,7 +28,7 @@ import com.twitter.Validator;
* Created by mariotaku on 15/4/29.
*/
public class TwitterValidatorMETLengthChecker extends METLengthChecker {
private Validator mValidator;
private final Validator mValidator;
public TwitterValidatorMETLengthChecker(@NonNull Validator validator) {
mValidator = validator;

View File

@ -84,7 +84,6 @@ import android.support.v4.view.accessibility.AccessibilityEventCompat;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.ShareActionProvider;
import android.system.ErrnoException;
import android.text.Editable;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.format.DateFormat;
@ -223,6 +222,7 @@ import org.mariotaku.twidere.provider.TwidereDataStore.Preferences;
import org.mariotaku.twidere.provider.TwidereDataStore.SavedSearches;
import org.mariotaku.twidere.provider.TwidereDataStore.SearchHistory;
import org.mariotaku.twidere.provider.TwidereDataStore.Statuses;
import org.mariotaku.twidere.provider.TwidereDataStore.Suggestions;
import org.mariotaku.twidere.provider.TwidereDataStore.Tabs;
import org.mariotaku.twidere.provider.TwidereDataStore.UnreadCounts;
import org.mariotaku.twidere.service.RefreshService;
@ -363,6 +363,12 @@ public final class Utils implements Constants {
VIRTUAL_TABLE_ID_DRAFTS_UNSENT);
CONTENT_PROVIDER_URI_MATCHER.addURI(TwidereDataStore.AUTHORITY, Drafts.CONTENT_PATH_NOTIFICATIONS,
VIRTUAL_TABLE_ID_DRAFTS_NOTIFICATIONS);
CONTENT_PROVIDER_URI_MATCHER.addURI(TwidereDataStore.AUTHORITY, Drafts.CONTENT_PATH_NOTIFICATIONS,
VIRTUAL_TABLE_ID_DRAFTS_NOTIFICATIONS);
CONTENT_PROVIDER_URI_MATCHER.addURI(TwidereDataStore.AUTHORITY, Suggestions.AutoComplete.CONTENT_PATH,
VIRTUAL_TABLE_ID_SUGGESTIONS_AUTO_COMPLETE);
CONTENT_PROVIDER_URI_MATCHER.addURI(TwidereDataStore.AUTHORITY, Suggestions.Search.CONTENT_PATH,
VIRTUAL_TABLE_ID_SUGGESTIONS_SEARCH);
LINK_HANDLER_URI_MATCHER.addURI(AUTHORITY_STATUS, null, LINK_ID_STATUS);
LINK_HANDLER_URI_MATCHER.addURI(AUTHORITY_USER, null, LINK_ID_USER);
@ -1625,21 +1631,6 @@ public final class Utils implements Constants {
return textView;
}
public static boolean removeLineBreaks(Editable s) {
boolean deleted = false;
try {
for (int i = s.length() - 1; i >= 0; i--) {
if (s.charAt(i) == '\n') {
s.delete(i, i + 1);
deleted |= true;
}
}
} catch (IndexOutOfBoundsException e) {
throw new IndexOutOfBoundsException("Error processing " + s + ", original message: " + e.getMessage());
}
return deleted;
}
public static boolean setLastSeen(Context context, UserMentionEntity[] entities, long time) {
if (entities == null) return false;
boolean result = false;
@ -3386,6 +3377,9 @@ public final class Utils implements Constants {
final Intent shareIntent = createStatusShareIntent(context, status);
shareSubMenu.removeGroup(MENU_GROUP_STATUS_SHARE);
addIntentToMenu(context, shareSubMenu, shareIntent, MENU_GROUP_STATUS_SHARE);
} else {
final Intent shareIntent = createStatusShareIntent(context, status);
shareItem.setIntent(Intent.createChooser(shareIntent, context.getString(R.string.share_status)));
}
}

View File

@ -31,26 +31,29 @@ public class StatusTextTokenizer implements MultiAutoCompleteTextView.Tokenizer
@Override
public int findTokenStart(CharSequence text, int cursor) {
int i = cursor;
// Search backward to find start symbol
while (i > 0 && !isStartSymbol(text.charAt(i - 1))) {
int i = cursor - 1;
int len = text.length();
while (i >= 0 && i < len && !isStartSymbol(text.charAt(i))) {
i--;
}
if (i < 0) return cursor;
return i;
}
@Override
public int findTokenEnd(CharSequence text, int cursor) {
int i = cursor;
int i = cursor - 1;
int len = text.length();
// Search backward to find start symbol
while (i > 0 && !isStartSymbol(text.charAt(i - 1))) {
while (i >= 0 && i < len && isStartSymbol(text.charAt(i))) {
i--;
}
// Search forward to find space
while (i < len && !isSpace(text.charAt(i))) {
i++;
}
if (i < 0) return cursor;
return i;
}

View File

@ -51,7 +51,7 @@ public class ComposeEditText extends AppCompatMultiAutoCompleteTextView {
protected void onAttachedToWindow() {
super.onAttachedToWindow();
if (!isInEditMode() && mAdapter == null) {
mAdapter = new UserHashtagAutoCompleteAdapter(this);
mAdapter = new UserHashtagAutoCompleteAdapter(getContext());
}
setAdapter(mAdapter);
updateAccountId();

View File

@ -62,7 +62,7 @@ public class ComposeMaterialEditText extends AppCompatMultiAutoCompleteTextView
protected void onAttachedToWindow() {
super.onAttachedToWindow();
if (!isInEditMode() && mAdapter == null) {
mAdapter = new UserHashtagAutoCompleteAdapter(this);
mAdapter = new UserHashtagAutoCompleteAdapter(getContext());
}
setAdapter(mAdapter);
updateAccountId();

View File

@ -35,6 +35,8 @@ import org.mariotaku.twidere.R;
import org.mariotaku.twidere.adapter.MessageConversationAdapter;
import org.mariotaku.twidere.model.ParcelableDirectMessage.CursorIndices;
import org.mariotaku.twidere.model.ParcelableMedia;
import org.mariotaku.twidere.util.HtmlSpanBuilder;
import org.mariotaku.twidere.util.JsonSerializer;
import org.mariotaku.twidere.util.MediaLoaderWrapper;
import org.mariotaku.twidere.util.StatusActionModeCallback;
import org.mariotaku.twidere.util.ThemeUtils;
@ -83,8 +85,8 @@ public class MessageViewHolder extends ViewHolder implements OnMediaClickListene
final long accountId = cursor.getLong(indices.account_id);
final long timestamp = cursor.getLong(indices.message_timestamp);
final ParcelableMedia[] media = ParcelableMedia.fromSerializedJson(cursor.getString(indices.media));
textView.setText(Html.fromHtml(cursor.getString(indices.text)));
final ParcelableMedia[] media = JsonSerializer.parseArray(cursor.getString(indices.media), ParcelableMedia.class);
textView.setText(HtmlSpanBuilder.fromHtml(cursor.getString(indices.text)));
linkify.applyAllLinks(textView, accountId, false);
time.setText(Utils.formatToLongTimeString(context, timestamp));
mediaContainer.setVisibility(media != null && media.length > 0 ? View.VISIBLE : View.GONE);

View File

@ -24,6 +24,7 @@ import org.mariotaku.twidere.model.ParcelableLocation;
import org.mariotaku.twidere.model.ParcelableMedia;
import org.mariotaku.twidere.model.ParcelableStatus;
import org.mariotaku.twidere.util.AsyncTwitterWrapper;
import org.mariotaku.twidere.util.HtmlSpanBuilder;
import org.mariotaku.twidere.util.MediaLoaderWrapper;
import org.mariotaku.twidere.util.MediaLoadingHandler;
import org.mariotaku.twidere.util.SharedPreferencesWrapper;
@ -118,7 +119,7 @@ public class StatusViewHolder extends ViewHolder implements Constants, OnClickLi
nameView.setScreenName("@" + TWIDERE_PREVIEW_SCREEN_NAME);
nameView.updateText();
if (adapter.getLinkHighlightingStyle() == VALUE_LINK_HIGHLIGHT_OPTION_CODE_NONE) {
textView.setText(Html.fromHtml(TWIDERE_PREVIEW_TEXT_HTML));
textView.setText(HtmlSpanBuilder.fromHtml(TWIDERE_PREVIEW_TEXT_HTML));
adapter.getTwidereLinkify().applyAllLinks(textView, -1, -1, false, adapter.getLinkHighlightingStyle());
} else {
textView.setText(toPlainText(TWIDERE_PREVIEW_TEXT_HTML));
@ -180,7 +181,7 @@ public class StatusViewHolder extends ViewHolder implements Constants, OnClickLi
if (adapter.getLinkHighlightingStyle() != VALUE_LINK_HIGHLIGHT_OPTION_CODE_NONE
&& !TextUtils.isEmpty(status.quoted_text_html)) {
final Spanned text = Html.fromHtml(status.quoted_text_html);
final Spanned text = HtmlSpanBuilder.fromHtml(status.quoted_text_html);
quotedTextView.setText(text);
linkify.applyAllLinks(quotedTextView, status.account_id, getLayoutPosition(),
status.is_possibly_sensitive, adapter.getLinkHighlightingStyle());
@ -260,7 +261,7 @@ public class StatusViewHolder extends ViewHolder implements Constants, OnClickLi
} else if (adapter.getLinkHighlightingStyle() == VALUE_LINK_HIGHLIGHT_OPTION_CODE_NONE) {
textView.setText(status.text_unescaped);
} else {
textView.setText(Html.fromHtml(status.text_html));
textView.setText(HtmlSpanBuilder.fromHtml(status.text_html));
linkify.applyAllLinks(textView, status.account_id, getLayoutPosition(),
status.is_possibly_sensitive,
adapter.getLinkHighlightingStyle());

View File

@ -23,7 +23,6 @@
android:id="@id/share"
android:icon="@drawable/ic_action_share"
android:title="@string/share"
app:actionProviderClass="org.mariotaku.twidere.menu.SupportStatusShareProvider"
app:showAsAction="always" />
<item
android:id="@id/copy"