diff --git a/build.gradle b/build.gradle
index 43d71c338f..4c00f12fa7 100644
--- a/build.gradle
+++ b/build.gradle
@@ -59,8 +59,8 @@ dependencies {
compile 'pl.tajchert:waitingdots:0.1.0'
compile 'com.soundcloud.android:android-crop:0.9.10@aar'
- compile 'com.android.support:appcompat-v7:22.1.1'
- compile 'com.android.support:recyclerview-v7:21.0.3'
+ compile 'com.android.support:appcompat-v7:22.2.1'
+ compile 'com.android.support:recyclerview-v7:22.2.1'
compile 'com.melnykov:floatingactionbutton:1.3.0'
compile 'com.google.zxing:android-integration:3.1.0'
compile ('com.android.support:support-v4-preferencefragment:1.0.0@aar'){
@@ -119,8 +119,8 @@ dependencyVerification {
'com.afollestad:material-dialogs:624dffff240533ca69414464f416c81c88c5c29689bb169093b9a333104f2471',
'pl.tajchert:waitingdots:2835d49e0787dbcb606c5a60021ced66578503b1e9fddcd7a5ef0cd5f095ba2c',
'com.soundcloud.android:android-crop:ffd4b973cf6e97f7d64118a0dc088df50e9066fd5634fe6911dd0c0c5d346177',
- 'com.android.support:appcompat-v7:9a2355537c2f01cf0b95523605c18606b8d824017e6e94a05c77b0cfc8f21c96',
- 'com.android.support:recyclerview-v7:e525ad3f33c84bb12b73d2dc975b55364a53f0f2d0697e043efba59ba73e22d2',
+ 'com.android.support:appcompat-v7:4b5ccba8c4557ef04f99aa0a80f8aa7d50f05f926a709010a54afd5c878d3618',
+ 'com.android.support:recyclerview-v7:b0f530a5b14334d56ce0de85527ffe93ac419bc928e2884287ce1dddfedfb505',
'com.melnykov:floatingactionbutton:15d58d4fac0f7a288d0e5301bbaf501a146f5b3f5921277811bf99bd3b397263',
'com.google.zxing:android-integration:89e56aadf1164bd71e57949163c53abf90af368b51669c0d4a47a163335f95c4',
'com.android.support:support-v4-preferencefragment:5470f5872514a6226fa1fc6f4e000991f38805691c534cf0bd2778911fc773ad',
@@ -149,8 +149,8 @@ dependencyVerification {
'com.fasterxml.jackson.core:jackson-annotations:0ca408c24202a7626ec8b861e99d85eca5e38b73311dd6dd12e3e9deecc3fe94',
'com.fasterxml.jackson.core:jackson-core:cbf4604784b4de226262845447a1ad3bb38a6728cebe86562e2c5afada8be2c0',
'org.whispersystems:curve25519-java:9ccef8f5aba05d9942336f023c589d6278b4f9135bdc34a7bade1f4e7ad65fa3',
- 'com.android.support:support-v4:1e2e4d35ac7fd30db5ce3bc177b92e4d5af86acef2ef93e9221599d733346f56',
- 'com.android.support:support-annotations:7bc07519aa613b186001160403bcfd68260fa82c61cc7e83adeedc9b862b94ae',
+ 'com.android.support:support-v4:c62f0d025dafa86f423f48df9185b0d89496adbc5f6a9be5a7c394d84cf91423',
+ 'com.android.support:support-annotations:104f353b53d5dd8d64b2f77eece4b37f6b961de9732eb6b706395e91033ec70a',
]
}
diff --git a/res/drawable-hdpi/ic_badge_24dp.png b/res/drawable-hdpi/ic_badge_24dp.png
new file mode 100644
index 0000000000..aaf4fba850
Binary files /dev/null and b/res/drawable-hdpi/ic_badge_24dp.png differ
diff --git a/res/drawable-hdpi/ic_signal_grey_24dp.png b/res/drawable-hdpi/ic_signal_grey_24dp.png
new file mode 100644
index 0000000000..e01390dad8
Binary files /dev/null and b/res/drawable-hdpi/ic_signal_grey_24dp.png differ
diff --git a/res/drawable-hdpi/ic_signal_white_48dp.png b/res/drawable-hdpi/ic_signal_white_48dp.png
new file mode 100644
index 0000000000..28ede6b2a5
Binary files /dev/null and b/res/drawable-hdpi/ic_signal_white_48dp.png differ
diff --git a/res/drawable-mdpi/ic_badge_24dp.png b/res/drawable-mdpi/ic_badge_24dp.png
new file mode 100644
index 0000000000..49b7c74eaf
Binary files /dev/null and b/res/drawable-mdpi/ic_badge_24dp.png differ
diff --git a/res/drawable-mdpi/ic_signal_grey_24dp.png b/res/drawable-mdpi/ic_signal_grey_24dp.png
new file mode 100644
index 0000000000..2399fe616e
Binary files /dev/null and b/res/drawable-mdpi/ic_signal_grey_24dp.png differ
diff --git a/res/drawable-mdpi/ic_signal_white_48dp.png b/res/drawable-mdpi/ic_signal_white_48dp.png
new file mode 100644
index 0000000000..6e9f3c3ed4
Binary files /dev/null and b/res/drawable-mdpi/ic_signal_white_48dp.png differ
diff --git a/res/drawable-v12/recycler_view_fast_scroller_bubble.xml b/res/drawable-v12/recycler_view_fast_scroller_bubble.xml
new file mode 100644
index 0000000000..f135c5c6d6
--- /dev/null
+++ b/res/drawable-v12/recycler_view_fast_scroller_bubble.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/drawable-xhdpi/ic_badge_24dp.png b/res/drawable-xhdpi/ic_badge_24dp.png
new file mode 100644
index 0000000000..15fde7bffd
Binary files /dev/null and b/res/drawable-xhdpi/ic_badge_24dp.png differ
diff --git a/res/drawable-xhdpi/ic_signal_grey_24dp.png b/res/drawable-xhdpi/ic_signal_grey_24dp.png
new file mode 100644
index 0000000000..9088e211df
Binary files /dev/null and b/res/drawable-xhdpi/ic_signal_grey_24dp.png differ
diff --git a/res/drawable-xhdpi/ic_signal_white_48dp.png b/res/drawable-xhdpi/ic_signal_white_48dp.png
new file mode 100644
index 0000000000..189b9ecb44
Binary files /dev/null and b/res/drawable-xhdpi/ic_signal_white_48dp.png differ
diff --git a/res/drawable-xxhdpi/ic_badge_24dp.png b/res/drawable-xxhdpi/ic_badge_24dp.png
new file mode 100644
index 0000000000..e64872158a
Binary files /dev/null and b/res/drawable-xxhdpi/ic_badge_24dp.png differ
diff --git a/res/drawable-xxhdpi/ic_signal_grey_24dp.png b/res/drawable-xxhdpi/ic_signal_grey_24dp.png
new file mode 100644
index 0000000000..d6c3d2bf5b
Binary files /dev/null and b/res/drawable-xxhdpi/ic_signal_grey_24dp.png differ
diff --git a/res/drawable-xxhdpi/ic_signal_white_48dp.png b/res/drawable-xxhdpi/ic_signal_white_48dp.png
new file mode 100644
index 0000000000..a0b322b5bf
Binary files /dev/null and b/res/drawable-xxhdpi/ic_signal_white_48dp.png differ
diff --git a/res/drawable-xxxhdpi/ic_badge_24dp.png b/res/drawable-xxxhdpi/ic_badge_24dp.png
new file mode 100644
index 0000000000..549a87dc66
Binary files /dev/null and b/res/drawable-xxxhdpi/ic_badge_24dp.png differ
diff --git a/res/drawable-xxxhdpi/ic_signal_grey_24dp.png b/res/drawable-xxxhdpi/ic_signal_grey_24dp.png
new file mode 100644
index 0000000000..7df7b11d80
Binary files /dev/null and b/res/drawable-xxxhdpi/ic_signal_grey_24dp.png differ
diff --git a/res/drawable-xxxhdpi/ic_signal_white_48dp.png b/res/drawable-xxxhdpi/ic_signal_white_48dp.png
new file mode 100644
index 0000000000..c17e9ae4d5
Binary files /dev/null and b/res/drawable-xxxhdpi/ic_signal_white_48dp.png differ
diff --git a/res/drawable/badge_drawable.xml b/res/drawable/badge_drawable.xml
new file mode 100644
index 0000000000..5a87e3c781
--- /dev/null
+++ b/res/drawable/badge_drawable.xml
@@ -0,0 +1,6 @@
+
+
+
diff --git a/res/drawable/recycler_view_fast_scroller_bubble.xml b/res/drawable/recycler_view_fast_scroller_bubble.xml
new file mode 100644
index 0000000000..417f6e4bc6
--- /dev/null
+++ b/res/drawable/recycler_view_fast_scroller_bubble.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/drawable/recycler_view_fast_scroller_handle.xml b/res/drawable/recycler_view_fast_scroller_handle.xml
new file mode 100644
index 0000000000..fa2ca8a58a
--- /dev/null
+++ b/res/drawable/recycler_view_fast_scroller_handle.xml
@@ -0,0 +1,17 @@
+
+
+ -
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/layout/contact_selection_list_fragment.xml b/res/layout/contact_selection_list_fragment.xml
index f54bcb98c8..a0c8e9f44a 100644
--- a/res/layout/contact_selection_list_fragment.xml
+++ b/res/layout/contact_selection_list_fragment.xml
@@ -1,5 +1,5 @@
-
@@ -9,8 +9,8 @@
android:layout_width="match_parent"
android:layout_height="wrap_content">
-
@@ -23,4 +23,11 @@
android:textSize="20sp" />
-
+
+
+
+
diff --git a/res/layout/contact_selection_list_item.xml b/res/layout/contact_selection_list_item.xml
index 149350a7bd..ab4ab40aaa 100644
--- a/res/layout/contact_selection_list_item.xml
+++ b/res/layout/contact_selection_list_item.xml
@@ -1,72 +1,79 @@
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="?android:attr/listPreferredItemHeight"
+ android:orientation="horizontal"
+ android:gravity="center_vertical"
+ android:background="@drawable/conversation_list_item_background"
+ android:paddingLeft="48dp"
+ android:paddingRight="20dp">
-
-
-
+ android:layout_weight="1"
+ android:orientation="vertical">
+
+
+
+
+
+
+
+
+
+
-
-
-
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:focusable="false"
+ android:clickable="false" />
diff --git a/res/layout/contact_selection_recyclerview_header.xml b/res/layout/contact_selection_recyclerview_header.xml
new file mode 100644
index 0000000000..82cdc469e0
--- /dev/null
+++ b/res/layout/contact_selection_recyclerview_header.xml
@@ -0,0 +1,9 @@
+
+
diff --git a/res/layout/recycler_view_fast_scroller.xml b/res/layout/recycler_view_fast_scroller.xml
new file mode 100644
index 0000000000..2edd24c130
--- /dev/null
+++ b/res/layout/recycler_view_fast_scroller.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index a36cc0a386..7657dc2565 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -129,7 +129,8 @@
-
+
+
diff --git a/src/org/thoughtcrime/securesms/ContactSelectionListFragment.java b/src/org/thoughtcrime/securesms/ContactSelectionListFragment.java
index bec8d3a138..bcb7c909a9 100644
--- a/src/org/thoughtcrime/securesms/ContactSelectionListFragment.java
+++ b/src/org/thoughtcrime/securesms/ContactSelectionListFragment.java
@@ -18,30 +18,36 @@ package org.thoughtcrime.securesms;
import android.database.Cursor;
+import android.graphics.Canvas;
+import android.graphics.Rect;
import android.os.Build;
+import android.os.Build.VERSION;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.app.Fragment;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.Loader;
-import android.support.v4.widget.CursorAdapter;
+import android.support.v4.view.ViewCompat;
import android.support.v4.widget.SwipeRefreshLayout;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
-import android.widget.AdapterView;
import android.widget.TextView;
+import org.thoughtcrime.securesms.components.RecyclerViewFastScroller;
import org.thoughtcrime.securesms.contacts.ContactSelectionListAdapter;
import org.thoughtcrime.securesms.contacts.ContactSelectionListItem;
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader;
+import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter;
+import org.thoughtcrime.securesms.util.ViewUtil;
+import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
-import se.emilsjolander.stickylistheaders.StickyListHeadersListView;
-
/**
* Fragment for selecting a one or more contacts from a list.
*
@@ -65,9 +71,10 @@ public class ContactSelectionListFragment extends Fragment
private Map selectedContacts;
private OnContactSelectedListener onContactSelectedListener;
- private StickyListHeadersListView listView;
private SwipeRefreshLayout swipeRefresh;
private String cursorFilter;
+ private RecyclerView recyclerView;
+ private RecyclerViewFastScroller fastScroller;
@Override
public void onActivityCreated(Bundle icicle) {
@@ -89,13 +96,12 @@ public class ContactSelectionListFragment extends Fragment
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.contact_selection_list_fragment, container, false);
- emptyText = (TextView) view.findViewById(android.R.id.empty);
- swipeRefresh = (SwipeRefreshLayout) view.findViewById(R.id.swipe_refresh);
- listView = (StickyListHeadersListView) view.findViewById(android.R.id.list);
- listView.setFocusable(true);
- listView.setFastScrollEnabled(true);
- listView.setDrawingListUnderStickyHeader(false);
- listView.setOnItemClickListener(new ListClickListener());
+ emptyText = ViewUtil.findById(view, android.R.id.empty);
+ recyclerView = ViewUtil.findById(view, R.id.recycler_view);
+ swipeRefresh = ViewUtil.findById(view, R.id.swipe_refresh);
+ fastScroller = ViewUtil.findById(view, R.id.fast_scroller);
+ recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
+ fastScroller.setRecyclerView(recyclerView);
swipeRefresh.setEnabled(getActivity().getIntent().getBooleanExtra(REFRESHABLE, true) &&
Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN);
@@ -117,9 +123,13 @@ public class ContactSelectionListFragment extends Fragment
}
private void initializeCursor() {
- ContactSelectionListAdapter adapter = new ContactSelectionListAdapter(getActivity(), null, isMulti());
+ ContactSelectionListAdapter adapter = new ContactSelectionListAdapter(getActivity(),
+ null,
+ new ListClickListener(),
+ isMulti());
selectedContacts = adapter.getSelectedContacts();
- listView.setAdapter(adapter);
+ recyclerView.setAdapter(adapter);
+ recyclerView.addItemDecoration(new StickyHeaderDecoration(adapter, true));
this.getLoaderManager().initLoader(0, null, this);
}
@@ -147,19 +157,18 @@ public class ContactSelectionListFragment extends Fragment
@Override
public void onLoadFinished(Loader loader, Cursor data) {
- ((CursorAdapter) listView.getAdapter()).changeCursor(data);
+ ((CursorRecyclerViewAdapter) recyclerView.getAdapter()).changeCursor(data);
emptyText.setText(R.string.contact_selection_group_activity__no_contacts);
}
@Override
public void onLoaderReset(Loader loader) {
- ((CursorAdapter) listView.getAdapter()).changeCursor(null);
+ ((CursorRecyclerViewAdapter) recyclerView.getAdapter()).changeCursor(null);
}
- private class ListClickListener implements AdapterView.OnItemClickListener {
+ private class ListClickListener implements ContactSelectionListAdapter.ItemClickListener {
@Override
- public void onItemClick(AdapterView> l, View v, int position, long id) {
- ContactSelectionListItem contact = (ContactSelectionListItem)v;
+ public void onItemClick(ContactSelectionListItem contact) {
if (!isMulti() || !selectedContacts.containsKey(contact.getContactId())) {
selectedContacts.put(contact.getContactId(), contact.getNumber());
@@ -185,4 +194,181 @@ public class ContactSelectionListFragment extends Fragment
void onContactSelected(String number);
void onContactDeselected(String number);
}
+
+ /**
+ * A sticky header decoration for android's RecyclerView.
+ */
+ public static class StickyHeaderDecoration extends RecyclerView.ItemDecoration {
+
+ private Map mHeaderCache;
+
+ private StickyHeaderAdapter mAdapter;
+
+ private boolean mRenderInline;
+
+ /**
+ * @param adapter the sticky header adapter to use
+ */
+ public StickyHeaderDecoration(StickyHeaderAdapter adapter, boolean renderInline) {
+ mAdapter = adapter;
+ mHeaderCache = new HashMap<>();
+ mRenderInline = renderInline;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
+ RecyclerView.State state)
+ {
+ int position = parent.getChildAdapterPosition(view);
+
+ int headerHeight = 0;
+ if (position != RecyclerView.NO_POSITION && hasHeader(position)) {
+ View header = getHeader(parent, position).itemView;
+ headerHeight = getHeaderHeightForLayout(header);
+ }
+
+ outRect.set(0, headerHeight, 0, 0);
+ }
+
+ private boolean hasHeader(int position) {
+ if (position == 0) {
+ return true;
+ }
+
+ int previous = position - 1;
+ return mAdapter.getHeaderId(position) != mAdapter.getHeaderId(previous);
+ }
+
+ private RecyclerView.ViewHolder getHeader(RecyclerView parent, int position) {
+ final long key = mAdapter.getHeaderId(position);
+
+ if (mHeaderCache.containsKey(key)) {
+ return mHeaderCache.get(key);
+ } else {
+ final RecyclerView.ViewHolder holder = mAdapter.onCreateHeaderViewHolder(parent);
+ final View header = holder.itemView;
+
+ //noinspection unchecked
+ mAdapter.onBindHeaderViewHolder(holder, position);
+
+ int widthSpec = View.MeasureSpec.makeMeasureSpec(parent.getWidth(), View.MeasureSpec.EXACTLY);
+ int heightSpec = View.MeasureSpec.makeMeasureSpec(parent.getHeight(), View.MeasureSpec.UNSPECIFIED);
+
+ int childWidth = ViewGroup.getChildMeasureSpec(widthSpec,
+ parent.getPaddingLeft() + parent.getPaddingRight(), header.getLayoutParams().width);
+ int childHeight = ViewGroup.getChildMeasureSpec(heightSpec,
+ parent.getPaddingTop() + parent.getPaddingBottom(), header.getLayoutParams().height);
+
+ header.measure(childWidth, childHeight);
+ header.layout(0, 0, header.getMeasuredWidth(), header.getMeasuredHeight());
+
+ mHeaderCache.put(key, holder);
+
+ return holder;
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
+ final int count = parent.getChildCount();
+
+ for (int layoutPos = 0; layoutPos < count; layoutPos++) {
+ final View child = parent.getChildAt(layoutPos);
+
+ final int adapterPos = parent.getChildAdapterPosition(child);
+
+ if (adapterPos != RecyclerView.NO_POSITION && (layoutPos == 0 || hasHeader(adapterPos))) {
+ View header = getHeader(parent, adapterPos).itemView;
+ c.save();
+ final int left = child.getLeft();
+ final int top = getHeaderTop(parent, child, header, adapterPos, layoutPos);
+ c.translate(left, top);
+ header.draw(c);
+ c.restore();
+ }
+ }
+ }
+
+ private int getHeaderTop(RecyclerView parent, View child, View header, int adapterPos,
+ int layoutPos)
+ {
+ int headerHeight = getHeaderHeightForLayout(header);
+ int top = getChildY(parent, child) - headerHeight;
+ if (layoutPos == 0) {
+ final int count = parent.getChildCount();
+ final long currentId = mAdapter.getHeaderId(adapterPos);
+ // find next view with header and compute the offscreen push if needed
+ for (int i = 1; i < count; i++) {
+ int adapterPosHere = parent.getChildAdapterPosition(parent.getChildAt(i));
+ if (adapterPosHere != RecyclerView.NO_POSITION) {
+ long nextId = mAdapter.getHeaderId(adapterPosHere);
+ if (nextId != currentId) {
+ final View next = parent.getChildAt(i);
+ final int offset = getChildY(parent, next) - (headerHeight + getHeader(parent, adapterPosHere).itemView.getHeight());
+ if (offset < 0) {
+ return offset;
+ } else {
+ break;
+ }
+ }
+ }
+ }
+
+ top = Math.max(0, top);
+ }
+
+ return top;
+ }
+
+ private int getChildY(RecyclerView parent, View child) {
+ if (VERSION.SDK_INT < 11) {
+ Rect rect = new Rect();
+ parent.getChildVisibleRect(child, rect, null);
+ return rect.top;
+ } else {
+ return (int)ViewCompat.getY(child);
+ }
+ }
+
+ private int getHeaderHeightForLayout(View header) {
+ return mRenderInline ? 0 : header.getHeight();
+ }
+ }
+
+ /**
+ * The adapter to assist the {@link StickyHeaderDecoration} in creating and binding the header views.
+ *
+ * @param the header view holder
+ */
+ public interface StickyHeaderAdapter {
+
+ /**
+ * Returns the header id for the item at the given position.
+ *
+ * @param position the item position
+ * @return the header id
+ */
+ long getHeaderId(int position);
+
+ /**
+ * Creates a new header ViewHolder.
+ *
+ * @param parent the header's view parent
+ * @return a view holder for the created view
+ */
+ T onCreateHeaderViewHolder(ViewGroup parent);
+
+ /**
+ * Updates the header view to reflect the header data for the given position
+ * @param viewHolder the header view holder
+ * @param position the header's item position
+ */
+ void onBindHeaderViewHolder(T viewHolder, int position);
+ }
}
diff --git a/src/org/thoughtcrime/securesms/components/AvatarImageView.java b/src/org/thoughtcrime/securesms/components/AvatarImageView.java
index b97edadb59..6f7f462141 100644
--- a/src/org/thoughtcrime/securesms/components/AvatarImageView.java
+++ b/src/org/thoughtcrime/securesms/components/AvatarImageView.java
@@ -3,8 +3,13 @@ package org.thoughtcrime.securesms.components;
import android.content.Context;
import android.content.Intent;
import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.LayerDrawable;
+import android.os.AsyncTask;
import android.provider.ContactsContract;
import android.support.annotation.Nullable;
+import android.support.v4.content.ContextCompat;
+import android.support.v4.util.Pair;
import android.util.AttributeSet;
import android.view.View;
import android.widget.ImageView;
@@ -16,10 +21,13 @@ import org.thoughtcrime.securesms.contacts.avatars.ContactPhotoFactory;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.Recipients;
+import org.thoughtcrime.securesms.util.DirectoryHelper;
+import org.thoughtcrime.securesms.util.DirectoryHelper.UserCapabilities.Capability;
public class AvatarImageView extends ImageView {
private boolean inverted;
+ private boolean showBadge;
public AvatarImageView(Context context) {
super(context);
@@ -33,18 +41,22 @@ public class AvatarImageView extends ImageView {
if (attrs != null) {
TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.AvatarImageView, 0, 0);
inverted = typedArray.getBoolean(0, false);
+ showBadge = typedArray.getBoolean(1, false);
typedArray.recycle();
}
}
- public void setAvatar(@Nullable Recipients recipients, boolean quickContactEnabled) {
+ public void setAvatar(final @Nullable Recipients recipients, boolean quickContactEnabled) {
if (recipients != null) {
MaterialColor backgroundColor = recipients.getColor();
setImageDrawable(recipients.getContactPhoto().asDrawable(getContext(), backgroundColor.toConversationColor(getContext()), inverted));
setAvatarClickHandler(recipients, quickContactEnabled);
+ setTag(recipients);
+ if (showBadge) new BadgeResolutionTask(getContext()).execute(recipients);
} else {
setImageDrawable(ContactPhotoFactory.getDefaultContactPhoto(null).asDrawable(getContext(), ContactColors.UNKNOWN_COLOR.toConversationColor(getContext()), inverted));
setOnClickListener(null);
+ setTag(null);
}
}
@@ -74,4 +86,29 @@ public class AvatarImageView extends ImageView {
}
}
+ private class BadgeResolutionTask extends AsyncTask> {
+ private final Context context;
+
+ public BadgeResolutionTask(Context context) {
+ this.context = context;
+ }
+
+ @Override
+ protected Pair doInBackground(Recipients... recipients) {
+ Capability textCapability = DirectoryHelper.getUserCapabilities(context, recipients[0]).getTextCapability();
+ return new Pair<>(recipients[0], textCapability == Capability.SUPPORTED);
+ }
+
+ @Override
+ protected void onPostExecute(Pair result) {
+ if (getTag() == result.first && result.second) {
+ final Drawable badged = new LayerDrawable(new Drawable[] {
+ getDrawable(),
+ ContextCompat.getDrawable(context, R.drawable.badge_drawable)
+ });
+
+ setImageDrawable(badged);
+ }
+ }
+ }
}
diff --git a/src/org/thoughtcrime/securesms/components/RecyclerViewFastScroller.java b/src/org/thoughtcrime/securesms/components/RecyclerViewFastScroller.java
new file mode 100644
index 0000000000..70c7336223
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/components/RecyclerViewFastScroller.java
@@ -0,0 +1,206 @@
+/**
+ * Modified version of
+ * https://github.com/AndroidDeveloperLB/LollipopContactsRecyclerViewFastScroller
+ *
+ * Their license:
+ *
+ * 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.thoughtcrime.securesms.components;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.os.Build.VERSION;
+import android.support.annotation.NonNull;
+import android.support.v4.view.ViewCompat;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewTreeObserver;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.util.Util;
+import org.thoughtcrime.securesms.util.ViewUtil;
+
+public class RecyclerViewFastScroller extends LinearLayout {
+ private static final int BUBBLE_ANIMATION_DURATION = 100;
+ private static final int TRACK_SNAP_RANGE = 5;
+
+ private TextView bubble;
+ private View handle;
+ private RecyclerView recyclerView;
+ private int height;
+ private ObjectAnimator currentAnimator;
+
+ private final RecyclerView.OnScrollListener onScrollListener = new RecyclerView.OnScrollListener() {
+ @Override
+ public void onScrolled(final RecyclerView recyclerView, final int dx, final int dy) {
+ if (bubble == null || handle.isSelected())
+ return;
+ final int verticalScrollOffset = recyclerView.computeVerticalScrollOffset();
+ final int verticalScrollRange = recyclerView.computeVerticalScrollRange();
+ float proportion = (float)verticalScrollOffset / ((float)verticalScrollRange - height);
+ setBubbleAndHandlePosition(height * proportion);
+ }
+ };
+
+ public interface FastScrollAdapter {
+ CharSequence getBubbleText(int pos);
+ }
+
+ public RecyclerViewFastScroller(final Context context) {
+ this(context, null);
+ }
+
+ public RecyclerViewFastScroller(final Context context, final AttributeSet attrs) {
+ super(context, attrs);
+ setOrientation(HORIZONTAL);
+ setClipChildren(false);
+ inflate(context, R.layout.recycler_view_fast_scroller, this);
+ bubble = ViewUtil.findById(this, R.id.fastscroller_bubble);
+ handle = ViewUtil.findById(this, R.id.fastscroller_handle);
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+ height = h;
+ }
+
+ @Override
+ @TargetApi(11)
+ public boolean onTouchEvent(@NonNull MotionEvent event) {
+ final int action = event.getAction();
+ switch (action) {
+ case MotionEvent.ACTION_DOWN:
+ if (event.getX() < ViewUtil.getX(handle) - handle.getPaddingLeft() ||
+ event.getY() < ViewUtil.getY(handle) - handle.getPaddingTop() ||
+ event.getY() > ViewUtil.getY(handle) + handle.getHeight() + handle.getPaddingBottom())
+ {
+ return false;
+ }
+ if (currentAnimator != null) {
+ currentAnimator.cancel();
+ }
+ if (bubble.getVisibility() != VISIBLE) {
+ showBubble();
+ }
+ handle.setSelected(true);
+ case MotionEvent.ACTION_MOVE:
+ final float y = event.getY();
+ setBubbleAndHandlePosition(y);
+ setRecyclerViewPosition(y);
+ return true;
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ handle.setSelected(false);
+ hideBubble();
+ return true;
+ }
+ return super.onTouchEvent(event);
+ }
+
+ public void setRecyclerView(final RecyclerView recyclerView) {
+ this.recyclerView = recyclerView;
+ recyclerView.addOnScrollListener(onScrollListener);
+ recyclerView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
+ @Override
+ public boolean onPreDraw() {
+ recyclerView.getViewTreeObserver().removeOnPreDrawListener(this);
+ if (bubble == null || handle.isSelected())
+ return true;
+ final int verticalScrollOffset = recyclerView.computeVerticalScrollOffset();
+ final int verticalScrollRange = recyclerView.computeVerticalScrollRange();
+ float proportion = (float)verticalScrollOffset / ((float)verticalScrollRange - height);
+ setBubbleAndHandlePosition(height * proportion);
+ return true;
+ }
+ });
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ if (recyclerView != null)
+ recyclerView.removeOnScrollListener(onScrollListener);
+ }
+
+ private void setRecyclerViewPosition(float y) {
+ if (recyclerView != null) {
+ final int itemCount = recyclerView.getAdapter().getItemCount();
+ float proportion;
+ if (ViewUtil.getY(handle) == 0)
+ proportion = 0f;
+ else if (ViewUtil.getY(handle) + handle.getHeight() >= height - TRACK_SNAP_RANGE)
+ proportion = 1f;
+ else
+ proportion = y / (float) height;
+ final int targetPos = Util.clamp((int)(proportion * (float)itemCount), 0, itemCount - 1);
+ ((LinearLayoutManager) recyclerView.getLayoutManager()).scrollToPositionWithOffset(targetPos, 0);
+ final CharSequence bubbleText = ((FastScrollAdapter) recyclerView.getAdapter()).getBubbleText(targetPos);
+ if (bubble != null)
+ bubble.setText(bubbleText);
+ }
+ }
+
+ private void setBubbleAndHandlePosition(float y) {
+ final int handleHeight = handle.getHeight();
+ final int bubbleHeight = bubble.getHeight();
+ ViewUtil.setY(handle, Util.clamp((int)(y - handleHeight / 2), 0, height - handleHeight));
+ ViewUtil.setY(bubble, Util.clamp((int)(y - bubbleHeight), 0, height - bubbleHeight - handleHeight / 2));
+ }
+
+ @TargetApi(11)
+ private void showBubble() {
+ bubble.setVisibility(VISIBLE);
+ if (VERSION.SDK_INT >= 11) {
+ if (currentAnimator != null) currentAnimator.cancel();
+ currentAnimator = ObjectAnimator.ofFloat(bubble, "alpha", 0f, 1f).setDuration(BUBBLE_ANIMATION_DURATION);
+ currentAnimator.start();
+ }
+ }
+
+ @TargetApi(11)
+ private void hideBubble() {
+ if (VERSION.SDK_INT >= 11) {
+ if (currentAnimator != null) currentAnimator.cancel();
+ currentAnimator = ObjectAnimator.ofFloat(bubble, "alpha", 1f, 0f).setDuration(BUBBLE_ANIMATION_DURATION);
+ currentAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ super.onAnimationEnd(animation);
+ bubble.setVisibility(INVISIBLE);
+ currentAnimator = null;
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ super.onAnimationCancel(animation);
+ bubble.setVisibility(INVISIBLE);
+ currentAnimator = null;
+ }
+ });
+ currentAnimator.start();
+ } else {
+ bubble.setVisibility(INVISIBLE);
+ }
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/contacts/ContactSelectionListAdapter.java b/src/org/thoughtcrime/securesms/contacts/ContactSelectionListAdapter.java
index 87ae41d951..d882f02ac1 100644
--- a/src/org/thoughtcrime/securesms/contacts/ContactSelectionListAdapter.java
+++ b/src/org/thoughtcrime/securesms/contacts/ContactSelectionListAdapter.java
@@ -20,101 +20,174 @@ import android.content.Context;
import android.content.res.TypedArray;
import android.database.Cursor;
import android.provider.ContactsContract;
-import android.support.v4.widget.CursorAdapter;
-import android.util.Log;
+import android.support.annotation.DrawableRes;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v7.widget.RecyclerView;
+import android.text.SpannableString;
+import android.text.Spanned;
+import android.text.style.ImageSpan;
import android.view.LayoutInflater;
import android.view.View;
+import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.TextView;
import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.components.RecyclerViewFastScroller.FastScrollAdapter;
+import org.thoughtcrime.securesms.ContactSelectionListFragment.StickyHeaderAdapter;
+import org.thoughtcrime.securesms.contacts.ContactSelectionListAdapter.HeaderViewHolder;
+import org.thoughtcrime.securesms.contacts.ContactSelectionListAdapter.ViewHolder;
+import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter;
import java.util.HashMap;
import java.util.Map;
-import se.emilsjolander.stickylistheaders.StickyListHeadersAdapter;
-
/**
* List adapter to display all contacts and their related information
*
* @author Jake McGinty
*/
-public class ContactSelectionListAdapter extends CursorAdapter
- implements StickyListHeadersAdapter
+public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter
+ implements FastScrollAdapter,
+ StickyHeaderAdapter
{
private final static String TAG = ContactSelectionListAdapter.class.getSimpleName();
private final static int STYLE_ATTRIBUTES[] = new int[]{R.attr.contact_selection_push_user,
R.attr.contact_selection_lay_user};
- private final boolean multiSelect;
- private final LayoutInflater li;
- private final TypedArray drawables;
+ private final boolean multiSelect;
+ private final LayoutInflater li;
+ private final TypedArray drawables;
+ private final ItemClickListener clickListener;
private final HashMap selectedContacts = new HashMap<>();
- public ContactSelectionListAdapter(Context context, Cursor cursor, boolean multiSelect) {
- super(context, cursor, 0);
- this.li = LayoutInflater.from(context);
- this.drawables = context.obtainStyledAttributes(STYLE_ATTRIBUTES);
- this.multiSelect = multiSelect;
+ public static class ViewHolder extends RecyclerView.ViewHolder {
+ public ViewHolder(@NonNull final View itemView,
+ @Nullable final ItemClickListener clickListener)
+ {
+ super(itemView);
+ itemView.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (clickListener != null) clickListener.onItemClick(getView());
+ }
+ });
+ }
+
+ public ContactSelectionListItem getView() {
+ return (ContactSelectionListItem) itemView;
+ }
+ }
+
+ public static class HeaderViewHolder extends RecyclerView.ViewHolder {
+ public HeaderViewHolder(View itemView) {
+ super(itemView);
+ }
+ }
+
+ public ContactSelectionListAdapter(@NonNull Context context,
+ @Nullable Cursor cursor,
+ @Nullable ItemClickListener clickListener,
+ boolean multiSelect)
+ {
+ super(context, cursor);
+ this.li = LayoutInflater.from(context);
+ this.drawables = context.obtainStyledAttributes(STYLE_ATTRIBUTES);
+ this.multiSelect = multiSelect;
+ this.clickListener = clickListener;
}
@Override
- public View newView(Context context, Cursor cursor, ViewGroup parent) {
- return li.inflate(R.layout.contact_selection_list_item, parent, false);
+ public long getHeaderId(int i) {
+ return getHeaderString(i).hashCode();
}
@Override
- public void bindView(View view, Context context, Cursor cursor) {
+ public ViewHolder onCreateItemViewHolder(ViewGroup parent, int viewType) {
+ return new ViewHolder(li.inflate(R.layout.contact_selection_list_item, parent, false), clickListener);
+ }
+
+ @Override
+ public void onBindItemViewHolder(ViewHolder viewHolder, @NonNull Cursor cursor) {
long id = cursor.getLong(cursor.getColumnIndexOrThrow(ContactsDatabase.ID_COLUMN));
int contactType = cursor.getInt(cursor.getColumnIndexOrThrow(ContactsDatabase.CONTACT_TYPE_COLUMN));
String name = cursor.getString(cursor.getColumnIndexOrThrow(ContactsDatabase.NAME_COLUMN));
String number = cursor.getString(cursor.getColumnIndexOrThrow(ContactsDatabase.NUMBER_COLUMN));
int numberType = cursor.getInt(cursor.getColumnIndexOrThrow(ContactsDatabase.NUMBER_TYPE_COLUMN));
String label = cursor.getString(cursor.getColumnIndexOrThrow(ContactsDatabase.LABEL_COLUMN));
- String labelText = ContactsContract.CommonDataKinds.Phone.getTypeLabel(context.getResources(),
+ String labelText = ContactsContract.CommonDataKinds.Phone.getTypeLabel(getContext().getResources(),
numberType, label).toString();
int color = (contactType == ContactsDatabase.PUSH_TYPE) ? drawables.getColor(0, 0xa0000000) :
- drawables.getColor(1, 0xff000000);
+ drawables.getColor(1, 0xff000000);
-
- ((ContactSelectionListItem)view).unbind();
- ((ContactSelectionListItem)view).set(id, contactType, name, number, labelText, color, multiSelect);
- ((ContactSelectionListItem)view).setChecked(selectedContacts.containsKey(id));
+ viewHolder.getView().unbind();
+ viewHolder.getView().set(id, contactType, name, number, labelText, color, multiSelect);
+ viewHolder.getView().setChecked(selectedContacts.containsKey(id));
}
@Override
- public View getHeaderView(int i, View convertView, ViewGroup viewGroup) {
- Cursor cursor = getCursor();
-
- final TextView text;
- if (convertView == null) {
- text = (TextView)li.inflate(R.layout.contact_selection_list_header, viewGroup, false);
- } else {
- text = (TextView)convertView;
- }
-
- cursor.moveToPosition(i);
-
- int contactType = cursor.getInt(cursor.getColumnIndexOrThrow(ContactsDatabase.CONTACT_TYPE_COLUMN));
-
- if (contactType == ContactsDatabase.PUSH_TYPE) text.setText(R.string.contact_selection_list__header_signal_users);
- else text.setText(R.string.contact_selection_list__header_other);
-
- return text;
+ public HeaderViewHolder onCreateHeaderViewHolder(ViewGroup parent) {
+ return new HeaderViewHolder(LayoutInflater.from(getContext()).inflate(R.layout.contact_selection_recyclerview_header, parent, false));
}
@Override
- public long getHeaderId(int i) {
- Cursor cursor = getCursor();
- cursor.moveToPosition(i);
+ public void onBindHeaderViewHolder(HeaderViewHolder viewHolder, int position) {
+ ((TextView)viewHolder.itemView).setText(getSpannedHeaderString(position, R.drawable.ic_signal_grey_24dp));
+ }
- return cursor.getInt(cursor.getColumnIndexOrThrow(ContactsDatabase.CONTACT_TYPE_COLUMN));
+ @Override
+ public CharSequence getBubbleText(int position) {
+ return getSpannedHeaderString(position, R.drawable.ic_signal_white_48dp);
}
public Map getSelectedContacts() {
return selectedContacts;
}
+
+ private CharSequence getSpannedHeaderString(int position, @DrawableRes int drawable) {
+ Cursor cursor = getCursorAtPositionOrThrow(position);
+
+ if (cursor.getInt(cursor.getColumnIndexOrThrow(ContactsDatabase.CONTACT_TYPE_COLUMN)) == ContactsDatabase.PUSH_TYPE) {
+ SpannableString spannable = new SpannableString(" ");
+ spannable.setSpan(new ImageSpan(getContext(), drawable, ImageSpan.ALIGN_BOTTOM), 0, spannable.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ return spannable;
+ } else {
+ return getHeaderString(position);
+ }
+ }
+
+ private String getHeaderString(int position) {
+ Cursor cursor = getCursorAtPositionOrThrow(position);
+
+ if (cursor.getInt(cursor.getColumnIndexOrThrow(ContactsDatabase.CONTACT_TYPE_COLUMN)) == ContactsDatabase.PUSH_TYPE) {
+ return getContext().getString(R.string.app_name);
+ } else {
+ String letter = cursor.getString(cursor.getColumnIndexOrThrow(ContactsDatabase.NAME_COLUMN))
+ .trim()
+ .substring(0,1)
+ .toUpperCase();
+ if (Character.isLetterOrDigit(letter.codePointAt(0))) {
+ return letter;
+ } else {
+ return "#";
+ }
+ }
+ }
+
+ private Cursor getCursorAtPositionOrThrow(int position) {
+ Cursor cursor = getCursor();
+ if (cursor == null) {
+ throw new IllegalStateException("Cursor should not be null here.");
+ }
+ if (!cursor.moveToPosition(position));
+ return cursor;
+ }
+
+ public interface ItemClickListener {
+ void onItemClick(ContactSelectionListItem item);
+ }
}
diff --git a/src/org/thoughtcrime/securesms/contacts/ContactSelectionListItem.java b/src/org/thoughtcrime/securesms/contacts/ContactSelectionListItem.java
index c7b16a6582..70a1870893 100644
--- a/src/org/thoughtcrime/securesms/contacts/ContactSelectionListItem.java
+++ b/src/org/thoughtcrime/securesms/contacts/ContactSelectionListItem.java
@@ -5,7 +5,7 @@ import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.View;
import android.widget.CheckBox;
-import android.widget.RelativeLayout;
+import android.widget.LinearLayout;
import android.widget.TextView;
import org.thoughtcrime.securesms.R;
@@ -14,7 +14,7 @@ import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.Recipients;
-public class ContactSelectionListItem extends RelativeLayout implements Recipients.RecipientsModifiedListener {
+public class ContactSelectionListItem extends LinearLayout implements Recipients.RecipientsModifiedListener {
private AvatarImageView contactPhotoImage;
private TextView numberView;
@@ -34,10 +34,6 @@ public class ContactSelectionListItem extends RelativeLayout implements Recipien
super(context, attrs);
}
- public ContactSelectionListItem(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- }
-
@Override
protected void onFinishInflate() {
super.onFinishInflate();
diff --git a/src/org/thoughtcrime/securesms/util/ViewUtil.java b/src/org/thoughtcrime/securesms/util/ViewUtil.java
index 9cd4af625e..fdfbfa943d 100644
--- a/src/org/thoughtcrime/securesms/util/ViewUtil.java
+++ b/src/org/thoughtcrime/securesms/util/ViewUtil.java
@@ -24,6 +24,7 @@ import android.support.annotation.IdRes;
import android.support.annotation.LayoutRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
+import android.support.v4.view.ViewCompat;
import android.support.v4.view.animation.FastOutSlowInInterpolator;
import android.text.TextUtils;
import android.text.TextUtils.TruncateAt;
@@ -33,6 +34,7 @@ import android.view.ViewGroup;
import android.view.ViewStub;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
+import android.widget.LinearLayout.LayoutParams;
import android.widget.TextView;
public class ViewUtil {
@@ -45,6 +47,42 @@ public class ViewUtil {
}
}
+ public static void setY(final @NonNull View v, final int y) {
+ if (VERSION.SDK_INT >= 11) {
+ ViewCompat.setY(v, y);
+ } else {
+ ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams)v.getLayoutParams();
+ params.topMargin = y;
+ v.setLayoutParams(params);
+ }
+ }
+
+ public static float getY(final @NonNull View v) {
+ if (VERSION.SDK_INT >= 11) {
+ return ViewCompat.getY(v);
+ } else {
+ return ((ViewGroup.MarginLayoutParams)v.getLayoutParams()).topMargin;
+ }
+ }
+
+ public static void setX(final @NonNull View v, final int x) {
+ if (VERSION.SDK_INT >= 11) {
+ ViewCompat.setX(v, x);
+ } else {
+ ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams)v.getLayoutParams();
+ params.leftMargin = x;
+ v.setLayoutParams(params);
+ }
+ }
+
+ public static float getX(final @NonNull View v) {
+ if (VERSION.SDK_INT >= 11) {
+ return ViewCompat.getX(v);
+ } else {
+ return ((LayoutParams)v.getLayoutParams()).leftMargin;
+ }
+ }
+
public static void swapChildInPlace(ViewGroup parent, View toRemove, View toAdd, int defaultIndex) {
int childIndex = parent.indexOfChild(toRemove);
if (childIndex > -1) parent.removeView(toRemove);