use FlexibleAdapter with LiveData in KeyListFragment
This commit is contained in:
parent
72f3ed89a6
commit
f87209d242
|
@ -36,7 +36,9 @@ dependencies {
|
||||||
|
|
||||||
// RecyclerView
|
// RecyclerView
|
||||||
compile 'com.tonicartos:superslim:0.4.13'
|
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
|
// Material Drawer
|
||||||
compile 'com.mikepenz:materialdrawer:5.6.0@aar'
|
compile 'com.mikepenz:materialdrawer:5.6.0@aar'
|
||||||
|
@ -107,6 +109,7 @@ dependencies {
|
||||||
// Comment out the libs referenced as git submodules!
|
// Comment out the libs referenced as git submodules!
|
||||||
dependencyVerification {
|
dependencyVerification {
|
||||||
verify = [
|
verify = [
|
||||||
|
'eu.davidea:flexible-adapter-ui:7ed5327d15c823e5fcf7d6e1017d8a47d079d1adc7141858f3cb427517ef35cd',
|
||||||
'com.android.support:design:7225973f7ee03765008a9c2f17a40b154c6885169fef022276e811c926a2202c',
|
'com.android.support:design:7225973f7ee03765008a9c2f17a40b154c6885169fef022276e811c926a2202c',
|
||||||
'com.journeyapps:zxing-android-embedded:2422d83c2c09a7b645f516c8458ececba6a7da47b94e40778d876facf495c660',
|
'com.journeyapps:zxing-android-embedded:2422d83c2c09a7b645f516c8458ececba6a7da47b94e40778d876facf495c660',
|
||||||
'org.sufficientlysecure:donations:2be4183afa5e35263e37346344cfea48681f3c987e6832dd4acde227c13ccad6',
|
'org.sufficientlysecure:donations:2be4183afa5e35263e37346344cfea48681f3c987e6832dd4acde227c13ccad6',
|
||||||
|
@ -115,6 +118,7 @@ dependencyVerification {
|
||||||
'com.mikepenz:materialize:942ccf5e2aa1a46803aa884e8dc7bbaf2a9e8e9996a0cf92e3fe2f44a8592ba4',
|
'com.mikepenz:materialize:942ccf5e2aa1a46803aa884e8dc7bbaf2a9e8e9996a0cf92e3fe2f44a8592ba4',
|
||||||
'com.android.support:appcompat-v7:0c7808fbbc5838d831e32e3c0a6f84e1f2c981deb8f11e010650f2b57923a335',
|
'com.android.support:appcompat-v7:0c7808fbbc5838d831e32e3c0a6f84e1f2c981deb8f11e010650f2b57923a335',
|
||||||
'com.nispok:snackbar:46b5eb9d630d329e13c2ce00ee9fb115ffb66c23c72cff32ee97eedd76824c6f',
|
'com.nispok:snackbar:46b5eb9d630d329e13c2ce00ee9fb115ffb66c23c72cff32ee97eedd76824c6f',
|
||||||
|
'eu.davidea:flexible-adapter:560e940e8cf0f4ed8f632f5f89527deeda7a61cce5f02f42cc0983f7c0d2de5f',
|
||||||
'com.android.support:recyclerview-v7:d735e4727878e99ef3980c10d15dc3468462fd509d4fb60cb8bd20b0f735085c',
|
'com.android.support:recyclerview-v7:d735e4727878e99ef3980c10d15dc3468462fd509d4fb60cb8bd20b0f735085c',
|
||||||
'com.android.support:cardview-v7:8ed955dd037d82a7b4bbcaedb4f896523c3e4c1bf3ca698ce807c350767a2886',
|
'com.android.support:cardview-v7:8ed955dd037d82a7b4bbcaedb4f896523c3e4c1bf3ca698ce807c350767a2886',
|
||||||
'org.sufficientlysecure:html-textview:ed740adf05cae2373999c7a3047c803183d9807b2cf66162902090d7c112a832',
|
'org.sufficientlysecure:html-textview:ed740adf05cae2373999c7a3047c803183d9807b2cf66162902090d7c112a832',
|
||||||
|
@ -145,6 +149,7 @@ dependencyVerification {
|
||||||
'org.apache.james:apache-mime4j-core:561987f604911e1870b2b4eabf0b0658d666c66cb1e65fba3e9e4bffe63acab9',
|
'org.apache.james:apache-mime4j-core:561987f604911e1870b2b4eabf0b0658d666c66cb1e65fba3e9e4bffe63acab9',
|
||||||
'com.splitwise:tokenautocomplete:f921f83ee26b5265f719b312c30452ef8e219557826c5ce5bf02e29647967939',
|
'com.splitwise:tokenautocomplete:f921f83ee26b5265f719b312c30452ef8e219557826c5ce5bf02e29647967939',
|
||||||
'com.cocosw:bottomsheet:85bd91fd837b02ebd7a888501cb26035c7cd985a6aa87303fca249da8231a2c3',
|
'com.cocosw:bottomsheet:85bd91fd837b02ebd7a888501cb26035c7cd985a6aa87303fca249da8231a2c3',
|
||||||
|
'eu.davidea:flexible-adapter-livedata:c8718b46ff4fbf290ea18f0c5bfe8326badeadf5fd95899a1404c561a24f48a1',
|
||||||
'com.mikepenz:materialdrawer:8bba1428dcef5ad7c2decf49c612ad980b38e2f1031cbd66c152a8a104793929',
|
'com.mikepenz:materialdrawer:8bba1428dcef5ad7c2decf49c612ad980b38e2f1031cbd66c152a8a104793929',
|
||||||
'com.mikepenz:iconics-core:478d7e245098f7c28b5b20a0e6b1e5cb108ef3eaf595af7190bc60f91063aa3d',
|
'com.mikepenz:iconics-core:478d7e245098f7c28b5b20a0e6b1e5cb108ef3eaf595af7190bc60f91063aa3d',
|
||||||
'com.mikepenz:google-material-typeface:f27c629ba5d2a90ecfbd7f221ff98cd363e1ee6be06b099b82bae490766e14a5',
|
'com.mikepenz:google-material-typeface:f27c629ba5d2a90ecfbd7f221ff98cd363e1ee6be06b099b82bae490766e14a5',
|
||||||
|
|
|
@ -28,12 +28,10 @@ import android.widget.ViewAnimator;
|
||||||
|
|
||||||
import com.nispok.snackbar.Snackbar;
|
import com.nispok.snackbar.Snackbar;
|
||||||
|
|
||||||
import org.hamcrest.BaseMatcher;
|
|
||||||
import org.hamcrest.Description;
|
import org.hamcrest.Description;
|
||||||
import org.hamcrest.Matcher;
|
import org.hamcrest.Matcher;
|
||||||
import org.sufficientlysecure.keychain.R;
|
import org.sufficientlysecure.keychain.R;
|
||||||
import org.sufficientlysecure.keychain.ui.adapter.KeyAdapter.KeyItem;
|
import org.sufficientlysecure.keychain.ui.adapter.KeyAdapter.KeyItem;
|
||||||
import org.sufficientlysecure.keychain.ui.adapter.KeySectionedListAdapter;
|
|
||||||
import org.sufficientlysecure.keychain.ui.widget.EncryptKeyCompletionView;
|
import org.sufficientlysecure.keychain.ui.widget.EncryptKeyCompletionView;
|
||||||
|
|
||||||
import static android.support.test.espresso.matcher.ViewMatchers.hasDescendant;
|
import static android.support.test.espresso.matcher.ViewMatchers.hasDescendant;
|
||||||
|
@ -90,15 +88,14 @@ public abstract class CustomMatchers {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Matcher<RecyclerView.ViewHolder> withKeyHolderId(final long keyId) {
|
public static Matcher<RecyclerView.ViewHolder> withKeyHolderId(final long keyId) {
|
||||||
return new BoundedMatcher<RecyclerView.ViewHolder, KeySectionedListAdapter.KeyItemViewHolder>
|
return new BoundedMatcher<RecyclerView.ViewHolder, RecyclerView.ViewHolder>(RecyclerView.ViewHolder.class) {
|
||||||
(KeySectionedListAdapter.KeyItemViewHolder.class) {
|
|
||||||
@Override
|
@Override
|
||||||
public void describeTo(Description description) {
|
public void describeTo(Description description) {
|
||||||
description.appendText("with ViewHolder id: " + keyId);
|
description.appendText("with ViewHolder id: " + keyId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean matchesSafely(KeySectionedListAdapter.KeyItemViewHolder item) {
|
protected boolean matchesSafely(View item) {
|
||||||
return item.getItemId() == keyId;
|
return item.getItemId() == keyId;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -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) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<UnifiedKeyInfo> getUnifiedKeyInfo() {
|
||||||
|
SqlDelightQuery query = Key.FACTORY.selectAllUnifiedKeyInfo();
|
||||||
|
List<UnifiedKeyInfo> 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -28,8 +28,8 @@ public abstract class AutocryptPeer implements AutocryptPeersModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isGossipKeyRevoked() {
|
public boolean isGossipKeyRevoked() {
|
||||||
Long revokedInt = gossip_key_is_revoked_int();
|
Boolean gossip_key_is_revoked = gossip_key_is_revoked_int();
|
||||||
return revokedInt != null && revokedInt != 0;
|
return gossip_key_is_revoked != null && gossip_key_is_revoked;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isGossipKeyExpired() {
|
public boolean isGossipKeyExpired() {
|
||||||
|
@ -45,8 +45,8 @@ public abstract class AutocryptPeer implements AutocryptPeersModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isKeyRevoked() {
|
public boolean isKeyRevoked() {
|
||||||
Long revokedInt = key_is_revoked_int();
|
Boolean revoked = key_is_revoked_int();
|
||||||
return revokedInt != null && revokedInt != 0;
|
return revoked != null && revoked;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isKeyExpired() {
|
public boolean isKeyExpired() {
|
||||||
|
|
|
@ -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<Key> FACTORY = new Factory<>(AutoValue_Key::new);
|
||||||
|
public static final SelectAllUnifiedKeyInfoMapper<UnifiedKeyInfo> UNIFIED_KEY_INFO_MAPPER =
|
||||||
|
FACTORY.selectAllUnifiedKeyInfoMapper(AutoValue_Key_UnifiedKeyInfo::new);
|
||||||
|
|
||||||
|
@AutoValue
|
||||||
|
public static abstract class UnifiedKeyInfo implements SelectAllUnifiedKeyInfoModel {
|
||||||
|
private List<String> 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<String> autocrypt_package_names() {
|
||||||
|
if (autocryptPackageNames == null) {
|
||||||
|
String csv = autocrypt_package_names_csv();
|
||||||
|
autocryptPackageNames = csv == null ? Collections.emptyList() :
|
||||||
|
Arrays.asList(csv.split(","));
|
||||||
|
}
|
||||||
|
return autocryptPackageNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -34,11 +34,15 @@ import android.database.SQLException;
|
||||||
import android.database.sqlite.SQLiteException;
|
import android.database.sqlite.SQLiteException;
|
||||||
import android.provider.BaseColumns;
|
import android.provider.BaseColumns;
|
||||||
|
|
||||||
|
import org.sufficientlysecure.keychain.ApiAppsModel;
|
||||||
import org.sufficientlysecure.keychain.AutocryptPeersModel;
|
import org.sufficientlysecure.keychain.AutocryptPeersModel;
|
||||||
|
import org.sufficientlysecure.keychain.CertsModel;
|
||||||
import org.sufficientlysecure.keychain.Constants;
|
import org.sufficientlysecure.keychain.Constants;
|
||||||
import org.sufficientlysecure.keychain.KeyMetadataModel;
|
import org.sufficientlysecure.keychain.KeyMetadataModel;
|
||||||
import org.sufficientlysecure.keychain.KeyRingsPublicModel;
|
import org.sufficientlysecure.keychain.KeyRingsPublicModel;
|
||||||
|
import org.sufficientlysecure.keychain.UserPacketsModel;
|
||||||
import org.sufficientlysecure.keychain.model.ApiApp;
|
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.ApiAppsAllowedKeysColumns;
|
||||||
import org.sufficientlysecure.keychain.provider.KeychainContract.CertsColumns;
|
import org.sufficientlysecure.keychain.provider.KeychainContract.CertsColumns;
|
||||||
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRingsColumns;
|
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"
|
+ 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 =
|
private static final String CREATE_KEY_SIGNATURES =
|
||||||
"CREATE TABLE IF NOT EXISTS " + Tables.KEY_SIGNATURES + " ("
|
"CREATE TABLE IF NOT EXISTS " + Tables.KEY_SIGNATURES + " ("
|
||||||
+ KeySignaturesColumns.MASTER_KEY_ID + " INTEGER NOT NULL, "
|
+ KeySignaturesColumns.MASTER_KEY_ID + " INTEGER NOT NULL, "
|
||||||
|
@ -212,14 +196,14 @@ public class KeychainDatabase {
|
||||||
|
|
||||||
db.execSQL(KeyRingsPublicModel.CREATE_TABLE);
|
db.execSQL(KeyRingsPublicModel.CREATE_TABLE);
|
||||||
db.execSQL(CREATE_KEYS);
|
db.execSQL(CREATE_KEYS);
|
||||||
db.execSQL(CREATE_USER_PACKETS);
|
db.execSQL(UserPacketsModel.CREATE_TABLE);
|
||||||
db.execSQL(CREATE_CERTS);
|
db.execSQL(CertsModel.CREATE_TABLE);
|
||||||
db.execSQL(KeyMetadataModel.CREATE_TABLE);
|
db.execSQL(KeyMetadataModel.CREATE_TABLE);
|
||||||
db.execSQL(CREATE_KEY_SIGNATURES);
|
db.execSQL(CREATE_KEY_SIGNATURES);
|
||||||
db.execSQL(CREATE_API_APPS_ALLOWED_KEYS);
|
db.execSQL(CREATE_API_APPS_ALLOWED_KEYS);
|
||||||
db.execSQL(CREATE_OVERRIDDEN_WARNINGS);
|
db.execSQL(CREATE_OVERRIDDEN_WARNINGS);
|
||||||
db.execSQL(AutocryptPeersModel.CREATE_TABLE);
|
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 keys_by_rank ON keys (" + KeysColumns.RANK + ", " + KeysColumns.MASTER_KEY_ID + ");");
|
||||||
db.execSQL("CREATE INDEX uids_by_rank ON user_packets (" + UserPacketsColumns.RANK + ", "
|
db.execSQL("CREATE INDEX uids_by_rank ON user_packets (" + UserPacketsColumns.RANK + ", "
|
||||||
|
|
|
@ -23,18 +23,19 @@ import java.util.List;
|
||||||
|
|
||||||
import android.animation.ObjectAnimator;
|
import android.animation.ObjectAnimator;
|
||||||
import android.app.Activity;
|
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.content.Intent;
|
||||||
import android.database.Cursor;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.AsyncTask;
|
import android.os.AsyncTask;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
import android.support.v4.app.FragmentActivity;
|
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.v4.view.MenuItemCompat;
|
||||||
|
import android.support.v7.widget.LinearLayoutManager;
|
||||||
|
import android.support.v7.widget.RecyclerView;
|
||||||
import android.support.v7.widget.SearchView;
|
import android.support.v7.widget.SearchView;
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.view.ActionMode;
|
import android.view.ActionMode;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
|
@ -46,55 +47,59 @@ import android.widget.Button;
|
||||||
import android.widget.ViewAnimator;
|
import android.widget.ViewAnimator;
|
||||||
|
|
||||||
import androidx.work.WorkStatus;
|
import androidx.work.WorkStatus;
|
||||||
import com.futuremind.recyclerviewfastscroll.FastScroller;
|
|
||||||
import com.getbase.floatingactionbutton.FloatingActionButton;
|
import com.getbase.floatingactionbutton.FloatingActionButton;
|
||||||
import com.getbase.floatingactionbutton.FloatingActionsMenu;
|
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.Constants;
|
||||||
import org.sufficientlysecure.keychain.R;
|
import org.sufficientlysecure.keychain.R;
|
||||||
import org.sufficientlysecure.keychain.compatibility.ClipboardReflection;
|
import org.sufficientlysecure.keychain.compatibility.ClipboardReflection;
|
||||||
import org.sufficientlysecure.keychain.keysync.KeyserverSyncManager;
|
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.BenchmarkResult;
|
||||||
import org.sufficientlysecure.keychain.operations.results.OperationResult;
|
import org.sufficientlysecure.keychain.operations.results.OperationResult;
|
||||||
import org.sufficientlysecure.keychain.pgp.PgpHelper;
|
import org.sufficientlysecure.keychain.pgp.PgpHelper;
|
||||||
import org.sufficientlysecure.keychain.provider.KeychainContract;
|
|
||||||
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
|
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
|
||||||
import org.sufficientlysecure.keychain.provider.KeychainDatabase;
|
import org.sufficientlysecure.keychain.provider.KeychainDatabase;
|
||||||
import org.sufficientlysecure.keychain.service.BenchmarkInputParcel;
|
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.CryptoOperationHelper;
|
||||||
import org.sufficientlysecure.keychain.ui.base.RecyclerFragment;
|
import org.sufficientlysecure.keychain.ui.base.RecyclerFragment;
|
||||||
import org.sufficientlysecure.keychain.ui.keyview.ViewKeyActivity;
|
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;
|
||||||
import org.sufficientlysecure.keychain.ui.util.Notify.Style;
|
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.FabContainer;
|
||||||
import org.sufficientlysecure.keychain.util.Preferences;
|
import org.sufficientlysecure.keychain.util.Preferences;
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
|
|
||||||
public class KeyListFragment extends RecyclerFragment<KeySectionedListAdapter>
|
public class KeyListFragment extends RecyclerFragment<FlexibleAdapter<FlexibleKeyItem>>
|
||||||
implements SearchView.OnQueryTextListener,
|
implements SearchView.OnQueryTextListener, OnItemClickListener, OnItemLongClickListener, FabContainer {
|
||||||
LoaderManager.LoaderCallbacks<Cursor>, FabContainer {
|
|
||||||
|
|
||||||
static final int REQUEST_ACTION = 1;
|
static final int REQUEST_ACTION = 1;
|
||||||
private static final int REQUEST_DELETE = 2;
|
private static final int REQUEST_DELETE = 2;
|
||||||
private static final int REQUEST_VIEW_KEY = 3;
|
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 ActionMode mActionMode = null;
|
||||||
|
|
||||||
private Button vSearchButton;
|
private Button vSearchButton;
|
||||||
private ViewAnimator vSearchContainer;
|
private ViewAnimator vSearchContainer;
|
||||||
private String mQuery;
|
|
||||||
|
|
||||||
private FloatingActionsMenu mFab;
|
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
|
@Override
|
||||||
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -107,28 +112,17 @@ public class KeyListFragment extends RecyclerFragment<KeySectionedListAdapter>
|
||||||
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
|
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
|
||||||
switch (item.getItemId()) {
|
switch (item.getItemId()) {
|
||||||
case R.id.menu_key_list_multi_encrypt: {
|
case R.id.menu_key_list_multi_encrypt: {
|
||||||
long[] keyIds = getAdapter().getSelectedMasterKeyIds();
|
long[] keyIds = getSelectedMasterKeyIds();
|
||||||
Intent intent = new Intent(getActivity(), EncryptFilesActivity.class);
|
multiSelectEncrypt(keyIds);
|
||||||
intent.setAction(EncryptFilesActivity.ACTION_ENCRYPT_DATA);
|
|
||||||
intent.putExtra(EncryptFilesActivity.EXTRA_ENCRYPTION_KEY_IDS, keyIds);
|
|
||||||
|
|
||||||
startActivityForResult(intent, REQUEST_ACTION);
|
|
||||||
mode.finish();
|
mode.finish();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case R.id.menu_key_list_multi_delete: {
|
case R.id.menu_key_list_multi_delete: {
|
||||||
long[] keyIds = getAdapter().getSelectedMasterKeyIds();
|
long[] keyIds = getSelectedMasterKeyIds();
|
||||||
boolean hasSecret = getAdapter().isAnySecretKeySelected();
|
boolean hasSecret = isAnySecretKeySelected();
|
||||||
Intent intent = new Intent(getActivity(), DeleteKeyDialogActivity.class);
|
multiSelectDelete(keyIds, hasSecret);
|
||||||
intent.putExtra(DeleteKeyDialogActivity.EXTRA_DELETE_MASTER_KEY_IDS, keyIds);
|
mode.finish();
|
||||||
intent.putExtra(DeleteKeyDialogActivity.EXTRA_HAS_SECRET, hasSecret);
|
|
||||||
if (hasSecret) {
|
|
||||||
intent.putExtra(DeleteKeyDialogActivity.EXTRA_KEYSERVER,
|
|
||||||
Preferences.getPreferences(getActivity()).getPreferredKeyserver());
|
|
||||||
}
|
|
||||||
|
|
||||||
startActivityForResult(intent, REQUEST_DELETE);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -139,54 +133,61 @@ public class KeyListFragment extends RecyclerFragment<KeySectionedListAdapter>
|
||||||
public void onDestroyActionMode(ActionMode mode) {
|
public void onDestroyActionMode(ActionMode mode) {
|
||||||
mActionMode = null;
|
mActionMode = null;
|
||||||
if (getAdapter() != null) {
|
if (getAdapter() != null) {
|
||||||
getAdapter().finishSelection();
|
getAdapter().clearSelection();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
private FastScroller fastScroller;
|
||||||
|
|
||||||
private final KeySectionedListAdapter.KeyListListener mKeyListener
|
private void multiSelectDelete(long[] keyIds, boolean hasSecret) {
|
||||||
= new KeySectionedListAdapter.KeyListListener() {
|
Intent intent = new Intent(getActivity(), DeleteKeyDialogActivity.class);
|
||||||
@Override
|
intent.putExtra(DeleteKeyDialogActivity.EXTRA_DELETE_MASTER_KEY_IDS, keyIds);
|
||||||
public void onKeyDummyItemClicked() {
|
intent.putExtra(DeleteKeyDialogActivity.EXTRA_HAS_SECRET, hasSecret);
|
||||||
createKey();
|
if (hasSecret) {
|
||||||
|
intent.putExtra(DeleteKeyDialogActivity.EXTRA_KEYSERVER,
|
||||||
|
Preferences.getPreferences(getActivity()).getPreferredKeyserver());
|
||||||
}
|
}
|
||||||
|
startActivityForResult(intent, REQUEST_DELETE);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
private void multiSelectEncrypt(long[] keyIds) {
|
||||||
public void onKeyItemClicked(long masterKeyId) {
|
Intent intent = new Intent(getActivity(), EncryptFilesActivity.class);
|
||||||
Intent viewIntent = new Intent(getActivity(), ViewKeyActivity.class);
|
intent.setAction(EncryptFilesActivity.ACTION_ENCRYPT_DATA);
|
||||||
viewIntent.setData(KeyRings.buildGenericKeyRingUri(masterKeyId));
|
intent.putExtra(EncryptFilesActivity.EXTRA_ENCRYPTION_KEY_IDS, keyIds);
|
||||||
startActivityForResult(viewIntent, REQUEST_VIEW_KEY);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
startActivityForResult(intent, REQUEST_ACTION);
|
||||||
public void onSlingerButtonClicked(long masterKeyId) {
|
}
|
||||||
Intent safeSlingerIntent = new Intent(getActivity(), SafeSlingerActivity.class);
|
|
||||||
safeSlingerIntent.putExtra(SafeSlingerActivity.EXTRA_MASTER_KEY_ID, masterKeyId);
|
|
||||||
startActivityForResult(safeSlingerIntent, REQUEST_ACTION);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
private long[] getSelectedMasterKeyIds() {
|
||||||
public void onSelectionStateChanged(int selectedCount) {
|
FlexibleAdapter<FlexibleKeyItem> adapter = getAdapter();
|
||||||
if (selectedCount < 1) {
|
List<Integer> selectedPositions = adapter.getSelectedPositions();
|
||||||
if (mActionMode != null) {
|
long[] keyIds = new long[selectedPositions.size()];
|
||||||
mActionMode.finish();
|
for (int i = 0; i < selectedPositions.size(); i++) {
|
||||||
}
|
FlexibleKeyItem selectedItem = adapter.getItem(selectedPositions.get(i));
|
||||||
} else {
|
if (selectedItem != null) {
|
||||||
if (mActionMode == null) {
|
keyIds[i] = selectedItem.keyInfo.master_key_id();
|
||||||
mActionMode = getActivity().startActionMode(mActionCallback);
|
|
||||||
}
|
|
||||||
|
|
||||||
String keysSelected = getResources().getQuantityString(
|
|
||||||
R.plurals.key_list_selected_keys, selectedCount, selectedCount);
|
|
||||||
mActionMode.setTitle(keysSelected);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
return keyIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isAnySecretKeySelected() {
|
||||||
|
FlexibleAdapter<FlexibleKeyItem> 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
|
@Override
|
||||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||||
View view = inflater.inflate(R.layout.key_list_fragment, container, false);
|
View view = inflater.inflate(R.layout.key_list_fragment, container, false);
|
||||||
|
@ -210,6 +211,11 @@ public class KeyListFragment extends RecyclerFragment<KeySectionedListAdapter>
|
||||||
importFile();
|
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;
|
return view;
|
||||||
}
|
}
|
||||||
|
@ -222,7 +228,10 @@ public class KeyListFragment extends RecyclerFragment<KeySectionedListAdapter>
|
||||||
super.onActivityCreated(savedInstanceState);
|
super.onActivityCreated(savedInstanceState);
|
||||||
|
|
||||||
// show app name instead of "keys" from nav drawer
|
// 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);
|
activity.setTitle(R.string.app_name);
|
||||||
|
|
||||||
// We have a menu item to show in action bar.
|
// We have a menu item to show in action bar.
|
||||||
|
@ -231,23 +240,64 @@ public class KeyListFragment extends RecyclerFragment<KeySectionedListAdapter>
|
||||||
// Start out with a progress indicator.
|
// Start out with a progress indicator.
|
||||||
hideList(false);
|
hideList(false);
|
||||||
|
|
||||||
// click on search button (in empty view) starts query for search string
|
setLayoutManager(new LinearLayoutManager(activity));
|
||||||
vSearchContainer = activity.findViewById(R.id.search_container);
|
|
||||||
vSearchButton = activity.findViewById(R.id.search_button);
|
|
||||||
vSearchButton.setOnClickListener(v -> startSearchForQuery());
|
|
||||||
|
|
||||||
KeySectionedListAdapter adapter = new KeySectionedListAdapter(getContext(), null);
|
KeyListViewModel keyListViewModel = ViewModelProviders.of(this).get(KeyListViewModel.class);
|
||||||
adapter.setKeyListener(mKeyListener);
|
keyListViewModel.getLiveData(getContext()).observe(this, this::onLoadKeyItems);
|
||||||
|
}
|
||||||
|
|
||||||
setAdapter(adapter);
|
public static class KeyListViewModel extends ViewModel {
|
||||||
setLayoutManager(new LayoutManager(getActivity()));
|
LiveData<List<FlexibleKeyItem>> liveData;
|
||||||
|
|
||||||
FastScroller fastScroller = getActivity().findViewById(R.id.fastscroll);
|
LiveData<List<FlexibleKeyItem>> getLiveData(Context context) {
|
||||||
fastScroller.setRecyclerView(getRecyclerView());
|
if (liveData == null) {
|
||||||
|
liveData = new KeyListLiveData(context);
|
||||||
|
}
|
||||||
|
return liveData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Prepare the loader. Either re-connect with an existing one,
|
public static class KeyListLiveData extends AsyncTaskLiveData<List<FlexibleKeyItem>> {
|
||||||
// or start a new one.
|
private final KeyRingDao keyRingDao;
|
||||||
getLoaderManager().initLoader(0, null, this);
|
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<FlexibleKeyItem> asyncLoadData() {
|
||||||
|
List<UnifiedKeyInfo> unifiedKeyInfo = keyRingDao.getUnifiedKeyInfo();
|
||||||
|
return flexibleKeyItemFactory.mapUnifiedKeyInfoToFlexibleKeyItems(unifiedKeyInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onLoadKeyItems(List<FlexibleKeyItem> flexibleKeyItems) {
|
||||||
|
FlexibleAdapter<FlexibleKeyItem> 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
|
@Override
|
||||||
|
@ -305,53 +355,11 @@ public class KeyListFragment extends RecyclerFragment<KeySectionedListAdapter>
|
||||||
}
|
}
|
||||||
|
|
||||||
Intent searchIntent = new Intent(activity, ImportKeysActivity.class);
|
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);
|
searchIntent.setAction(ImportKeysActivity.ACTION_IMPORT_KEY_FROM_KEYSERVER);
|
||||||
startActivity(searchIntent);
|
startActivity(searchIntent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public Loader<Cursor> 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<Cursor> 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<Cursor> 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
|
@Override
|
||||||
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
|
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
|
||||||
inflater.inflate(R.menu.key_list, menu);
|
inflater.inflate(R.menu.key_list, menu);
|
||||||
|
@ -375,19 +383,13 @@ public class KeyListFragment extends RecyclerFragment<KeySectionedListAdapter>
|
||||||
MenuItemCompat.setOnActionExpandListener(searchItem, new MenuItemCompat.OnActionExpandListener() {
|
MenuItemCompat.setOnActionExpandListener(searchItem, new MenuItemCompat.OnActionExpandListener() {
|
||||||
@Override
|
@Override
|
||||||
public boolean onMenuItemActionExpand(MenuItem item) {
|
public boolean onMenuItemActionExpand(MenuItem item) {
|
||||||
|
|
||||||
// disable swipe-to-refresh
|
|
||||||
// mSwipeRefreshLayout.setIsLocked(true);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onMenuItemActionCollapse(MenuItem item) {
|
public boolean onMenuItemActionCollapse(MenuItem item) {
|
||||||
mQuery = null;
|
getAdapter().setFilter(null);
|
||||||
getLoaderManager().restartLoader(0, null, KeyListFragment.this);
|
getAdapter().filterItems();
|
||||||
|
|
||||||
// enable swipe-to-refresh
|
|
||||||
// mSwipeRefreshLayout.setIsLocked(false);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -395,6 +397,54 @@ public class KeyListFragment extends RecyclerFragment<KeySectionedListAdapter>
|
||||||
super.onCreateOptionsMenu(menu, inflater);
|
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
|
@Override
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
public boolean onOptionsItemSelected(MenuItem item) {
|
||||||
switch (item.getItemId()) {
|
switch (item.getItemId()) {
|
||||||
|
@ -410,7 +460,7 @@ public class KeyListFragment extends RecyclerFragment<KeySectionedListAdapter>
|
||||||
try {
|
try {
|
||||||
KeychainDatabase.debugBackup(getActivity(), true);
|
KeychainDatabase.debugBackup(getActivity(), true);
|
||||||
Notify.create(getActivity(), "Restored debug_backup.db", Notify.Style.OK).show();
|
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) {
|
} catch (IOException e) {
|
||||||
Timber.e(e, "IO Error");
|
Timber.e(e, "IO Error");
|
||||||
Notify.create(getActivity(), "IO Error " + e.getMessage(), Notify.Style.ERROR).show();
|
Notify.create(getActivity(), "IO Error " + e.getMessage(), Notify.Style.ERROR).show();
|
||||||
|
@ -456,20 +506,12 @@ public class KeyListFragment extends RecyclerFragment<KeySectionedListAdapter>
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onQueryTextChange(String s) {
|
public boolean onQueryTextChange(String searchText) {
|
||||||
Timber.d("onQueryTextChange s: %s", s);
|
getAdapter().setFilter(searchText);
|
||||||
// Called when the action bar search text has changed. Update the
|
getAdapter().filterItems(300);
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (s.length() > 2) {
|
if (searchText.length() > 2) {
|
||||||
vSearchButton.setText(getString(R.string.btn_search_for_query, mQuery));
|
vSearchButton.setText(getString(R.string.btn_search_for_query, searchText));
|
||||||
vSearchContainer.setDisplayedChild(1);
|
vSearchContainer.setDisplayedChild(1);
|
||||||
vSearchContainer.setVisibility(View.VISIBLE);
|
vSearchContainer.setVisibility(View.VISIBLE);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -17,6 +17,10 @@
|
||||||
|
|
||||||
package org.sufficientlysecure.keychain.ui;
|
package org.sufficientlysecure.keychain.ui;
|
||||||
|
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
@ -26,19 +30,15 @@ import android.view.MenuInflater;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
|
|
||||||
import com.tonicartos.superslim.LayoutManager;
|
import com.tonicartos.superslim.LayoutManager;
|
||||||
|
|
||||||
import org.sufficientlysecure.keychain.R;
|
import org.sufficientlysecure.keychain.R;
|
||||||
import org.sufficientlysecure.keychain.operations.results.OperationResult;
|
import org.sufficientlysecure.keychain.operations.results.OperationResult;
|
||||||
import org.sufficientlysecure.keychain.operations.results.OperationResult.SubLogEntryParcel;
|
import org.sufficientlysecure.keychain.operations.results.OperationResult.SubLogEntryParcel;
|
||||||
import org.sufficientlysecure.keychain.provider.TemporaryFileProvider;
|
import org.sufficientlysecure.keychain.provider.TemporaryFileProvider;
|
||||||
import org.sufficientlysecure.keychain.ui.adapter.NestedLogAdapter;
|
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.dialog.ShareLogDialogFragment;
|
||||||
import org.sufficientlysecure.keychain.ui.util.Notify;
|
import org.sufficientlysecure.keychain.ui.util.Notify;
|
||||||
import org.sufficientlysecure.keychain.ui.util.Notify.Style;
|
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<NestedLogAdapter>
|
public class LogDisplayFragment extends RecyclerFragment<NestedLogAdapter>
|
||||||
|
|
|
@ -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<FlexibleHeaderViewHolder> {
|
||||||
|
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<IFlexible> adapter) {
|
||||||
|
return new FlexibleHeaderViewHolder(view, adapter);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void bindViewHolder(FlexibleAdapter<IFlexible> adapter, FlexibleHeaderViewHolder holder, int position,
|
||||||
|
List<Object> 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<FlexibleKeyItemViewHolder, FlexibleKeyHeader>
|
||||||
|
implements IFilterable<String> {
|
||||||
|
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<IFlexible> adapter) {
|
||||||
|
return new FlexibleKeyItemViewHolder(view, adapter);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void bindViewHolder(
|
||||||
|
FlexibleAdapter<IFlexible> adapter, FlexibleKeyItemViewHolder holder, int position, List<Object> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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<String, FlexibleKeyHeader> initialsHeaderMap = new HashMap<>();
|
||||||
|
private FlexibleKeyHeader myKeysHeader;
|
||||||
|
|
||||||
|
public FlexibleKeyItemFactory(Resources resources) {
|
||||||
|
String myKeysHeaderText = resources.getString(R.string.my_keys);
|
||||||
|
myKeysHeader = new FlexibleKeyHeader(myKeysHeaderText);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<FlexibleKeyItem> mapUnifiedKeyInfoToFlexibleKeyItems(List<UnifiedKeyInfo> unifiedKeyInfos) {
|
||||||
|
List<FlexibleKeyItem> 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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,15 +17,20 @@
|
||||||
|
|
||||||
package org.sufficientlysecure.keychain.ui.adapter;
|
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.content.Context;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.graphics.PorterDuff;
|
|
||||||
import android.support.v4.widget.CursorAdapter;
|
import android.support.v4.widget.CursorAdapter;
|
||||||
import android.text.format.DateUtils;
|
import android.text.format.DateUtils;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.ImageButton;
|
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.TextView;
|
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;
|
||||||
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils.State;
|
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 {
|
public class KeyAdapter extends CursorAdapter {
|
||||||
|
|
||||||
protected String mQuery;
|
protected String mQuery;
|
||||||
|
@ -105,8 +104,6 @@ public class KeyAdapter extends CursorAdapter {
|
||||||
public TextView mMainUserIdRest;
|
public TextView mMainUserIdRest;
|
||||||
public TextView mCreationDate;
|
public TextView mCreationDate;
|
||||||
public ImageView mStatus;
|
public ImageView mStatus;
|
||||||
public View mSlinger;
|
|
||||||
public ImageButton mSlingerButton;
|
|
||||||
|
|
||||||
public KeyItem mDisplayedItem;
|
public KeyItem mDisplayedItem;
|
||||||
|
|
||||||
|
@ -116,8 +113,6 @@ public class KeyAdapter extends CursorAdapter {
|
||||||
mMainUserId = view.findViewById(R.id.key_list_item_name);
|
mMainUserId = view.findViewById(R.id.key_list_item_name);
|
||||||
mMainUserIdRest = view.findViewById(R.id.key_list_item_email);
|
mMainUserIdRest = view.findViewById(R.id.key_list_item_email);
|
||||||
mStatus = view.findViewById(R.id.key_list_item_status_icon);
|
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);
|
mCreationDate = view.findViewById(R.id.key_list_item_creation);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -154,28 +149,17 @@ public class KeyAdapter extends CursorAdapter {
|
||||||
KeyFormattingUtils
|
KeyFormattingUtils
|
||||||
.setStatusImage(context, mStatus, null, State.REVOKED, R.color.key_flag_gray);
|
.setStatusImage(context, mStatus, null, State.REVOKED, R.color.key_flag_gray);
|
||||||
mStatus.setVisibility(View.VISIBLE);
|
mStatus.setVisibility(View.VISIBLE);
|
||||||
mSlinger.setVisibility(View.GONE);
|
|
||||||
textColor = context.getResources().getColor(R.color.key_flag_gray);
|
textColor = context.getResources().getColor(R.color.key_flag_gray);
|
||||||
} else if (item.mIsExpired) {
|
} else if (item.mIsExpired) {
|
||||||
KeyFormattingUtils.setStatusImage(context, mStatus, null, State.EXPIRED, R.color.key_flag_gray);
|
KeyFormattingUtils.setStatusImage(context, mStatus, null, State.EXPIRED, R.color.key_flag_gray);
|
||||||
mStatus.setVisibility(View.VISIBLE);
|
mStatus.setVisibility(View.VISIBLE);
|
||||||
mSlinger.setVisibility(View.GONE);
|
|
||||||
textColor = context.getResources().getColor(R.color.key_flag_gray);
|
textColor = context.getResources().getColor(R.color.key_flag_gray);
|
||||||
} else if (!item.mIsSecure) {
|
} else if (!item.mIsSecure) {
|
||||||
KeyFormattingUtils.setStatusImage(context, mStatus, null, State.INSECURE, R.color.key_flag_gray);
|
KeyFormattingUtils.setStatusImage(context, mStatus, null, State.INSECURE, R.color.key_flag_gray);
|
||||||
mStatus.setVisibility(View.VISIBLE);
|
mStatus.setVisibility(View.VISIBLE);
|
||||||
mSlinger.setVisibility(View.GONE);
|
|
||||||
textColor = context.getResources().getColor(R.color.key_flag_gray);
|
textColor = context.getResources().getColor(R.color.key_flag_gray);
|
||||||
} else if (item.mIsSecret) {
|
} else if (item.mIsSecret) {
|
||||||
mStatus.setVisibility(View.GONE);
|
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);
|
textColor = FormattingUtils.getColorFromAttr(context, R.attr.colorText);
|
||||||
} else {
|
} else {
|
||||||
// this is a public key - show if it's verified
|
// 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);
|
KeyFormattingUtils.setStatusImage(context, mStatus, State.UNVERIFIED);
|
||||||
mStatus.setVisibility(View.VISIBLE);
|
mStatus.setVisibility(View.VISIBLE);
|
||||||
}
|
}
|
||||||
mSlinger.setVisibility(View.GONE);
|
|
||||||
textColor = FormattingUtils.getColorFromAttr(context, R.attr.colorText);
|
textColor = FormattingUtils.getColorFromAttr(context, R.attr.colorText);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
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<KeySectionedListAdapter.KeyListCursor, Character,
|
|
||||||
SectionCursorAdapter.ViewHolder, KeySectionedListAdapter.KeyHeaderViewHolder> 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<Integer> 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<String> 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<String> 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<String> 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<String, Drawable> 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
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 <T> section type.
|
|
||||||
* @param <VH> the view holder extending {@code BaseViewHolder<Cursor>} that is bound to the cursor data.
|
|
||||||
* @param <SH> the view holder extending {@code BaseViewHolder<<T>>} that is bound to the section data.
|
|
||||||
*/
|
|
||||||
public abstract class SectionCursorAdapter<C extends SimpleCursor, T, VH extends SectionCursorAdapter.ViewHolder,
|
|
||||||
SH extends SectionCursorAdapter.ViewHolder> extends CursorAdapter<C, RecyclerView.ViewHolder> {
|
|
||||||
|
|
||||||
public static final String TAG = "SectionCursorAdapter";
|
|
||||||
|
|
||||||
private static final short VIEW_TYPE_ITEM = 0x1;
|
|
||||||
private static final short VIEW_TYPE_SECTION = 0x2;
|
|
||||||
|
|
||||||
private SparseArrayCompat<T> mSectionMap = new SparseArrayCompat<>();
|
|
||||||
private Comparator<T> mSectionComparator;
|
|
||||||
|
|
||||||
public SectionCursorAdapter(Context context, C cursor, int flags) {
|
|
||||||
this(context, cursor, flags, new Comparator<T>() {
|
|
||||||
@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<T> 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<T> 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<T> {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
<item>
|
<item>
|
||||||
<selector>
|
<selector>
|
||||||
<item android:state_selected="true" android:drawable="@color/pressed_gray"/>
|
<item android:state_selected="true" android:drawable="@color/pressed_gray"/>
|
||||||
|
<item android:state_activated="true" android:drawable="@color/selected_gray"/>
|
||||||
</selector>
|
</selector>
|
||||||
</item>
|
</item>
|
||||||
</ripple>
|
</ripple>
|
|
@ -34,7 +34,7 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<RelativeLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
@ -43,18 +43,7 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:clipToPadding="false"
|
android:clipToPadding="false"
|
||||||
android:paddingBottom="72dp"
|
android:paddingBottom="72dp" />
|
||||||
android:paddingLeft="16dp"
|
|
||||||
android:paddingRight="32dp"
|
|
||||||
android:paddingStart="16dp" />
|
|
||||||
|
|
||||||
<com.futuremind.recyclerviewfastscroll.FastScroller
|
|
||||||
android:id="@+id/fastscroll"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:layout_alignParentRight="true"
|
|
||||||
android:orientation="vertical"
|
|
||||||
app:fastscroll__bubbleColor="@color/primary" />
|
|
||||||
|
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
|
|
||||||
|
@ -149,6 +138,21 @@
|
||||||
</com.getbase.floatingactionbutton.FloatingActionsMenu>
|
</com.getbase.floatingactionbutton.FloatingActionsMenu>
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
|
|
||||||
|
<eu.davidea.fastscroller.FastScroller
|
||||||
|
android:id="@+id/fast_scroller"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_centerHorizontal="true"
|
||||||
|
android:layout_alignParentEnd="true"
|
||||||
|
android:layout_alignParentRight="true"
|
||||||
|
app:fastScrollerAutoHideEnabled="true"
|
||||||
|
app:fastScrollerAutoHideDelayInMillis="1000"
|
||||||
|
app:fastScrollerBubblePosition="adjacent"
|
||||||
|
app:fastScrollerBubbleEnabled="true"
|
||||||
|
app:fastScrollerHandleAlwaysVisible="false"
|
||||||
|
app:fastScrollerIgnoreTouchesOutsideHandle="false"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,16 +1,12 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:super="http://schemas.android.com/apk/lib-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:background="?android:colorBackground"
|
android:background="?android:colorBackground"
|
||||||
|
android:paddingRight="12dp"
|
||||||
super:slm_headerDisplay="sticky|inline"
|
android:paddingLeft="12dp">
|
||||||
super:slm_section_sectionManager="linear"
|
|
||||||
tools:ignore="ResAuto">
|
|
||||||
|
|
||||||
<TextView style="@style/SectionHeader"
|
<TextView style="@style/SectionHeader"
|
||||||
android:id="@android:id/text1"
|
android:id="@android:id/text1"
|
||||||
|
|
|
@ -11,6 +11,8 @@
|
||||||
android:descendantFocusability="blocksDescendants"
|
android:descendantFocusability="blocksDescendants"
|
||||||
android:background="@drawable/list_item_ripple"
|
android:background="@drawable/list_item_ripple"
|
||||||
android:focusable="false"
|
android:focusable="false"
|
||||||
|
android:paddingLeft="12dp"
|
||||||
|
android:paddingRight="12dp"
|
||||||
tools:layout_marginTop="30dp">
|
tools:layout_marginTop="30dp">
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
|
@ -61,34 +63,6 @@
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/key_list_item_slinger_view"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="?android:attr/listPreferredItemHeight"
|
|
||||||
android:layout_gravity="center_vertical"
|
|
||||||
android:orientation="horizontal"
|
|
||||||
tools:visibility="gone">
|
|
||||||
|
|
||||||
<View
|
|
||||||
android:layout_width="1dip"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:gravity="right"
|
|
||||||
android:layout_marginBottom="8dp"
|
|
||||||
android:layout_marginTop="8dp"
|
|
||||||
android:background="?android:attr/listDivider" />
|
|
||||||
|
|
||||||
<ImageButton
|
|
||||||
android:id="@+id/key_list_item_slinger_button"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:layout_gravity="center"
|
|
||||||
android:src="@drawable/ic_repeat_grey_24dp"
|
|
||||||
android:padding="16dp"
|
|
||||||
android:background="?android:selectableItemBackground"
|
|
||||||
android:contentDescription="@string/cd_exchange_keys"/>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/key_list_item_status_icon"
|
android:id="@+id/key_list_item_status_icon"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
|
|
@ -8,8 +8,8 @@ CREATE TABLE IF NOT EXISTS certs(
|
||||||
creation INTEGER NOT NULL,
|
creation INTEGER NOT NULL,
|
||||||
data BLOB NOT NULL,
|
data BLOB NOT NULL,
|
||||||
PRIMARY KEY(master_key_id, rank, key_id_certifier),
|
PRIMARY KEY(master_key_id, rank, key_id_certifier),
|
||||||
FOREIGN KEY(master_key_id) REFERENCES keyrings_public(master_key_id) ON DELETE CASCADE
|
FOREIGN KEY(master_key_id) REFERENCES keyrings_public(master_key_id) ON DELETE CASCADE,
|
||||||
-- FOREIGN KEY(master_key_id, rank) REFERENCES user_packets(master_key_id, rank) ON DELETE CASCADE
|
FOREIGN KEY(master_key_id, rank) REFERENCES user_packets(master_key_id, rank) ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
|
|
||||||
selectVerifyingCertDetails:
|
selectVerifyingCertDetails:
|
||||||
|
|
|
@ -6,4 +6,4 @@ CREATE TABLE IF NOT EXISTS keyrings_public (
|
||||||
selectByMasterKeyId:
|
selectByMasterKeyId:
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM keyrings_public
|
FROM keyrings_public
|
||||||
WHERE master_key_id = ?;
|
WHERE master_key_id = ?;
|
||||||
|
|
|
@ -1,21 +1,37 @@
|
||||||
|
import java.lang.Boolean;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS keys (
|
CREATE TABLE IF NOT EXISTS keys (
|
||||||
master_key_id INTEGER,
|
master_key_id INTEGER NOT NULL,
|
||||||
rank INTEGER,
|
rank INTEGER NOT NULL,
|
||||||
key_id INTEGER,
|
key_id INTEGER NOT NULL,
|
||||||
key_size INTEGER,
|
key_size INTEGER,
|
||||||
key_curve_oid TEXT,
|
key_curve_oid TEXT,
|
||||||
algorithm INTEGER,
|
algorithm INTEGER NOT NULL,
|
||||||
fingerprint BLOB,
|
fingerprint BLOB NOT NULL,
|
||||||
can_certify INTEGER,
|
can_certify INTEGER AS Boolean NOT NULL,
|
||||||
can_sign INTEGER,
|
can_sign INTEGER AS Boolean NOT NULL,
|
||||||
can_encrypt INTEGER,
|
can_encrypt INTEGER AS Boolean NOT NULL,
|
||||||
can_authenticate INTEGER,
|
can_authenticate INTEGER AS Boolean NOT NULL,
|
||||||
is_revoked INTEGER,
|
is_revoked INTEGER AS Boolean NOT NULL,
|
||||||
has_secret INTEGER,
|
has_secret INTEGER AS Boolean NOT NULL,
|
||||||
is_secure INTEGER,
|
is_secure INTEGER AS Boolean NOT NULL,
|
||||||
creation INTEGER,
|
creation INTEGER NOT NULL,
|
||||||
expiry INTEGER,
|
expiry INTEGER,
|
||||||
PRIMARY KEY(master_key_id, rank),
|
PRIMARY KEY(master_key_id, rank),
|
||||||
FOREIGN KEY(master_key_id) REFERENCES
|
FOREIGN KEY(master_key_id) REFERENCES
|
||||||
keyrings_public(master_key_id) ON DELETE CASCADE
|
keyrings_public(master_key_id) ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
|
|
||||||
|
selectAllUnifiedKeyInfo:
|
||||||
|
SELECT keys.master_key_id, MIN(user_packets.rank), user_packets.name, user_packets.email, user_packets.comment, keys.creation, keys.expiry, keys.is_revoked, keys.is_secure, certs.verified,
|
||||||
|
(EXISTS (SELECT * FROM user_packets AS dups WHERE dups.master_key_id != keys.master_key_id AND dups.rank = 0 AND dups.name = user_packets.name COLLATE NOCASE AND dups.email = user_packets.email COLLATE NOCASE )) AS has_duplicate_int,
|
||||||
|
(EXISTS (SELECT * FROM keys AS k WHERE k.has_secret != 0 AND k.master_key_id = keys.master_key_id )) AS has_any_secret_int,
|
||||||
|
GROUP_CONCAT(DISTINCT aTI.package_name) AS autocrypt_package_names_csv,
|
||||||
|
GROUP_CONCAT(user_packets.user_id, '|||') AS user_id_list
|
||||||
|
FROM keys
|
||||||
|
INNER JOIN user_packets ON ( keys.master_key_id = user_packets.master_key_id AND user_packets.type IS NULL )
|
||||||
|
LEFT JOIN certs ON ( keys.master_key_id = certs.master_key_id AND certs.verified = 1 )
|
||||||
|
LEFT JOIN autocrypt_peers AS aTI ON ( aTI.master_key_id = keys.master_key_id )
|
||||||
|
WHERE keys.rank = 0
|
||||||
|
GROUP BY keys.master_key_id
|
||||||
|
ORDER BY has_secret DESC, user_packets.name COLLATE NOCASE ASC;
|
|
@ -0,0 +1,15 @@
|
||||||
|
CREATE TABLE IF NOT EXISTS user_packets(
|
||||||
|
master_key_id INTEGER,
|
||||||
|
type INTEGER,
|
||||||
|
user_id TEXT,
|
||||||
|
name TEXT,
|
||||||
|
email TEXT,
|
||||||
|
comment TEXT,
|
||||||
|
attribute_data BLOB,
|
||||||
|
is_primary INTEGER,
|
||||||
|
is_revoked INTEGER,
|
||||||
|
rank INTEGER,
|
||||||
|
PRIMARY KEY(master_key_id, rank),
|
||||||
|
FOREIGN KEY(master_key_id) REFERENCES
|
||||||
|
keyrings_public(master_key_id) ON DELETE CASCADE
|
||||||
|
);
|
Loading…
Reference in a new issue