From f87209d24266b7044b4f9bbf00d19c7d47d7f2af Mon Sep 17 00:00:00 2001 From: Vincent Breitmoser Date: Wed, 20 Jun 2018 00:45:34 +0200 Subject: [PATCH] use FlexibleAdapter with LiveData in KeyListFragment --- OpenKeychain/build.gradle | 7 +- .../keychain/matcher/CustomMatchers.java | 7 +- .../NoScrollableSwipeRefreshLayout.java | 473 ------------ .../keychain/livedata/KeyRingDao.java | 41 ++ .../keychain/model/AutocryptPeer.java | 8 +- .../keychain/model/Key.java | 50 ++ .../keychain/provider/KeychainDatabase.java | 30 +- .../keychain/ui/KeyListFragment.java | 344 +++++---- .../keychain/ui/LogDisplayFragment.java | 10 +- .../ui/adapter/FlexibleKeyHeader.java | 65 ++ .../keychain/ui/adapter/FlexibleKeyItem.java | 236 ++++++ .../ui/adapter/FlexibleKeyItemFactory.java | 60 ++ .../keychain/ui/adapter/KeyAdapter.java | 31 +- .../ui/adapter/KeySectionedListAdapter.java | 691 ------------------ .../ui/util/adapter/SectionCursorAdapter.java | 337 --------- .../res/drawable-v21/list_item_ripple.xml | 1 + .../src/main/res/layout/key_list_fragment.xml | 30 +- .../res/layout/key_list_header_public.xml | 8 +- .../src/main/res/layout/key_list_item.xml | 30 +- .../org/sufficientlysecure/keychain/Certs.sq | 4 +- .../keychain/KeyRingsPublic.sq | 2 +- .../org/sufficientlysecure/keychain/Keys.sq | 44 +- .../keychain/UserPackets.sq | 15 + 23 files changed, 746 insertions(+), 1778 deletions(-) delete mode 100644 OpenKeychain/src/main/java/android/support/v4/widget/NoScrollableSwipeRefreshLayout.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/livedata/KeyRingDao.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/model/Key.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/FlexibleKeyHeader.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/FlexibleKeyItem.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/FlexibleKeyItemFactory.java delete mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeySectionedListAdapter.java delete mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/adapter/SectionCursorAdapter.java create mode 100644 OpenKeychain/src/main/sqldelight/org/sufficientlysecure/keychain/UserPackets.sq diff --git a/OpenKeychain/build.gradle b/OpenKeychain/build.gradle index 0163bc22e..cf5fdd6ed 100644 --- a/OpenKeychain/build.gradle +++ b/OpenKeychain/build.gradle @@ -36,7 +36,9 @@ dependencies { // RecyclerView compile 'com.tonicartos:superslim:0.4.13' - compile 'com.futuremind.recyclerfastscroll:fastscroll:0.2.4' + compile 'eu.davidea:flexible-adapter:5.0.5' + compile 'eu.davidea:flexible-adapter-ui:1.0.0-b5' + compile 'eu.davidea:flexible-adapter-livedata:1.0.0-b2' // Material Drawer compile 'com.mikepenz:materialdrawer:5.6.0@aar' @@ -107,6 +109,7 @@ dependencies { // Comment out the libs referenced as git submodules! dependencyVerification { verify = [ + 'eu.davidea:flexible-adapter-ui:7ed5327d15c823e5fcf7d6e1017d8a47d079d1adc7141858f3cb427517ef35cd', 'com.android.support:design:7225973f7ee03765008a9c2f17a40b154c6885169fef022276e811c926a2202c', 'com.journeyapps:zxing-android-embedded:2422d83c2c09a7b645f516c8458ececba6a7da47b94e40778d876facf495c660', 'org.sufficientlysecure:donations:2be4183afa5e35263e37346344cfea48681f3c987e6832dd4acde227c13ccad6', @@ -115,6 +118,7 @@ dependencyVerification { 'com.mikepenz:materialize:942ccf5e2aa1a46803aa884e8dc7bbaf2a9e8e9996a0cf92e3fe2f44a8592ba4', 'com.android.support:appcompat-v7:0c7808fbbc5838d831e32e3c0a6f84e1f2c981deb8f11e010650f2b57923a335', 'com.nispok:snackbar:46b5eb9d630d329e13c2ce00ee9fb115ffb66c23c72cff32ee97eedd76824c6f', + 'eu.davidea:flexible-adapter:560e940e8cf0f4ed8f632f5f89527deeda7a61cce5f02f42cc0983f7c0d2de5f', 'com.android.support:recyclerview-v7:d735e4727878e99ef3980c10d15dc3468462fd509d4fb60cb8bd20b0f735085c', 'com.android.support:cardview-v7:8ed955dd037d82a7b4bbcaedb4f896523c3e4c1bf3ca698ce807c350767a2886', 'org.sufficientlysecure:html-textview:ed740adf05cae2373999c7a3047c803183d9807b2cf66162902090d7c112a832', @@ -145,6 +149,7 @@ dependencyVerification { 'org.apache.james:apache-mime4j-core:561987f604911e1870b2b4eabf0b0658d666c66cb1e65fba3e9e4bffe63acab9', 'com.splitwise:tokenautocomplete:f921f83ee26b5265f719b312c30452ef8e219557826c5ce5bf02e29647967939', 'com.cocosw:bottomsheet:85bd91fd837b02ebd7a888501cb26035c7cd985a6aa87303fca249da8231a2c3', + 'eu.davidea:flexible-adapter-livedata:c8718b46ff4fbf290ea18f0c5bfe8326badeadf5fd95899a1404c561a24f48a1', 'com.mikepenz:materialdrawer:8bba1428dcef5ad7c2decf49c612ad980b38e2f1031cbd66c152a8a104793929', 'com.mikepenz:iconics-core:478d7e245098f7c28b5b20a0e6b1e5cb108ef3eaf595af7190bc60f91063aa3d', 'com.mikepenz:google-material-typeface:f27c629ba5d2a90ecfbd7f221ff98cd363e1ee6be06b099b82bae490766e14a5', diff --git a/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/matcher/CustomMatchers.java b/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/matcher/CustomMatchers.java index c408c2266..aef83afb0 100644 --- a/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/matcher/CustomMatchers.java +++ b/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/matcher/CustomMatchers.java @@ -28,12 +28,10 @@ import android.widget.ViewAnimator; import com.nispok.snackbar.Snackbar; -import org.hamcrest.BaseMatcher; import org.hamcrest.Description; import org.hamcrest.Matcher; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.ui.adapter.KeyAdapter.KeyItem; -import org.sufficientlysecure.keychain.ui.adapter.KeySectionedListAdapter; import org.sufficientlysecure.keychain.ui.widget.EncryptKeyCompletionView; import static android.support.test.espresso.matcher.ViewMatchers.hasDescendant; @@ -90,15 +88,14 @@ public abstract class CustomMatchers { } public static Matcher withKeyHolderId(final long keyId) { - return new BoundedMatcher - (KeySectionedListAdapter.KeyItemViewHolder.class) { + return new BoundedMatcher(RecyclerView.ViewHolder.class) { @Override public void describeTo(Description description) { description.appendText("with ViewHolder id: " + keyId); } @Override - protected boolean matchesSafely(KeySectionedListAdapter.KeyItemViewHolder item) { + protected boolean matchesSafely(View item) { return item.getItemId() == keyId; } }; diff --git a/OpenKeychain/src/main/java/android/support/v4/widget/NoScrollableSwipeRefreshLayout.java b/OpenKeychain/src/main/java/android/support/v4/widget/NoScrollableSwipeRefreshLayout.java deleted file mode 100644 index 19570350e..000000000 --- a/OpenKeychain/src/main/java/android/support/v4/widget/NoScrollableSwipeRefreshLayout.java +++ /dev/null @@ -1,473 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * 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 android.support.v4.widget; - -import android.content.Context; -import android.content.res.Resources; -import android.content.res.TypedArray; -import android.graphics.Canvas; -import android.support.v4.view.ViewCompat; -import android.util.AttributeSet; -import android.util.DisplayMetrics; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewConfiguration; -import android.view.ViewGroup; -import android.view.animation.AccelerateInterpolator; -import android.view.animation.Animation; -import android.view.animation.Animation.AnimationListener; -import android.view.animation.DecelerateInterpolator; -import android.view.animation.Transformation; -import android.widget.AbsListView; - - -/** - * Same as SwipeRefreshLayout, only updateContentOffsetTop and REFRESH_TRIGGER_DISTANCE - * have been modified! - */ -public class NoScrollableSwipeRefreshLayout extends ViewGroup { - private static final long RETURN_TO_ORIGINAL_POSITION_TIMEOUT = 300; - private static final float ACCELERATE_INTERPOLATION_FACTOR = 1.5f; - private static final float DECELERATE_INTERPOLATION_FACTOR = 2f; - private static final float PROGRESS_BAR_HEIGHT = 4; - private static final float MAX_SWIPE_DISTANCE_FACTOR = .6f; - private static final int REFRESH_TRIGGER_DISTANCE = 200; - - private SwipeProgressBar mProgressBar; //the thing that shows progress is going - private View mTarget; //the content that gets pulled down - private int mOriginalOffsetTop; - private OnRefreshListener mListener; - private MotionEvent mDownEvent; - private int mFrom; - private boolean mRefreshing = false; - private int mTouchSlop; - private float mDistanceToTriggerSync = -1; - private float mPrevY; - private int mMediumAnimationDuration; - private float mFromPercentage = 0; - private float mCurrPercentage = 0; - private int mProgressBarHeight; - private int mCurrentTargetOffsetTop; - // Target is returning to its start offset because it was cancelled or a - // refresh was triggered. - private boolean mReturningToStart; - private final DecelerateInterpolator mDecelerateInterpolator; - private final AccelerateInterpolator mAccelerateInterpolator; - private static final int[] LAYOUT_ATTRS = new int[] { - android.R.attr.enabled - }; - - private final Animation mAnimateToStartPosition = new Animation() { - @Override - public void applyTransformation(float interpolatedTime, Transformation t) { - int targetTop = 0; - if (mFrom != mOriginalOffsetTop) { - targetTop = (mFrom + (int)((mOriginalOffsetTop - mFrom) * interpolatedTime)); - } - int offset = targetTop - mTarget.getTop(); - final int currentTop = mTarget.getTop(); - if (offset + currentTop < 0) { - offset = 0 - currentTop; - } - setTargetOffsetTopAndBottom(offset); - } - }; - - private Animation mShrinkTrigger = new Animation() { - @Override - public void applyTransformation(float interpolatedTime, Transformation t) { - float percent = mFromPercentage + ((0 - mFromPercentage) * interpolatedTime); - mProgressBar.setTriggerPercentage(percent); - } - }; - - private final AnimationListener mReturnToStartPositionListener = new BaseAnimationListener() { - @Override - public void onAnimationEnd(Animation animation) { - // Once the target content has returned to its start position, reset - // the target offset to 0 - mCurrentTargetOffsetTop = 0; - } - }; - - private final AnimationListener mShrinkAnimationListener = new BaseAnimationListener() { - @Override - public void onAnimationEnd(Animation animation) { - mCurrPercentage = 0; - } - }; - - private final Runnable mReturnToStartPosition = new Runnable() { - - @Override - public void run() { - mReturningToStart = true; - animateOffsetToStartPosition(mCurrentTargetOffsetTop + getPaddingTop(), - mReturnToStartPositionListener); - } - - }; - - // Cancel the refresh gesture and animate everything back to its original state. - private final Runnable mCancel = new Runnable() { - - @Override - public void run() { - mReturningToStart = true; - // Timeout fired since the user last moved their finger; animate the - // trigger to 0 and put the target back at its original position - if (mProgressBar != null) { - mFromPercentage = mCurrPercentage; - mShrinkTrigger.setDuration(mMediumAnimationDuration); - mShrinkTrigger.setAnimationListener(mShrinkAnimationListener); - mShrinkTrigger.reset(); - mShrinkTrigger.setInterpolator(mDecelerateInterpolator); - startAnimation(mShrinkTrigger); - } - animateOffsetToStartPosition(mCurrentTargetOffsetTop + getPaddingTop(), - mReturnToStartPositionListener); - } - - }; - - /** - * Simple constructor to use when creating a SwipeRefreshLayout from code. - * @param context - */ - public NoScrollableSwipeRefreshLayout(Context context) { - this(context, null); - } - - /** - * Constructor that is called when inflating SwipeRefreshLayout from XML. - * @param context - * @param attrs - */ - public NoScrollableSwipeRefreshLayout(Context context, AttributeSet attrs) { - super(context, attrs); - - mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); - - mMediumAnimationDuration = getResources().getInteger( - android.R.integer.config_mediumAnimTime); - - setWillNotDraw(false); - mProgressBar = new SwipeProgressBar(this); - final DisplayMetrics metrics = getResources().getDisplayMetrics(); - mProgressBarHeight = (int) (metrics.density * PROGRESS_BAR_HEIGHT); - mDecelerateInterpolator = new DecelerateInterpolator(DECELERATE_INTERPOLATION_FACTOR); - mAccelerateInterpolator = new AccelerateInterpolator(ACCELERATE_INTERPOLATION_FACTOR); - - final TypedArray a = context.obtainStyledAttributes(attrs, LAYOUT_ATTRS); - setEnabled(a.getBoolean(0, true)); - a.recycle(); - } - - @Override - public void onAttachedToWindow() { - super.onAttachedToWindow(); - removeCallbacks(mCancel); - removeCallbacks(mReturnToStartPosition); - } - - @Override - public void onDetachedFromWindow() { - super.onDetachedFromWindow(); - removeCallbacks(mReturnToStartPosition); - removeCallbacks(mCancel); - } - - private void animateOffsetToStartPosition(int from, AnimationListener listener) { - mFrom = from; - mAnimateToStartPosition.reset(); - mAnimateToStartPosition.setDuration(mMediumAnimationDuration); - mAnimateToStartPosition.setAnimationListener(listener); - mAnimateToStartPosition.setInterpolator(mDecelerateInterpolator); - mTarget.startAnimation(mAnimateToStartPosition); - } - - /** - * Set the listener to be notified when a refresh is triggered via the swipe - * gesture. - */ - public void setOnRefreshListener(OnRefreshListener listener) { - mListener = listener; - } - - private void setTriggerPercentage(float percent) { - if (percent == 0f) { - // No-op. A null trigger means it's uninitialized, and setting it to zero-percent - // means we're trying to reset state, so there's nothing to reset in this case. - mCurrPercentage = 0; - return; - } - mCurrPercentage = percent; - mProgressBar.setTriggerPercentage(percent); - } - - /** - * Notify the widget that refresh state has changed. Do not call this when - * refresh is triggered by a swipe gesture. - * - * @param refreshing Whether or not the view should show refresh progress. - */ - public void setRefreshing(boolean refreshing) { - if (mRefreshing != refreshing) { - ensureTarget(); - mCurrPercentage = 0; - mRefreshing = refreshing; - if (mRefreshing) { - mProgressBar.start(); - } else { - mProgressBar.stop(); - } - } - } - - /** - * Set the four colors used in the progress animation. The first color will - * also be the color of the bar that grows in response to a user swipe - * gesture. - * - * @param colorRes1 Color resource. - * @param colorRes2 Color resource. - * @param colorRes3 Color resource. - * @param colorRes4 Color resource. - */ - public void setColorScheme(int colorRes1, int colorRes2, int colorRes3, int colorRes4) { - ensureTarget(); - final Resources res = getResources(); - final int color1 = res.getColor(colorRes1); - final int color2 = res.getColor(colorRes2); - final int color3 = res.getColor(colorRes3); - final int color4 = res.getColor(colorRes4); - mProgressBar.setColorScheme(color1, color2, color3,color4); - } - - /** - * @return Whether the SwipeRefreshWidget is actively showing refresh - * progress. - */ - public boolean isRefreshing() { - return mRefreshing; - } - - private void ensureTarget() { - // Don't bother getting the parent height if the parent hasn't been laid out yet. - if (mTarget == null) { - if (getChildCount() > 1 && !isInEditMode()) { - throw new IllegalStateException( - "SwipeRefreshLayout can host only one direct child"); - } - mTarget = getChildAt(0); - mOriginalOffsetTop = mTarget.getTop() + getPaddingTop(); - } - if (mDistanceToTriggerSync == -1) { - if (getParent() != null && ((View)getParent()).getHeight() > 0) { - final DisplayMetrics metrics = getResources().getDisplayMetrics(); - mDistanceToTriggerSync = (int) Math.min( - ((View) getParent()) .getHeight() * MAX_SWIPE_DISTANCE_FACTOR, - REFRESH_TRIGGER_DISTANCE * metrics.density); - } - } - } - - @Override - public void draw(Canvas canvas) { - super.draw(canvas); - mProgressBar.draw(canvas); - } - - @Override - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - final int width = getMeasuredWidth(); - final int height = getMeasuredHeight(); - mProgressBar.setBounds(0, 0, width, mProgressBarHeight); - if (getChildCount() == 0) { - return; - } - final View child = getChildAt(0); - final int childLeft = getPaddingLeft(); - final int childTop = mCurrentTargetOffsetTop + getPaddingTop(); - final int childWidth = width - getPaddingLeft() - getPaddingRight(); - final int childHeight = height - getPaddingTop() - getPaddingBottom(); - child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight); - } - - @Override - public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - if (getChildCount() > 1 && !isInEditMode()) { - throw new IllegalStateException("SwipeRefreshLayout can host only one direct child"); - } - if (getChildCount() > 0) { - getChildAt(0).measure( - MeasureSpec.makeMeasureSpec( - getMeasuredWidth() - getPaddingLeft() - getPaddingRight(), - MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec( - getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), - MeasureSpec.EXACTLY)); - } - } - - /** - * @return Whether it is possible for the child view of this layout to - * scroll up. Override this if the child view is a custom view. - */ - public boolean canChildScrollUp() { - if (android.os.Build.VERSION.SDK_INT < 14) { - if (mTarget instanceof AbsListView) { - final AbsListView absListView = (AbsListView) mTarget; - return absListView.getChildCount() > 0 - && (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0) - .getTop() < absListView.getPaddingTop()); - } else { - return mTarget.getScrollY() > 0; - } - } else { - return ViewCompat.canScrollVertically(mTarget, -1); - } - } - - @Override - public boolean onInterceptTouchEvent(MotionEvent ev) { - ensureTarget(); - boolean handled = false; - if (mReturningToStart && ev.getAction() == MotionEvent.ACTION_DOWN) { - mReturningToStart = false; - } - if (isEnabled() && !mReturningToStart && !canChildScrollUp()) { - handled = onTouchEvent(ev); - } - return !handled ? super.onInterceptTouchEvent(ev) : handled; - } - - @Override - public void requestDisallowInterceptTouchEvent(boolean b) { - // Nope. - } - - @Override - public boolean onTouchEvent(MotionEvent event) { - final int action = event.getAction(); - boolean handled = false; - switch (action) { - case MotionEvent.ACTION_DOWN: - mCurrPercentage = 0; - mDownEvent = MotionEvent.obtain(event); - mPrevY = mDownEvent.getY(); - break; - case MotionEvent.ACTION_MOVE: - if (mDownEvent != null && !mReturningToStart) { - final float eventY = event.getY(); - float yDiff = eventY - mDownEvent.getY(); - if (yDiff > mTouchSlop) { - // User velocity passed min velocity; trigger a refresh - if (yDiff > mDistanceToTriggerSync) { - // User movement passed distance; trigger a refresh - startRefresh(); - handled = true; - break; - } else { - // Just track the user's movement - setTriggerPercentage( - mAccelerateInterpolator.getInterpolation( - yDiff / mDistanceToTriggerSync)); - float offsetTop = yDiff; - if (mPrevY > eventY) { - offsetTop = yDiff - mTouchSlop; - } - updateContentOffsetTop((int) (offsetTop)); - if (mPrevY > eventY && (mTarget.getTop() < mTouchSlop)) { - // If the user puts the view back at the top, we - // don't need to. This shouldn't be considered - // cancelling the gesture as the user can restart from the top. - removeCallbacks(mCancel); - } else { - updatePositionTimeout(); - } - mPrevY = event.getY(); - handled = true; - } - } - } - break; - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_CANCEL: - if (mDownEvent != null) { - mDownEvent.recycle(); - mDownEvent = null; - } - break; - } - return handled; - } - - private void startRefresh() { - removeCallbacks(mCancel); - mReturnToStartPosition.run(); - setRefreshing(true); - mListener.onRefresh(); - } - - private void updateContentOffsetTop(int targetTop) { - final int currentTop = mTarget.getTop(); - if (targetTop > mDistanceToTriggerSync) { - targetTop = (int) mDistanceToTriggerSync; - } else if (targetTop < 0) { - targetTop = 0; - } -// setTargetOffsetTopAndBottom(targetTop - currentTop); - } - - private void setTargetOffsetTopAndBottom(int offset) { - mTarget.offsetTopAndBottom(offset); - mCurrentTargetOffsetTop = mTarget.getTop(); - } - - private void updatePositionTimeout() { - removeCallbacks(mCancel); - postDelayed(mCancel, RETURN_TO_ORIGINAL_POSITION_TIMEOUT); - } - - /** - * Classes that wish to be notified when the swipe gesture correctly - * triggers a refresh should implement this interface. - */ - public interface OnRefreshListener { - void onRefresh(); - } - - /** - * Simple AnimationListener to avoid having to implement unneeded methods in - * AnimationListeners. - */ - private class BaseAnimationListener implements AnimationListener { - @Override - public void onAnimationStart(Animation animation) { - } - - @Override - public void onAnimationEnd(Animation animation) { - } - - @Override - public void onAnimationRepeat(Animation animation) { - } - } -} \ No newline at end of file diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/livedata/KeyRingDao.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/livedata/KeyRingDao.java new file mode 100644 index 000000000..68efe8010 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/livedata/KeyRingDao.java @@ -0,0 +1,41 @@ +package org.sufficientlysecure.keychain.livedata; + + +import java.util.ArrayList; +import java.util.List; + +import android.arch.persistence.db.SupportSQLiteDatabase; +import android.content.Context; +import android.database.Cursor; + +import com.squareup.sqldelight.SqlDelightQuery; +import org.sufficientlysecure.keychain.model.Key; +import org.sufficientlysecure.keychain.model.Key.UnifiedKeyInfo; +import org.sufficientlysecure.keychain.provider.KeychainDatabase; + + +public class KeyRingDao { + private final SupportSQLiteDatabase db; + + public static KeyRingDao getInstance(Context context) { + KeychainDatabase keychainDatabase = new KeychainDatabase(context); + + return new KeyRingDao(keychainDatabase.getWritableDatabase()); + } + + private KeyRingDao(SupportSQLiteDatabase writableDatabase) { + this.db = writableDatabase; + } + + public List getUnifiedKeyInfo() { + SqlDelightQuery query = Key.FACTORY.selectAllUnifiedKeyInfo(); + List result = new ArrayList<>(); + try (Cursor cursor = db.query(query)) { + while (cursor.moveToNext()) { + UnifiedKeyInfo unifiedKeyInfo = Key.UNIFIED_KEY_INFO_MAPPER.map(cursor); + result.add(unifiedKeyInfo); + } + } + return result; + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/model/AutocryptPeer.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/model/AutocryptPeer.java index b6dcc32f8..1a5c177de 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/model/AutocryptPeer.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/model/AutocryptPeer.java @@ -28,8 +28,8 @@ public abstract class AutocryptPeer implements AutocryptPeersModel { } public boolean isGossipKeyRevoked() { - Long revokedInt = gossip_key_is_revoked_int(); - return revokedInt != null && revokedInt != 0; + Boolean gossip_key_is_revoked = gossip_key_is_revoked_int(); + return gossip_key_is_revoked != null && gossip_key_is_revoked; } public boolean isGossipKeyExpired() { @@ -45,8 +45,8 @@ public abstract class AutocryptPeer implements AutocryptPeersModel { } public boolean isKeyRevoked() { - Long revokedInt = key_is_revoked_int(); - return revokedInt != null && revokedInt != 0; + Boolean revoked = key_is_revoked_int(); + return revoked != null && revoked; } public boolean isKeyExpired() { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/model/Key.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/model/Key.java new file mode 100644 index 000000000..523883bc1 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/model/Key.java @@ -0,0 +1,50 @@ +package org.sufficientlysecure.keychain.model; + + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import com.google.auto.value.AutoValue; +import org.sufficientlysecure.keychain.KeysModel; + + +@AutoValue +public abstract class Key implements KeysModel { + public static final Factory FACTORY = new Factory<>(AutoValue_Key::new); + public static final SelectAllUnifiedKeyInfoMapper UNIFIED_KEY_INFO_MAPPER = + FACTORY.selectAllUnifiedKeyInfoMapper(AutoValue_Key_UnifiedKeyInfo::new); + + @AutoValue + public static abstract class UnifiedKeyInfo implements SelectAllUnifiedKeyInfoModel { + private List autocryptPackageNames; + + public boolean is_expired() { + Long expiry = expiry(); + return expiry != null && expiry * 1000 < System.currentTimeMillis(); + } + + public boolean has_any_secret() { + return has_any_secret_int() != 0; + } + + public boolean is_verified() { + Integer verified = verified(); + return verified != null && verified == 1; + } + + public boolean has_duplicate() { + return has_duplicate_int() != 0; + } + + public List autocrypt_package_names() { + if (autocryptPackageNames == null) { + String csv = autocrypt_package_names_csv(); + autocryptPackageNames = csv == null ? Collections.emptyList() : + Arrays.asList(csv.split(",")); + } + return autocryptPackageNames; + } + + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainDatabase.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainDatabase.java index cd925c115..30192e3cc 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainDatabase.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainDatabase.java @@ -34,11 +34,15 @@ import android.database.SQLException; import android.database.sqlite.SQLiteException; import android.provider.BaseColumns; +import org.sufficientlysecure.keychain.ApiAppsModel; import org.sufficientlysecure.keychain.AutocryptPeersModel; +import org.sufficientlysecure.keychain.CertsModel; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.KeyMetadataModel; import org.sufficientlysecure.keychain.KeyRingsPublicModel; +import org.sufficientlysecure.keychain.UserPacketsModel; import org.sufficientlysecure.keychain.model.ApiApp; +import org.sufficientlysecure.keychain.model.Certification; import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAppsAllowedKeysColumns; import org.sufficientlysecure.keychain.provider.KeychainContract.CertsColumns; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRingsColumns; @@ -120,26 +124,6 @@ public class KeychainDatabase { + Tables.KEY_RINGS_PUBLIC + "(" + KeyRingsColumns.MASTER_KEY_ID + ") ON DELETE CASCADE" + ")"; - private static final String CREATE_CERTS = - "CREATE TABLE IF NOT EXISTS " + Tables.CERTS + "(" - + CertsColumns.MASTER_KEY_ID + " INTEGER," - + CertsColumns.RANK + " INTEGER, " // rank of certified uid - - + CertsColumns.KEY_ID_CERTIFIER + " INTEGER, " // certifying key - + CertsColumns.TYPE + " INTEGER, " - + CertsColumns.VERIFIED + " INTEGER, " - + CertsColumns.CREATION + " INTEGER, " - - + CertsColumns.DATA + " BLOB, " - - + "PRIMARY KEY(" + CertsColumns.MASTER_KEY_ID + ", " + CertsColumns.RANK + ", " - + CertsColumns.KEY_ID_CERTIFIER + "), " - + "FOREIGN KEY(" + CertsColumns.MASTER_KEY_ID + ") REFERENCES " - + Tables.KEY_RINGS_PUBLIC + "(" + KeyRingsColumns.MASTER_KEY_ID + ") ON DELETE CASCADE," - + "FOREIGN KEY(" + CertsColumns.MASTER_KEY_ID + ", " + CertsColumns.RANK + ") REFERENCES " - + Tables.USER_PACKETS + "(" + UserPacketsColumns.MASTER_KEY_ID + ", " + UserPacketsColumns.RANK + ") ON DELETE CASCADE" - + ")"; - private static final String CREATE_KEY_SIGNATURES = "CREATE TABLE IF NOT EXISTS " + Tables.KEY_SIGNATURES + " (" + KeySignaturesColumns.MASTER_KEY_ID + " INTEGER NOT NULL, " @@ -212,14 +196,14 @@ public class KeychainDatabase { db.execSQL(KeyRingsPublicModel.CREATE_TABLE); db.execSQL(CREATE_KEYS); - db.execSQL(CREATE_USER_PACKETS); - db.execSQL(CREATE_CERTS); + db.execSQL(UserPacketsModel.CREATE_TABLE); + db.execSQL(CertsModel.CREATE_TABLE); db.execSQL(KeyMetadataModel.CREATE_TABLE); db.execSQL(CREATE_KEY_SIGNATURES); db.execSQL(CREATE_API_APPS_ALLOWED_KEYS); db.execSQL(CREATE_OVERRIDDEN_WARNINGS); db.execSQL(AutocryptPeersModel.CREATE_TABLE); - db.execSQL(ApiApp.CREATE_TABLE); + db.execSQL(ApiAppsModel.CREATE_TABLE); db.execSQL("CREATE INDEX keys_by_rank ON keys (" + KeysColumns.RANK + ", " + KeysColumns.MASTER_KEY_ID + ");"); db.execSQL("CREATE INDEX uids_by_rank ON user_packets (" + UserPacketsColumns.RANK + ", " diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java index 68e4e4844..77f51c37f 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java @@ -23,18 +23,19 @@ import java.util.List; import android.animation.ObjectAnimator; import android.app.Activity; +import android.arch.lifecycle.LiveData; +import android.arch.lifecycle.ViewModel; +import android.arch.lifecycle.ViewModelProviders; +import android.content.Context; import android.content.Intent; -import android.database.Cursor; -import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; +import android.support.annotation.NonNull; import android.support.v4.app.FragmentActivity; -import android.support.v4.app.LoaderManager; -import android.support.v4.content.CursorLoader; -import android.support.v4.content.Loader; import android.support.v4.view.MenuItemCompat; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; import android.support.v7.widget.SearchView; -import android.text.TextUtils; import android.view.ActionMode; import android.view.LayoutInflater; import android.view.Menu; @@ -46,55 +47,59 @@ import android.widget.Button; import android.widget.ViewAnimator; import androidx.work.WorkStatus; -import com.futuremind.recyclerviewfastscroll.FastScroller; import com.getbase.floatingactionbutton.FloatingActionButton; import com.getbase.floatingactionbutton.FloatingActionsMenu; -import com.tonicartos.superslim.LayoutManager; +import eu.davidea.fastscroller.FastScroller; +import eu.davidea.flexibleadapter.FlexibleAdapter; +import eu.davidea.flexibleadapter.FlexibleAdapter.OnItemClickListener; +import eu.davidea.flexibleadapter.FlexibleAdapter.OnItemLongClickListener; +import eu.davidea.flexibleadapter.SelectableAdapter.Mode; +import eu.davidea.flexibleadapter.common.FlexibleItemDecoration; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.compatibility.ClipboardReflection; import org.sufficientlysecure.keychain.keysync.KeyserverSyncManager; +import org.sufficientlysecure.keychain.livedata.KeyRingDao; +import org.sufficientlysecure.keychain.model.Key.UnifiedKeyInfo; import org.sufficientlysecure.keychain.operations.results.BenchmarkResult; import org.sufficientlysecure.keychain.operations.results.OperationResult; import org.sufficientlysecure.keychain.pgp.PgpHelper; -import org.sufficientlysecure.keychain.provider.KeychainContract; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; import org.sufficientlysecure.keychain.provider.KeychainDatabase; import org.sufficientlysecure.keychain.service.BenchmarkInputParcel; -import org.sufficientlysecure.keychain.ui.adapter.KeySectionedListAdapter; +import org.sufficientlysecure.keychain.ui.adapter.FlexibleKeyHeader; +import org.sufficientlysecure.keychain.ui.adapter.FlexibleKeyItem; +import org.sufficientlysecure.keychain.ui.adapter.FlexibleKeyItemFactory; import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper; import org.sufficientlysecure.keychain.ui.base.RecyclerFragment; import org.sufficientlysecure.keychain.ui.keyview.ViewKeyActivity; +import org.sufficientlysecure.keychain.ui.keyview.loader.AsyncTaskLiveData; import org.sufficientlysecure.keychain.ui.util.Notify; import org.sufficientlysecure.keychain.ui.util.Notify.Style; +import org.sufficientlysecure.keychain.ui.util.recyclerview.DividerItemDecoration; import org.sufficientlysecure.keychain.util.FabContainer; import org.sufficientlysecure.keychain.util.Preferences; import timber.log.Timber; -public class KeyListFragment extends RecyclerFragment - implements SearchView.OnQueryTextListener, - LoaderManager.LoaderCallbacks, FabContainer { +public class KeyListFragment extends RecyclerFragment> + implements SearchView.OnQueryTextListener, OnItemClickListener, OnItemLongClickListener, FabContainer { static final int REQUEST_ACTION = 1; private static final int REQUEST_DELETE = 2; private static final int REQUEST_VIEW_KEY = 3; - // saves the mode object for multiselect, needed for reset at some point private ActionMode mActionMode = null; private Button vSearchButton; private ViewAnimator vSearchContainer; - private String mQuery; private FloatingActionsMenu mFab; - // Callbacks related to listview and menu events - private final ActionMode.Callback mActionCallback - = new ActionMode.Callback() { + private final ActionMode.Callback mActionCallback = new ActionMode.Callback() { @Override public boolean onCreateActionMode(ActionMode mode, Menu menu) { - getActivity().getMenuInflater().inflate(R.menu.key_list_multi, menu); + mode.getMenuInflater().inflate(R.menu.key_list_multi, menu); return true; } @@ -107,28 +112,17 @@ public class KeyListFragment extends RecyclerFragment public boolean onActionItemClicked(ActionMode mode, MenuItem item) { switch (item.getItemId()) { case R.id.menu_key_list_multi_encrypt: { - long[] keyIds = getAdapter().getSelectedMasterKeyIds(); - Intent intent = new Intent(getActivity(), EncryptFilesActivity.class); - intent.setAction(EncryptFilesActivity.ACTION_ENCRYPT_DATA); - intent.putExtra(EncryptFilesActivity.EXTRA_ENCRYPTION_KEY_IDS, keyIds); - - startActivityForResult(intent, REQUEST_ACTION); + long[] keyIds = getSelectedMasterKeyIds(); + multiSelectEncrypt(keyIds); mode.finish(); break; } case R.id.menu_key_list_multi_delete: { - long[] keyIds = getAdapter().getSelectedMasterKeyIds(); - boolean hasSecret = getAdapter().isAnySecretKeySelected(); - Intent intent = new Intent(getActivity(), DeleteKeyDialogActivity.class); - intent.putExtra(DeleteKeyDialogActivity.EXTRA_DELETE_MASTER_KEY_IDS, keyIds); - intent.putExtra(DeleteKeyDialogActivity.EXTRA_HAS_SECRET, hasSecret); - if (hasSecret) { - intent.putExtra(DeleteKeyDialogActivity.EXTRA_KEYSERVER, - Preferences.getPreferences(getActivity()).getPreferredKeyserver()); - } - - startActivityForResult(intent, REQUEST_DELETE); + long[] keyIds = getSelectedMasterKeyIds(); + boolean hasSecret = isAnySecretKeySelected(); + multiSelectDelete(keyIds, hasSecret); + mode.finish(); break; } } @@ -139,54 +133,61 @@ public class KeyListFragment extends RecyclerFragment public void onDestroyActionMode(ActionMode mode) { mActionMode = null; if (getAdapter() != null) { - getAdapter().finishSelection(); + getAdapter().clearSelection(); } } }; + private FastScroller fastScroller; - private final KeySectionedListAdapter.KeyListListener mKeyListener - = new KeySectionedListAdapter.KeyListListener() { - @Override - public void onKeyDummyItemClicked() { - createKey(); + private void multiSelectDelete(long[] keyIds, boolean hasSecret) { + Intent intent = new Intent(getActivity(), DeleteKeyDialogActivity.class); + intent.putExtra(DeleteKeyDialogActivity.EXTRA_DELETE_MASTER_KEY_IDS, keyIds); + intent.putExtra(DeleteKeyDialogActivity.EXTRA_HAS_SECRET, hasSecret); + if (hasSecret) { + intent.putExtra(DeleteKeyDialogActivity.EXTRA_KEYSERVER, + Preferences.getPreferences(getActivity()).getPreferredKeyserver()); } + startActivityForResult(intent, REQUEST_DELETE); + } - @Override - public void onKeyItemClicked(long masterKeyId) { - Intent viewIntent = new Intent(getActivity(), ViewKeyActivity.class); - viewIntent.setData(KeyRings.buildGenericKeyRingUri(masterKeyId)); - startActivityForResult(viewIntent, REQUEST_VIEW_KEY); - } + private void multiSelectEncrypt(long[] keyIds) { + Intent intent = new Intent(getActivity(), EncryptFilesActivity.class); + intent.setAction(EncryptFilesActivity.ACTION_ENCRYPT_DATA); + intent.putExtra(EncryptFilesActivity.EXTRA_ENCRYPTION_KEY_IDS, keyIds); - @Override - public void onSlingerButtonClicked(long masterKeyId) { - Intent safeSlingerIntent = new Intent(getActivity(), SafeSlingerActivity.class); - safeSlingerIntent.putExtra(SafeSlingerActivity.EXTRA_MASTER_KEY_ID, masterKeyId); - startActivityForResult(safeSlingerIntent, REQUEST_ACTION); - } + startActivityForResult(intent, REQUEST_ACTION); + } - @Override - public void onSelectionStateChanged(int selectedCount) { - if (selectedCount < 1) { - if (mActionMode != null) { - mActionMode.finish(); - } - } else { - if (mActionMode == null) { - mActionMode = getActivity().startActionMode(mActionCallback); - } - - String keysSelected = getResources().getQuantityString( - R.plurals.key_list_selected_keys, selectedCount, selectedCount); - mActionMode.setTitle(keysSelected); + private long[] getSelectedMasterKeyIds() { + FlexibleAdapter adapter = getAdapter(); + List selectedPositions = adapter.getSelectedPositions(); + long[] keyIds = new long[selectedPositions.size()]; + for (int i = 0; i < selectedPositions.size(); i++) { + FlexibleKeyItem selectedItem = adapter.getItem(selectedPositions.get(i)); + if (selectedItem != null) { + keyIds[i] = selectedItem.keyInfo.master_key_id(); } } - }; + return keyIds; + } + private boolean isAnySecretKeySelected() { + FlexibleAdapter adapter = getAdapter(); + for (int position : adapter.getSelectedPositions()) { + FlexibleKeyItem item = adapter.getItem(position); + if (item != null && item.keyInfo.has_any_secret()) { + return true; + } + } + return false; + } + + public void startSafeSlingerForKey(long masterKeyId) { + Intent safeSlingerIntent = new Intent(getActivity(), SafeSlingerActivity.class); + safeSlingerIntent.putExtra(SafeSlingerActivity.EXTRA_MASTER_KEY_ID, masterKeyId); + startActivityForResult(safeSlingerIntent, REQUEST_ACTION); + } - /** - * Load custom layout - */ @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.key_list_fragment, container, false); @@ -210,6 +211,11 @@ public class KeyListFragment extends RecyclerFragment importFile(); }); + fastScroller = view.findViewById(R.id.fast_scroller); + + vSearchContainer = view.findViewById(R.id.search_container); + vSearchButton = view.findViewById(R.id.search_button); + vSearchButton.setOnClickListener(v -> startSearchForQuery()); return view; } @@ -222,7 +228,10 @@ public class KeyListFragment extends RecyclerFragment super.onActivityCreated(savedInstanceState); // show app name instead of "keys" from nav drawer - final FragmentActivity activity = getActivity(); + FragmentActivity activity = getActivity(); + if (activity == null) { + throw new NullPointerException("Activity must be bound!"); + } activity.setTitle(R.string.app_name); // We have a menu item to show in action bar. @@ -231,23 +240,64 @@ public class KeyListFragment extends RecyclerFragment // Start out with a progress indicator. hideList(false); - // click on search button (in empty view) starts query for search string - vSearchContainer = activity.findViewById(R.id.search_container); - vSearchButton = activity.findViewById(R.id.search_button); - vSearchButton.setOnClickListener(v -> startSearchForQuery()); + setLayoutManager(new LinearLayoutManager(activity)); - KeySectionedListAdapter adapter = new KeySectionedListAdapter(getContext(), null); - adapter.setKeyListener(mKeyListener); + KeyListViewModel keyListViewModel = ViewModelProviders.of(this).get(KeyListViewModel.class); + keyListViewModel.getLiveData(getContext()).observe(this, this::onLoadKeyItems); + } - setAdapter(adapter); - setLayoutManager(new LayoutManager(getActivity())); + public static class KeyListViewModel extends ViewModel { + LiveData> liveData; - FastScroller fastScroller = getActivity().findViewById(R.id.fastscroll); - fastScroller.setRecyclerView(getRecyclerView()); + LiveData> getLiveData(Context context) { + if (liveData == null) { + liveData = new KeyListLiveData(context); + } + return liveData; + } + } - // Prepare the loader. Either re-connect with an existing one, - // or start a new one. - getLoaderManager().initLoader(0, null, this); + public static class KeyListLiveData extends AsyncTaskLiveData> { + private final KeyRingDao keyRingDao; + private FlexibleKeyItemFactory flexibleKeyItemFactory; + + KeyListLiveData(@NonNull Context context) { + super(context, KeyRings.CONTENT_URI); + keyRingDao = KeyRingDao.getInstance(context.getApplicationContext()); + flexibleKeyItemFactory = new FlexibleKeyItemFactory(context.getResources()); + } + + @Override + protected List asyncLoadData() { + List unifiedKeyInfo = keyRingDao.getUnifiedKeyInfo(); + return flexibleKeyItemFactory.mapUnifiedKeyInfoToFlexibleKeyItems(unifiedKeyInfo); + } + } + + private void onLoadKeyItems(List flexibleKeyItems) { + FlexibleAdapter adapter = getAdapter(); + if (adapter == null) { + adapter = new FlexibleAdapter<>(flexibleKeyItems, this, true); + adapter.setDisplayHeadersAtStartUp(true); + adapter.setStickyHeaders(true); + adapter.setMode(Mode.MULTI); + setAdapter(adapter); + adapter.setFastScroller(fastScroller); + fastScroller.setBubbleTextCreator(this::getBubbleText); + } else { + adapter.updateDataSet(flexibleKeyItems, true); + } + + showList(true); + } + + private String getBubbleText(int position) { + FlexibleKeyItem item = getAdapter().getItem(position); + if (item == null) { + return ""; + } + FlexibleKeyHeader header = item.getHeader(); + return header.getSectionTitle(); } @Override @@ -305,53 +355,11 @@ public class KeyListFragment extends RecyclerFragment } Intent searchIntent = new Intent(activity, ImportKeysActivity.class); - searchIntent.putExtra(ImportKeysActivity.EXTRA_QUERY, mQuery); + searchIntent.putExtra(ImportKeysActivity.EXTRA_QUERY, getAdapter().getFilter(String.class)); searchIntent.setAction(ImportKeysActivity.ACTION_IMPORT_KEY_FROM_KEYSERVER); startActivity(searchIntent); } - @Override - public Loader onCreateLoader(int id, Bundle args) { - // This is called when a new Loader needs to be created. This - // sample only has one Loader, so we don't care about the ID. - Uri uri; - if (!TextUtils.isEmpty(mQuery)) { - uri = KeyRings.buildUnifiedKeyRingsFindByUserIdUri(mQuery); - } else { - uri = KeyRings.buildUnifiedKeyRingsUri(); - } - - // Now create and return a CursorLoader that will take care of - // creating a Cursor for the data being displayed. - return new CursorLoader(getActivity(), uri, - KeySectionedListAdapter.KeyListCursor.PROJECTION, null, null, - KeySectionedListAdapter.KeyListCursor.ORDER); - } - - @Override - public void onLoadFinished(Loader loader, Cursor data) { - // Swap the new cursor in. (The framework will take care of closing the - // old cursor once we return.) - getAdapter().setSearchQuery(mQuery); - getAdapter().swapCursor(KeySectionedListAdapter.KeyListCursor.wrap(data)); - - // end action mode, if any - if (mActionMode != null) { - mActionMode.finish(); - } - - // The list should now be shown. - showList(isResumed()); - } - - @Override - public void onLoaderReset(Loader loader) { - // This is called when the last Cursor provided to onLoadFinished() - // above is about to be closed. We need to make sure we are no - // longer using it. - getAdapter().swapCursor(null); - } - @Override public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) { inflater.inflate(R.menu.key_list, menu); @@ -375,19 +383,13 @@ public class KeyListFragment extends RecyclerFragment MenuItemCompat.setOnActionExpandListener(searchItem, new MenuItemCompat.OnActionExpandListener() { @Override public boolean onMenuItemActionExpand(MenuItem item) { - - // disable swipe-to-refresh - // mSwipeRefreshLayout.setIsLocked(true); return true; } @Override public boolean onMenuItemActionCollapse(MenuItem item) { - mQuery = null; - getLoaderManager().restartLoader(0, null, KeyListFragment.this); - - // enable swipe-to-refresh - // mSwipeRefreshLayout.setIsLocked(false); + getAdapter().setFilter(null); + getAdapter().filterItems(); return true; } }); @@ -395,6 +397,54 @@ public class KeyListFragment extends RecyclerFragment super.onCreateOptionsMenu(menu, inflater); } + @Override + public boolean onItemClick(View view, int position) { + FlexibleKeyItem item = getAdapter().getItem(position); + if (item == null) { + return false; + } + + if (mActionMode != null && position != RecyclerView.NO_POSITION) { + toggleSelection(position); + return true; + } + + long masterKeyId = item.keyInfo.master_key_id(); + Intent viewIntent = new Intent(getActivity(), ViewKeyActivity.class); + viewIntent.setData(KeyRings.buildGenericKeyRingUri(masterKeyId)); + startActivityForResult(viewIntent, REQUEST_VIEW_KEY); + return false; + } + + @Override + public void onItemLongClick(int position) { + if (mActionMode == null) { + FragmentActivity activity = getActivity(); + if (activity != null) { + mActionMode = activity.startActionMode(mActionCallback); + } + } + toggleSelection(position); + } + + private void toggleSelection(int position) { + getAdapter().toggleSelection(position); + + int count = getAdapter().getSelectedItemCount(); + + if (count == 0) { + mActionMode.finish(); + } else { + setContextTitle(count); + } + } + + private void setContextTitle(int selectedCount) { + String keysSelected = getResources().getQuantityString( + R.plurals.key_list_selected_keys, selectedCount, selectedCount); + mActionMode.setTitle(keysSelected); + } + @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { @@ -410,7 +460,7 @@ public class KeyListFragment extends RecyclerFragment try { KeychainDatabase.debugBackup(getActivity(), true); Notify.create(getActivity(), "Restored debug_backup.db", Notify.Style.OK).show(); - getActivity().getContentResolver().notifyChange(KeychainContract.KeyRings.CONTENT_URI, null); + getActivity().getContentResolver().notifyChange(KeyRings.CONTENT_URI, null); } catch (IOException e) { Timber.e(e, "IO Error"); Notify.create(getActivity(), "IO Error " + e.getMessage(), Notify.Style.ERROR).show(); @@ -456,20 +506,12 @@ public class KeyListFragment extends RecyclerFragment } @Override - public boolean onQueryTextChange(String s) { - Timber.d("onQueryTextChange s: %s", s); - // Called when the action bar search text has changed. Update the - // search filter, and restart the loader to do a new query with this - // filter. - // If the nav drawer is opened, onQueryTextChange("") is executed. - // This hack prevents restarting the loader. - if (!s.equals(mQuery)) { - mQuery = s; - getLoaderManager().restartLoader(0, null, this); - } + public boolean onQueryTextChange(String searchText) { + getAdapter().setFilter(searchText); + getAdapter().filterItems(300); - if (s.length() > 2) { - vSearchButton.setText(getString(R.string.btn_search_for_query, mQuery)); + if (searchText.length() > 2) { + vSearchButton.setText(getString(R.string.btn_search_for_query, searchText)); vSearchContainer.setDisplayedChild(1); vSearchContainer.setVisibility(View.VISIBLE); } else { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/LogDisplayFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/LogDisplayFragment.java index 9173861e4..4092453c7 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/LogDisplayFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/LogDisplayFragment.java @@ -17,6 +17,10 @@ package org.sufficientlysecure.keychain.ui; + +import java.io.IOException; +import java.io.OutputStream; + import android.app.Activity; import android.content.Intent; import android.net.Uri; @@ -26,19 +30,15 @@ import android.view.MenuInflater; import android.view.MenuItem; import com.tonicartos.superslim.LayoutManager; - import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.operations.results.OperationResult; import org.sufficientlysecure.keychain.operations.results.OperationResult.SubLogEntryParcel; import org.sufficientlysecure.keychain.provider.TemporaryFileProvider; import org.sufficientlysecure.keychain.ui.adapter.NestedLogAdapter; +import org.sufficientlysecure.keychain.ui.base.RecyclerFragment; import org.sufficientlysecure.keychain.ui.dialog.ShareLogDialogFragment; import org.sufficientlysecure.keychain.ui.util.Notify; import org.sufficientlysecure.keychain.ui.util.Notify.Style; -import org.sufficientlysecure.keychain.ui.base.RecyclerFragment; - -import java.io.IOException; -import java.io.OutputStream; public class LogDisplayFragment extends RecyclerFragment diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/FlexibleKeyHeader.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/FlexibleKeyHeader.java new file mode 100644 index 000000000..fda48678f --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/FlexibleKeyHeader.java @@ -0,0 +1,65 @@ +package org.sufficientlysecure.keychain.ui.adapter; + + +import java.util.List; + +import android.view.View; +import android.widget.TextView; + +import eu.davidea.flexibleadapter.FlexibleAdapter; +import eu.davidea.flexibleadapter.items.AbstractHeaderItem; +import eu.davidea.flexibleadapter.items.IFlexible; +import eu.davidea.viewholders.FlexibleViewHolder; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.ui.adapter.FlexibleKeyHeader.FlexibleHeaderViewHolder; + + +public class FlexibleKeyHeader extends AbstractHeaderItem { + private final String sectionTitle; + + FlexibleKeyHeader(String sectionTitle) { + this.sectionTitle = sectionTitle; + setEnabled(false); + setSelectable(false); + } + + @Override + public boolean equals(Object o) { + if (o instanceof FlexibleKeyHeader) { + FlexibleKeyHeader other = (FlexibleKeyHeader) o; + return sectionTitle.equals(other.sectionTitle); + } + return false; + } + + @Override + public int getLayoutRes() { + return R.layout.key_list_header_public; + } + + public String getSectionTitle() { + return sectionTitle; + } + + @Override + public FlexibleHeaderViewHolder createViewHolder(View view, FlexibleAdapter adapter) { + return new FlexibleHeaderViewHolder(view, adapter); + } + + @Override + public void bindViewHolder(FlexibleAdapter adapter, FlexibleHeaderViewHolder holder, int position, + List payloads) { + holder.text1.setText(sectionTitle); + } + + static class FlexibleHeaderViewHolder extends FlexibleViewHolder { + final TextView text1; + + FlexibleHeaderViewHolder(View view, FlexibleAdapter adapter) { + super(view, adapter, true); + text1 = itemView.findViewById(android.R.id.text1); + } + + + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/FlexibleKeyItem.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/FlexibleKeyItem.java new file mode 100644 index 000000000..cd9f8726e --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/FlexibleKeyItem.java @@ -0,0 +1,236 @@ +package org.sufficientlysecure.keychain.ui.adapter; + + +import java.util.List; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.support.annotation.NonNull; +import android.support.v4.content.ContextCompat; +import android.text.format.DateUtils; +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; + +import eu.davidea.flexibleadapter.FlexibleAdapter; +import eu.davidea.flexibleadapter.items.AbstractSectionableItem; +import eu.davidea.flexibleadapter.items.IFilterable; +import eu.davidea.flexibleadapter.items.IFlexible; +import eu.davidea.viewholders.FlexibleViewHolder; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.model.Key.UnifiedKeyInfo; +import org.sufficientlysecure.keychain.ui.adapter.FlexibleKeyItem.FlexibleKeyItemViewHolder; +import org.sufficientlysecure.keychain.ui.util.FormattingUtils; +import org.sufficientlysecure.keychain.ui.util.Highlighter; +import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; +import org.sufficientlysecure.keychain.ui.util.PackageIconGetter; + + +public class FlexibleKeyItem extends AbstractSectionableItem + implements IFilterable { + public final UnifiedKeyInfo keyInfo; + + FlexibleKeyItem(UnifiedKeyInfo keyInfo, FlexibleKeyHeader header) { + super(header); + this.keyInfo = keyInfo; + + setSelectable(true); + } + + @Override + public int getLayoutRes() { + return R.layout.key_list_item; + } + + @Override + public FlexibleKeyItemViewHolder createViewHolder(View view, FlexibleAdapter adapter) { + return new FlexibleKeyItemViewHolder(view, adapter); + } + + @Override + public void bindViewHolder( + FlexibleAdapter adapter, FlexibleKeyItemViewHolder holder, int position, List payloads) { + String highlightString = adapter.getFilter(String.class); + holder.bind(keyInfo, highlightString); + } + + @Override + public boolean equals(Object o) { + if (o instanceof FlexibleKeyItem) { + FlexibleKeyItem other = (FlexibleKeyItem) o; + return keyInfo.master_key_id() == other.keyInfo.master_key_id(); + } + return false; + } + + @Override + public int hashCode() { + long masterKeyId = keyInfo.master_key_id(); + return (int) (masterKeyId ^ (masterKeyId >>> 32)); + } + + @Override + public boolean filter(String constraint) { + String uidList = keyInfo.user_id_list(); + return constraint == null || (uidList != null && uidList.contains(constraint)); + } + + public class FlexibleKeyItemViewHolder extends FlexibleViewHolder { + private static final long JUST_NOW_THRESHOLD = DateUtils.MINUTE_IN_MILLIS * 5; + + private final TextView vMainUserId; + private final TextView vMainUserIdRest; + private final TextView vCreationDate; + private final ImageView vStatusIcon; + private final ImageView vTrustIdIcon; + + FlexibleKeyItemViewHolder(View itemView, FlexibleAdapter adapter) { + super(itemView, adapter); + + vMainUserId = itemView.findViewById(R.id.key_list_item_name); + vMainUserIdRest = itemView.findViewById(R.id.key_list_item_email); + vStatusIcon = itemView.findViewById(R.id.key_list_item_status_icon); + vCreationDate = itemView.findViewById(R.id.key_list_item_creation); + vTrustIdIcon = itemView.findViewById(R.id.key_list_item_tid_icon); + } + + public void bind(UnifiedKeyInfo keyInfo, String highlightString) { + setEnabled(true); + + Context context = itemView.getContext(); + Highlighter highlighter = new Highlighter(context, highlightString); + + { // set name and stuff, common to both key types + if (keyInfo.name() == null) { + if (keyInfo.email() != null) { + vMainUserId.setText(highlighter.highlight(keyInfo.email())); + vMainUserIdRest.setVisibility(View.GONE); + } else { + vMainUserId.setText(R.string.user_id_no_name); + } + } else { + vMainUserId.setText(highlighter.highlight(keyInfo.name())); + // for some reason, this hangs for me + // FlexibleUtils.highlightText(vMainUserId, keyInfo.name(), highlightString); + if (keyInfo.email() != null) { + vMainUserIdRest.setText(highlighter.highlight(keyInfo.email())); + vMainUserIdRest.setVisibility(View.VISIBLE); + } else { + vMainUserIdRest.setVisibility(View.GONE); + } + } + } + + { // set edit button and status, specific by key type. Note: order is important! + int textColor; + if (keyInfo.is_revoked()) { + KeyFormattingUtils.setStatusImage( + context, + vStatusIcon, + null, + KeyFormattingUtils.State.REVOKED, + R.color.key_flag_gray + ); + + vStatusIcon.setVisibility(View.VISIBLE); + textColor = ContextCompat.getColor(context, R.color.key_flag_gray); + } else if (keyInfo.is_expired()) { + KeyFormattingUtils.setStatusImage( + context, + vStatusIcon, + null, + KeyFormattingUtils.State.EXPIRED, + R.color.key_flag_gray + ); + + vStatusIcon.setVisibility(View.VISIBLE); + textColor = ContextCompat.getColor(context, R.color.key_flag_gray); + } else if (!keyInfo.is_secure()) { + KeyFormattingUtils.setStatusImage( + context, + vStatusIcon, + null, + KeyFormattingUtils.State.INSECURE, + R.color.key_flag_gray + ); + + vStatusIcon.setVisibility(View.VISIBLE); + textColor = ContextCompat.getColor(context, R.color.key_flag_gray); + } else if (keyInfo.has_any_secret()) { + vStatusIcon.setVisibility(View.GONE); + textColor = FormattingUtils.getColorFromAttr(context, R.attr.colorText); + } else { + // this is a public key - show if it's verified + if (keyInfo.is_verified()) { + KeyFormattingUtils.setStatusImage( + context, + vStatusIcon, + KeyFormattingUtils.State.VERIFIED + ); + + vStatusIcon.setVisibility(View.VISIBLE); + } else { + KeyFormattingUtils.setStatusImage( + context, + vStatusIcon, + KeyFormattingUtils.State.UNVERIFIED + ); + + vStatusIcon.setVisibility(View.VISIBLE); + } + textColor = FormattingUtils.getColorFromAttr(context, R.attr.colorText); + } + + vMainUserId.setTextColor(textColor); + vMainUserIdRest.setTextColor(textColor); + + if (keyInfo.has_duplicate() || keyInfo.has_any_secret()) { + vCreationDate.setText(getSecretKeyReadableTime(context, keyInfo)); + vCreationDate.setTextColor(textColor); + vCreationDate.setVisibility(View.VISIBLE); + } else { + vCreationDate.setVisibility(View.GONE); + } + } + + { // set icons + + if (!keyInfo.has_any_secret() && !keyInfo.autocrypt_package_names().isEmpty()) { + String packageName = keyInfo.autocrypt_package_names().get(0); + Drawable drawable = PackageIconGetter.getInstance(context).getDrawableForPackageName(packageName); + if (drawable != null) { + vTrustIdIcon.setImageDrawable(drawable); + vTrustIdIcon.setVisibility(View.VISIBLE); + } else { + vTrustIdIcon.setVisibility(View.GONE); + } + } else { + vTrustIdIcon.setVisibility(View.GONE); + } + } + } + + + @NonNull + private String getSecretKeyReadableTime(Context context, UnifiedKeyInfo keyInfo) { + long creationMillis = keyInfo.creation() * 1000; + + boolean allowRelativeTimestamp = keyInfo.has_duplicate(); + if (allowRelativeTimestamp) { + long creationAgeMillis = System.currentTimeMillis() - creationMillis; + if (creationAgeMillis < JUST_NOW_THRESHOLD) { + return context.getString(R.string.label_key_created_just_now); + } + } + + String dateTime = DateUtils.formatDateTime(context, + creationMillis, + DateUtils.FORMAT_SHOW_DATE + | DateUtils.FORMAT_SHOW_TIME + | DateUtils.FORMAT_SHOW_YEAR + | DateUtils.FORMAT_ABBREV_MONTH); + return context.getString(R.string.label_key_created, dateTime); + } + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/FlexibleKeyItemFactory.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/FlexibleKeyItemFactory.java new file mode 100644 index 000000000..7b89a64c1 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/FlexibleKeyItemFactory.java @@ -0,0 +1,60 @@ +package org.sufficientlysecure.keychain.ui.adapter; + + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import android.content.res.Resources; +import android.support.annotation.NonNull; + +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.model.Key.UnifiedKeyInfo; + + +public class FlexibleKeyItemFactory { + private Map initialsHeaderMap = new HashMap<>(); + private FlexibleKeyHeader myKeysHeader; + + public FlexibleKeyItemFactory(Resources resources) { + String myKeysHeaderText = resources.getString(R.string.my_keys); + myKeysHeader = new FlexibleKeyHeader(myKeysHeaderText); + } + + public List mapUnifiedKeyInfoToFlexibleKeyItems(List unifiedKeyInfos) { + List result = new ArrayList<>(); + if (unifiedKeyInfos == null) { + return result; + } + for (UnifiedKeyInfo unifiedKeyInfo : unifiedKeyInfos) { + FlexibleKeyHeader header = getFlexibleKeyHeader(unifiedKeyInfo); + FlexibleKeyItem flexibleKeyItem = new FlexibleKeyItem(unifiedKeyInfo, header); + result.add(flexibleKeyItem); + } + return result; + } + + private FlexibleKeyHeader getFlexibleKeyHeader(UnifiedKeyInfo unifiedKeyInfo) { + if (unifiedKeyInfo.has_any_secret()) { + return myKeysHeader; + } + + String headerText = getHeaderText(unifiedKeyInfo); + + FlexibleKeyHeader header; + if (initialsHeaderMap.containsKey(headerText)) { + header = initialsHeaderMap.get(headerText); + } else { + header = new FlexibleKeyHeader(headerText); + initialsHeaderMap.put(headerText, header); + } + return header; + } + + @NonNull + private String getHeaderText(UnifiedKeyInfo unifiedKeyInfo) { + String headerText = unifiedKeyInfo.name(); + return headerText == null || headerText.isEmpty() ? "" : headerText.substring(0, 1).toUpperCase(); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeyAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeyAdapter.java index f077704b6..9912cb697 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeyAdapter.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeyAdapter.java @@ -17,15 +17,20 @@ package org.sufficientlysecure.keychain.ui.adapter; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.List; + import android.content.Context; import android.database.Cursor; -import android.graphics.PorterDuff; import android.support.v4.widget.CursorAdapter; import android.text.format.DateUtils; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.ImageButton; import android.widget.ImageView; import android.widget.TextView; @@ -40,12 +45,6 @@ import org.sufficientlysecure.keychain.ui.util.Highlighter; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils.State; -import java.io.Serializable; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Date; -import java.util.List; - public class KeyAdapter extends CursorAdapter { protected String mQuery; @@ -105,8 +104,6 @@ public class KeyAdapter extends CursorAdapter { public TextView mMainUserIdRest; public TextView mCreationDate; public ImageView mStatus; - public View mSlinger; - public ImageButton mSlingerButton; public KeyItem mDisplayedItem; @@ -116,8 +113,6 @@ public class KeyAdapter extends CursorAdapter { mMainUserId = view.findViewById(R.id.key_list_item_name); mMainUserIdRest = view.findViewById(R.id.key_list_item_email); mStatus = view.findViewById(R.id.key_list_item_status_icon); - mSlinger = view.findViewById(R.id.key_list_item_slinger_view); - mSlingerButton = view.findViewById(R.id.key_list_item_slinger_button); mCreationDate = view.findViewById(R.id.key_list_item_creation); } @@ -154,28 +149,17 @@ public class KeyAdapter extends CursorAdapter { KeyFormattingUtils .setStatusImage(context, mStatus, null, State.REVOKED, R.color.key_flag_gray); mStatus.setVisibility(View.VISIBLE); - mSlinger.setVisibility(View.GONE); textColor = context.getResources().getColor(R.color.key_flag_gray); } else if (item.mIsExpired) { KeyFormattingUtils.setStatusImage(context, mStatus, null, State.EXPIRED, R.color.key_flag_gray); mStatus.setVisibility(View.VISIBLE); - mSlinger.setVisibility(View.GONE); textColor = context.getResources().getColor(R.color.key_flag_gray); } else if (!item.mIsSecure) { KeyFormattingUtils.setStatusImage(context, mStatus, null, State.INSECURE, R.color.key_flag_gray); mStatus.setVisibility(View.VISIBLE); - mSlinger.setVisibility(View.GONE); textColor = context.getResources().getColor(R.color.key_flag_gray); } else if (item.mIsSecret) { mStatus.setVisibility(View.GONE); - if (mSlingerButton.hasOnClickListeners()) { - mSlingerButton.setColorFilter( - FormattingUtils.getColorFromAttr(context, R.attr.colorTertiaryText), - PorterDuff.Mode.SRC_IN); - mSlinger.setVisibility(View.VISIBLE); - } else { - mSlinger.setVisibility(View.GONE); - } textColor = FormattingUtils.getColorFromAttr(context, R.attr.colorText); } else { // this is a public key - show if it's verified @@ -186,7 +170,6 @@ public class KeyAdapter extends CursorAdapter { KeyFormattingUtils.setStatusImage(context, mStatus, State.UNVERIFIED); mStatus.setVisibility(View.VISIBLE); } - mSlinger.setVisibility(View.GONE); textColor = FormattingUtils.getColorFromAttr(context, R.attr.colorText); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeySectionedListAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeySectionedListAdapter.java deleted file mode 100644 index c82bc9500..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeySectionedListAdapter.java +++ /dev/null @@ -1,691 +0,0 @@ -/* - * Copyright (C) 2017 Schürmann & Breitmoser GbR - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sufficientlysecure.keychain.ui.adapter; - -import android.content.Context; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; -import android.database.Cursor; -import android.database.MatrixCursor; -import android.database.MergeCursor; -import android.graphics.PorterDuff; -import android.graphics.drawable.Drawable; -import android.support.annotation.NonNull; -import android.support.v4.content.ContextCompat; -import android.text.TextUtils; -import android.text.format.DateUtils; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageButton; -import android.widget.ImageView; -import android.widget.TextView; - -import com.futuremind.recyclerviewfastscroll.SectionTitleProvider; - -import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.provider.KeychainContract; -import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; -import org.sufficientlysecure.keychain.ui.util.FormattingUtils; -import org.sufficientlysecure.keychain.ui.util.Highlighter; -import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; -import org.sufficientlysecure.keychain.ui.util.adapter.CursorAdapter; -import org.sufficientlysecure.keychain.ui.util.adapter.SectionCursorAdapter; -import timber.log.Timber; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; - -public class KeySectionedListAdapter extends SectionCursorAdapter implements SectionTitleProvider { - - private static final short VIEW_ITEM_TYPE_KEY = 0x0; - private static final short VIEW_ITEM_TYPE_DUMMY = 0x1; - - private static final short VIEW_SECTION_TYPE_PRIVATE = 0x0; - private static final short VIEW_SECTION_TYPE_PUBLIC = 0x1; - - private static final long JUST_NOW_TIMESPAN = DateUtils.MINUTE_IN_MILLIS * 5; - - private String mQuery; - private List mSelected; - private KeyListListener mListener; - - private boolean mHasDummy = false; - - public KeySectionedListAdapter(Context context, Cursor cursor) { - super(context, KeyListCursor.wrap(cursor, KeyListCursor.class), 0); - - mQuery = ""; - mSelected = new ArrayList<>(); - } - - public void setSearchQuery(String query) { - mQuery = query; - } - - - @Override - public void onContentChanged() { - mHasDummy = false; - mSelected.clear(); - - if (mListener != null) { - mListener.onSelectionStateChanged(0); - } - - super.onContentChanged(); - } - - @Override - public KeyListCursor swapCursor(KeyListCursor cursor) { - if (cursor != null && (mQuery == null || TextUtils.isEmpty(mQuery))) { - boolean isSecret = cursor.moveToFirst() && cursor.isSecret(); - - if (!isSecret) { - MatrixCursor headerCursor = new MatrixCursor(KeyListCursor.PROJECTION); - Long[] row = new Long[KeyListCursor.PROJECTION.length]; - row[cursor.getColumnIndex(KeychainContract.KeyRings.HAS_ANY_SECRET)] = 1L; - row[cursor.getColumnIndex(KeychainContract.KeyRings.MASTER_KEY_ID)] = 0L; - headerCursor.addRow(row); - - Cursor[] toMerge = { - headerCursor, - cursor.getWrappedCursor() - }; - - cursor = KeyListCursor.wrap(new MergeCursor(toMerge)); - } - } - - return super.swapCursor(cursor); - } - - public void setKeyListener(KeyListListener listener) { - mListener = listener; - } - - private int getSelectedCount() { - return mSelected.size(); - } - - private void selectPosition(int position) { - mSelected.add(position); - notifyItemChanged(position); - } - - private void deselectPosition(int position) { - mSelected.remove(Integer.valueOf(position)); - notifyItemChanged(position); - } - - private boolean isSelected(int position) { - return mSelected.contains(position); - } - - public long[] getSelectedMasterKeyIds() { - long[] keys = new long[mSelected.size()]; - for (int i = 0; i < keys.length; i++) { - int index = getCursorPositionWithoutSections(mSelected.get(i)); - if (!moveCursor(index)) { - return keys; - } - - keys[i] = getIdFromCursor(getCursor()); - } - - return keys; - } - - public boolean isAnySecretKeySelected() { - for (int i = 0; i < mSelected.size(); i++) { - int index = getCursorPositionWithoutSections(mSelected.get(i)); - if (!moveCursor(index)) { - return false; - } - - if (getCursor().isSecret()) { - return true; - } - } - - return false; - } - - - /** - * Returns the number of database entries displayed. - * - * @return The item count - */ - public int getCount() { - if (getCursor() != null) { - return getCursor().getCount() - (mHasDummy ? 1 : 0); - } else { - return 0; - } - } - - @Override - public long getIdFromCursor(KeyListCursor cursor) { - return cursor.getKeyId(); - } - - @Override - protected Character getSectionFromCursor(KeyListCursor cursor) throws IllegalStateException { - if (cursor.isSecret()) { - if (cursor.getKeyId() == 0L) { - mHasDummy = true; - } - - return '#'; - } else { - String name = cursor.getName(); - if (name != null) { - return Character.toUpperCase(name.charAt(0)); - } else { - return '?'; - } - } - } - - @Override - protected short getSectionHeaderViewType(int sectionIndex) { - return (sectionIndex < 1) ? - VIEW_SECTION_TYPE_PRIVATE : - VIEW_SECTION_TYPE_PUBLIC; - } - - @Override - protected short getSectionItemViewType(int position) { - if (moveCursor(position)) { - KeyListCursor c = getCursor(); - - if (c.isSecret() && c.getKeyId() == 0L) { - return VIEW_ITEM_TYPE_DUMMY; - } - } else { - Timber.w("Unable to determine key view type. " - + "Reason: Could not move cursor over dataset."); - } - - return VIEW_ITEM_TYPE_KEY; - } - - @Override - protected KeyHeaderViewHolder onCreateSectionViewHolder(ViewGroup parent, int viewType) { - switch (viewType) { - case VIEW_SECTION_TYPE_PUBLIC: - return new KeyHeaderViewHolder(LayoutInflater.from(parent.getContext()) - .inflate(R.layout.key_list_header_public, parent, false)); - - case VIEW_SECTION_TYPE_PRIVATE: - return new KeyHeaderViewHolder(LayoutInflater.from(parent.getContext()) - .inflate(R.layout.key_list_header_private, parent, false)); - - default: - return null; - } - } - - @Override - protected ViewHolder onCreateItemViewHolder(ViewGroup parent, int viewType) { - switch (viewType) { - case VIEW_ITEM_TYPE_KEY: - return new KeyItemViewHolder(LayoutInflater.from(parent.getContext()) - .inflate(R.layout.key_list_item, parent, false)); - - case VIEW_ITEM_TYPE_DUMMY: - return new KeyDummyViewHolder(LayoutInflater.from(parent.getContext()) - .inflate(R.layout.key_list_dummy, parent, false)); - - default: - return null; - } - - } - - @Override - protected void onBindSectionViewHolder(KeyHeaderViewHolder holder, Character section) { - switch (holder.getItemViewTypeWithoutSections()) { - case VIEW_SECTION_TYPE_PUBLIC: { - String title = section.equals('?') ? - getContext().getString(R.string.user_id_no_name) : - String.valueOf(section); - - holder.bind(title); - break; - } - - case VIEW_SECTION_TYPE_PRIVATE: { - int count = getCount(); - String title = getContext().getResources() - .getQuantityString(R.plurals.n_keys, count, count); - holder.bind(title); - break; - } - - } - } - - @Override - protected void onBindItemViewHolder(ViewHolder holder, KeyListCursor cursor) { - if (holder.getItemViewTypeWithoutSections() == VIEW_ITEM_TYPE_KEY) { - Highlighter highlighter = new Highlighter(getContext(), mQuery); - ((KeyItemViewHolder) holder).bindKey(cursor, highlighter); - } - } - - public void finishSelection() { - Integer[] selected = mSelected.toArray( - new Integer[mSelected.size()] - ); - - mSelected.clear(); - - for (Integer aSelected : selected) { - notifyItemChanged(aSelected); - } - } - - @Override - public String getSectionTitle(int position) { - // this String will be shown in a bubble for specified position - if (moveCursor(getCursorPositionWithoutSections(position))) { - KeyListCursor cursor = getCursor(); - - if (cursor.isSecret()) { - if (cursor.getKeyId() == 0L) { - mHasDummy = true; - } - - return "My"; - } else { - String name = cursor.getName(); - if (name != null) { - return name.substring(0, 1).toUpperCase(); - } else { - return null; - } - } - } else { - Timber.w("Unable to determine section title. " - + "Reason: Could not move cursor over dataset."); - return null; - } - } - - private class KeyDummyViewHolder extends SectionCursorAdapter.ViewHolder - implements View.OnClickListener { - - KeyDummyViewHolder(View itemView) { - super(itemView); - - itemView.setClickable(true); - itemView.setOnClickListener(this); - itemView.setEnabled(getSelectedCount() == 0); - } - - @Override - public void onClick(View view) { - if (mListener != null) { - mListener.onKeyDummyItemClicked(); - } - } - } - - public class KeyItemViewHolder extends SectionCursorAdapter.ViewHolder - implements View.OnClickListener, View.OnLongClickListener { - - private final ImageView mTrustIdIcon; - private final TextView mMainUserId; - private final TextView mMainUserIdRest; - private final TextView mCreationDate; - private final ImageView mStatus; - private final View mSlinger; - private final ImageButton mSlingerButton; - - KeyItemViewHolder(View itemView) { - super(itemView); - - mMainUserId = itemView.findViewById(R.id.key_list_item_name); - mMainUserIdRest = itemView.findViewById(R.id.key_list_item_email); - mStatus = itemView.findViewById(R.id.key_list_item_status_icon); - mSlinger = itemView.findViewById(R.id.key_list_item_slinger_view); - mSlingerButton = itemView.findViewById(R.id.key_list_item_slinger_button); - mCreationDate = itemView.findViewById(R.id.key_list_item_creation); - mTrustIdIcon = itemView.findViewById(R.id.key_list_item_tid_icon); - - itemView.setClickable(true); - itemView.setLongClickable(true); - itemView.setOnClickListener(this); - itemView.setOnLongClickListener(this); - - mSlingerButton.setClickable(true); - mSlingerButton.setOnClickListener(this); - } - - void bindKey(KeyListCursor keyItem, Highlighter highlighter) { - itemView.setSelected(isSelected(getAdapterPosition())); - Context context = itemView.getContext(); - - { // set name and stuff, common to both key types - String name = keyItem.getName(); - String email = keyItem.getEmail(); - if (name == null) { - if (email != null) { - mMainUserId.setText(highlighter.highlight(email)); - mMainUserIdRest.setVisibility(View.GONE); - } else { - mMainUserId.setText(R.string.user_id_no_name); - } - } else { - mMainUserId.setText(highlighter.highlight(name)); - if (email != null) { - mMainUserIdRest.setText(highlighter.highlight(email)); - mMainUserIdRest.setVisibility(View.VISIBLE); - } else { - mMainUserIdRest.setVisibility(View.GONE); - } - } - } - - { // set edit button and status, specific by key type. Note: order is important! - int textColor; - if (keyItem.isRevoked()) { - KeyFormattingUtils.setStatusImage( - context, - mStatus, - null, - KeyFormattingUtils.State.REVOKED, - R.color.key_flag_gray - ); - - mStatus.setVisibility(View.VISIBLE); - mSlinger.setVisibility(View.GONE); - textColor = ContextCompat.getColor(context, R.color.key_flag_gray); - } else if (keyItem.isExpired()) { - KeyFormattingUtils.setStatusImage( - context, - mStatus, - null, - KeyFormattingUtils.State.EXPIRED, - R.color.key_flag_gray - ); - - mStatus.setVisibility(View.VISIBLE); - mSlinger.setVisibility(View.GONE); - textColor = ContextCompat.getColor(context, R.color.key_flag_gray); - } else if (!keyItem.isSecure()) { - KeyFormattingUtils.setStatusImage( - context, - mStatus, - null, - KeyFormattingUtils.State.INSECURE, - R.color.key_flag_gray - ); - - mStatus.setVisibility(View.VISIBLE); - mSlinger.setVisibility(View.GONE); - textColor = ContextCompat.getColor(context, R.color.key_flag_gray); - } else if (keyItem.isSecret()) { - mStatus.setVisibility(View.GONE); - if (mSlingerButton.hasOnClickListeners()) { - mSlingerButton.setColorFilter( - FormattingUtils.getColorFromAttr(context, R.attr.colorTertiaryText), - PorterDuff.Mode.SRC_IN - ); - - mSlinger.setVisibility(View.VISIBLE); - } else { - mSlinger.setVisibility(View.GONE); - } - textColor = FormattingUtils.getColorFromAttr(context, R.attr.colorText); - } else { - // this is a public key - show if it's verified - if (keyItem.isVerified()) { - KeyFormattingUtils.setStatusImage( - context, - mStatus, - KeyFormattingUtils.State.VERIFIED - ); - - mStatus.setVisibility(View.VISIBLE); - } else { - KeyFormattingUtils.setStatusImage( - context, - mStatus, - KeyFormattingUtils.State.UNVERIFIED - ); - - mStatus.setVisibility(View.VISIBLE); - } - mSlinger.setVisibility(View.GONE); - textColor = FormattingUtils.getColorFromAttr(context, R.attr.colorText); - } - - mMainUserId.setTextColor(textColor); - mMainUserIdRest.setTextColor(textColor); - - if (keyItem.hasDuplicate() || keyItem.isSecret()) { - mCreationDate.setText(getSecretKeyReadableTime(context, keyItem)); - mCreationDate.setTextColor(textColor); - mCreationDate.setVisibility(View.VISIBLE); - } else { - mCreationDate.setVisibility(View.GONE); - } - } - - { // set icons - List packageNames = keyItem.getAutocryptPeerIdPackages(); - - if (!keyItem.isSecret() && !packageNames.isEmpty()) { - String packageName = packageNames.get(0); - Drawable drawable = getDrawableForPackageName(packageName); - if (drawable != null) { - mTrustIdIcon.setImageDrawable(drawable); - mTrustIdIcon.setVisibility(View.VISIBLE); - } else { - mTrustIdIcon.setVisibility(View.GONE); - } - } else { - mTrustIdIcon.setVisibility(View.GONE); - } - } - } - - @NonNull - private String getSecretKeyReadableTime(Context context, KeyListCursor keyItem) { - long creationMillis = keyItem.getCreationTime(); - - boolean allowRelativeTimestamp = keyItem.hasDuplicate(); - if (allowRelativeTimestamp) { - long creationAgeMillis = System.currentTimeMillis() - creationMillis; - if (creationAgeMillis < JUST_NOW_TIMESPAN) { - return context.getString(R.string.label_key_created_just_now); - } - } - - String dateTime = DateUtils.formatDateTime(context, - creationMillis, - DateUtils.FORMAT_SHOW_DATE - | DateUtils.FORMAT_SHOW_TIME - | DateUtils.FORMAT_SHOW_YEAR - | DateUtils.FORMAT_ABBREV_MONTH); - return context.getString(R.string.label_key_created, dateTime); - } - - @Override - public void onClick(View v) { - int pos = getAdapterPosition(); - switch (v.getId()) { - case R.id.key_list_item_slinger_button: - if (mListener != null) { - mListener.onSlingerButtonClicked(getItemId()); - } - break; - - default: - if (getSelectedCount() == 0) { - if (mListener != null) { - mListener.onKeyItemClicked(getItemId()); - } - } else { - if (isSelected(pos)) { - deselectPosition(pos); - } else { - selectPosition(pos); - } - - if (mListener != null) { - mListener.onSelectionStateChanged(getSelectedCount()); - } - } - break; - } - - } - - @Override - public boolean onLongClick(View v) { - System.out.println("Long Click!"); - if (getSelectedCount() == 0) { - selectPosition(getAdapterPosition()); - - if (mListener != null) { - mListener.onSelectionStateChanged(getSelectedCount()); - } - return true; - } - - return false; - } - } - - static class KeyHeaderViewHolder extends SectionCursorAdapter.ViewHolder { - private TextView mText1; - - public KeyHeaderViewHolder(View itemView) { - super(itemView); - mText1 = itemView.findViewById(android.R.id.text1); - } - - public void bind(String title) { - mText1.setText(title); - } - } - - public static class KeyListCursor extends CursorAdapter.KeyCursor { - public static final String ORDER = KeychainContract.KeyRings.HAS_ANY_SECRET - + " DESC, " + KeychainContract.KeyRings.USER_ID + " COLLATE NOCASE ASC"; - - public static final String[] PROJECTION; - - static { - ArrayList arr = new ArrayList<>(); - arr.addAll(Arrays.asList(KeyCursor.PROJECTION)); - arr.addAll(Arrays.asList( - KeychainContract.KeyRings.VERIFIED, - KeychainContract.KeyRings.HAS_ANY_SECRET, - KeychainContract.KeyRings.FINGERPRINT, - KeychainContract.KeyRings.HAS_ENCRYPT, - KeychainContract.KeyRings.API_KNOWN_TO_PACKAGE_NAMES - )); - - PROJECTION = arr.toArray(new String[arr.size()]); - } - - public static KeyListCursor wrap(Cursor cursor) { - if (cursor != null) { - return new KeyListCursor(cursor); - } else { - return null; - } - } - - private KeyListCursor(Cursor cursor) { - super(cursor); - } - - public boolean hasEncrypt() { - int index = getColumnIndexOrThrow(KeychainContract.KeyRings.HAS_ENCRYPT); - return getInt(index) != 0; - } - - public byte[] getRawFingerprint() { - int index = getColumnIndexOrThrow(KeychainContract.KeyRings.FINGERPRINT); - return getBlob(index); - } - - public String getFingerprint() { - return KeyFormattingUtils.convertFingerprintToHex(getRawFingerprint()); - } - - public boolean isSecret() { - int index = getColumnIndexOrThrow(KeychainContract.KeyRings.HAS_ANY_SECRET); - return getInt(index) != 0; - } - - public boolean isVerified() { - int index = getColumnIndexOrThrow(KeychainContract.KeyRings.VERIFIED); - return getInt(index) > 0; - } - - public List getAutocryptPeerIdPackages() { - int index = getColumnIndexOrThrow(KeyRings.API_KNOWN_TO_PACKAGE_NAMES); - String packageNames = getString(index); - if (packageNames == null) { - return Collections.EMPTY_LIST; - } - return Arrays.asList(packageNames.split(",")); - } - } - - public interface KeyListListener { - void onKeyDummyItemClicked(); - - void onKeyItemClicked(long masterKeyId); - - void onSlingerButtonClicked(long masterKeyId); - - void onSelectionStateChanged(int selectedCount); - } - - private HashMap appIconCache = new HashMap<>(); - - private Drawable getDrawableForPackageName(String packageName) { - if (appIconCache.containsKey(packageName)) { - return appIconCache.get(packageName); - } - - PackageManager pm = getContext().getPackageManager(); - try { - ApplicationInfo ai = pm.getApplicationInfo(packageName, 0); - - Drawable appIcon = pm.getApplicationIcon(ai); - appIconCache.put(packageName, appIcon); - - return appIcon; - } catch (PackageManager.NameNotFoundException e) { - return null; - } - } -} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/adapter/SectionCursorAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/adapter/SectionCursorAdapter.java deleted file mode 100644 index 4af5faf82..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/adapter/SectionCursorAdapter.java +++ /dev/null @@ -1,337 +0,0 @@ -/* - * Copyright (C) 2017 Schürmann & Breitmoser GbR - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sufficientlysecure.keychain.ui.util.adapter; - -import android.content.Context; -import android.support.v4.util.SparseArrayCompat; -import android.support.v7.widget.RecyclerView; -import android.view.View; -import android.view.ViewGroup; - -import com.tonicartos.superslim.LayoutManager; - -import org.sufficientlysecure.keychain.ui.util.adapter.CursorAdapter.SimpleCursor; -import timber.log.Timber; - - -/** - * @param section type. - * @param the view holder extending {@code BaseViewHolder} that is bound to the cursor data. - * @param the view holder extending {@code BaseViewHolder<>} that is bound to the section data. - */ -public abstract class SectionCursorAdapter extends CursorAdapter { - - public static final String TAG = "SectionCursorAdapter"; - - private static final short VIEW_TYPE_ITEM = 0x1; - private static final short VIEW_TYPE_SECTION = 0x2; - - private SparseArrayCompat mSectionMap = new SparseArrayCompat<>(); - private Comparator mSectionComparator; - - public SectionCursorAdapter(Context context, C cursor, int flags) { - this(context, cursor, flags, new Comparator() { - @Override - public boolean equal(T obj1, T obj2) { - return (obj1 == null) ? - obj2 == null : obj1.equals(obj2); - } - }); - } - - public SectionCursorAdapter(Context context, C cursor, int flags, Comparator comparator) { - super(context, cursor, flags); - setSectionComparator(comparator); - } - - @Override - public void onContentChanged() { - if (hasValidData()) { - buildSections(); - } else { - mSectionMap.clear(); - } - - super.onContentChanged(); - } - - /** - * Assign a comparator which will be used to check whether - * a section is contained in the list of sections. The default implementation - * will check for null pointers and compare sections using the {@link #equals(Object)} method. - * @param comparator The comparator to compare section objects. - */ - public void setSectionComparator(Comparator comparator) { - this.mSectionComparator = comparator; - buildSections(); - } - - /** - * If the adapter's cursor is not null then this method will call buildSections(Cursor cursor). - */ - private void buildSections() { - if (hasValidData()) { - moveCursor(-1); - try { - mSectionMap.clear(); - appendSections(getCursor()); - } catch (IllegalStateException e) { - Timber.e(e, "Couldn't build sections. Perhaps you're moving the cursor" + - "in #getSectionFromCursor(Cursor)?"); - swapCursor(null); - - mSectionMap.clear(); - } - } - } - - private void appendSections(C cursor) throws IllegalStateException { - int cursorPosition = 0; - while(hasValidData() && cursor.moveToNext()) { - T section = getSectionFromCursor(cursor); - if (cursor.getPosition() != cursorPosition) { - throw new IllegalStateException("Do not move the cursor's position in getSectionFromCursor."); - } - if (!hasSection(section)) { - mSectionMap.append(cursorPosition + mSectionMap.size(), section); - } - cursorPosition++; - } - } - - public boolean hasSection(T section) { - for(int i = 0; i < mSectionMap.size(); i++) { - T obj = mSectionMap.valueAt(i); - if(mSectionComparator.equal(obj, section)) - return true; - } - - return false; - } - - /** - * The object which is return will determine what section this cursor position will be in. - * @return the section from the cursor at its current position. - * This object will be passed to newSectionView and bindSectionView. - */ - protected abstract T getSectionFromCursor(C cursor) throws IllegalStateException; - - /** - * Return the id of the item represented by the row the cursor - * is currently moved to. - * @param section The section item to get the id from - * @return The id of the dataset - */ - public long getIdFromSection(T section) { - return section != null ? section.hashCode() : 0L; - } - - @Override - public int getItemCount() { - return super.getItemCount() + mSectionMap.size(); - } - - @Override - public final long getItemId(int listPosition) { - int index = mSectionMap.indexOfKey(listPosition); - if (index < 0) { - int cursorPosition = getCursorPositionWithoutSections(listPosition); - return super.getItemId(cursorPosition); - } else { - T section = mSectionMap.valueAt(index); - return getIdFromSection(section); - } - } - - /** - * @param listPosition the position of the current item in the list with mSectionMap included - * @return Whether or not the listPosition points to a section. - */ - public boolean isSection(int listPosition) { - return mSectionMap.indexOfKey(listPosition) >= 0; - } - - /** - * This will map a position in the list adapter (which includes mSectionMap) to a position in - * the cursor (which does not contain mSectionMap). - * - * @param listPosition the position of the current item in the list with mSectionMap included - * @return the correct position to use with the cursor - */ - public int getCursorPositionWithoutSections(int listPosition) { - if (mSectionMap.size() == 0) { - return listPosition; - } else if (!isSection(listPosition)) { - int sectionIndex = getSectionForPosition(listPosition); - if (isListPositionBeforeFirstSection(listPosition, sectionIndex)) { - return listPosition; - } else { - return listPosition - (sectionIndex + 1); - } - } else { - return -1; - } - } - - public int getListPosition(int cursorPosition) { - for(int i = 0; i < mSectionMap.size(); i++) { - int sectionIndex = mSectionMap.keyAt(i); - if (sectionIndex > cursorPosition) { - return cursorPosition; - } - - cursorPosition +=1; - } - - return cursorPosition; - } - - /** - * Given the list position of an item in the adapter, returns the - * adapter position of the first item of the section the given item belongs to. - * @param listPosition The absolute list position. - * @return The position of the first item of the section. - */ - public int getFirstSectionPosition(int listPosition) { - int start = 0; - for(int i = 0; i <= listPosition; i++) { - if(isSection(i)) { - start = i; - } - } - - return start; - } - - - public int getSectionForPosition(int listPosition) { - boolean isSection = false; - int numPrecedingSections = 0; - for (int i = 0; i < mSectionMap.size(); i++) { - int sectionPosition = mSectionMap.keyAt(i); - - if (listPosition > sectionPosition) { - numPrecedingSections++; - } else if (listPosition == sectionPosition) { - isSection = true; - } else { - break; - } - } - - return isSection ? numPrecedingSections : Math.max(numPrecedingSections - 1, 0); - } - - private boolean isListPositionBeforeFirstSection(int listPosition, int sectionIndex) { - boolean hasSections = mSectionMap != null && mSectionMap.size() > 0; - return sectionIndex == 0 && hasSections && listPosition < mSectionMap.keyAt(0); - } - - @Override - public final int getItemViewType(int listPosition) { - int sectionIndex = mSectionMap.indexOfKey(listPosition); - if(sectionIndex < 0) { - int cursorPosition = getCursorPositionWithoutSections(listPosition); - return (getSectionItemViewType(cursorPosition) << 16) | VIEW_TYPE_ITEM; - } else { - return (getSectionHeaderViewType(sectionIndex) << 16) | VIEW_TYPE_SECTION; - } - } - - protected short getSectionHeaderViewType(int sectionIndex) { - return 0; - } - - protected short getSectionItemViewType(int position) { - return 0; - } - - @Override - @SuppressWarnings("unchecked") - public final void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { - LayoutManager.LayoutParams layoutParams = LayoutManager.LayoutParams - .from(holder.itemView.getLayoutParams()); - - // assign first position of section to each item - layoutParams.setFirstPosition(getFirstSectionPosition(position)); - - int viewType = holder.getItemViewType() & 0xFF; - switch (viewType) { - case VIEW_TYPE_ITEM : - moveCursorOrThrow(getCursorPositionWithoutSections(position)); - onBindItemViewHolder((VH) holder, getCursor()); - - layoutParams.isHeader = false; - break; - - case VIEW_TYPE_SECTION: - T section = mSectionMap.get(position); - onBindSectionViewHolder((SH) holder, section); - - layoutParams.isHeader = true; - break; - } - - holder.itemView.setLayoutParams(layoutParams); - } - - @Override - public final RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - switch (viewType & 0xFF) { - case VIEW_TYPE_SECTION: - return onCreateSectionViewHolder(parent, viewType >> 16); - - case VIEW_TYPE_ITEM: - return onCreateItemViewHolder(parent, viewType >> 16); - - default: - return null; - } - } - - protected abstract SH onCreateSectionViewHolder(ViewGroup parent, int viewType); - protected abstract VH onCreateItemViewHolder(ViewGroup parent, int viewType); - - protected abstract void onBindSectionViewHolder(SH holder, T section); - protected abstract void onBindItemViewHolder(VH holder, C cursor); - - public interface Comparator { - boolean equal(T obj1, T obj2); - } - - public static class ViewHolder extends RecyclerView.ViewHolder { - public ViewHolder(View itemView) { - super(itemView); - } - - /** - * Returns the view type assigned in - * {@link SectionCursorAdapter#getSectionHeaderViewType(int)} or - * {@link SectionCursorAdapter#getSectionItemViewType(int)} - * - * Note that a call to {@link #getItemViewType()} will return a value that contains - * internal stuff necessary to distinguish sections from items. - * @return The view type you set. - */ - public short getItemViewTypeWithoutSections(){ - return (short) (getItemViewType() >> 16); - } - } -} - diff --git a/OpenKeychain/src/main/res/drawable-v21/list_item_ripple.xml b/OpenKeychain/src/main/res/drawable-v21/list_item_ripple.xml index 32d726ac1..d76e28dbb 100644 --- a/OpenKeychain/src/main/res/drawable-v21/list_item_ripple.xml +++ b/OpenKeychain/src/main/res/drawable-v21/list_item_ripple.xml @@ -7,6 +7,7 @@ + \ No newline at end of file diff --git a/OpenKeychain/src/main/res/layout/key_list_fragment.xml b/OpenKeychain/src/main/res/layout/key_list_fragment.xml index d53afb940..3c1572a82 100644 --- a/OpenKeychain/src/main/res/layout/key_list_fragment.xml +++ b/OpenKeychain/src/main/res/layout/key_list_fragment.xml @@ -34,7 +34,7 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - @@ -43,18 +43,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:clipToPadding="false" - android:paddingBottom="72dp" - android:paddingLeft="16dp" - android:paddingRight="32dp" - android:paddingStart="16dp" /> - - + android:paddingBottom="72dp" /> @@ -149,6 +138,21 @@ + + diff --git a/OpenKeychain/src/main/res/layout/key_list_header_public.xml b/OpenKeychain/src/main/res/layout/key_list_header_public.xml index 97fb67984..6391a3bb7 100644 --- a/OpenKeychain/src/main/res/layout/key_list_header_public.xml +++ b/OpenKeychain/src/main/res/layout/key_list_header_public.xml @@ -1,16 +1,12 @@ + android:paddingRight="12dp" + android:paddingLeft="12dp"> - - - - - - - -