use FlexibleAdapter with LiveData in KeyListFragment

This commit is contained in:
Vincent Breitmoser 2018-06-20 00:45:34 +02:00
parent 72f3ed89a6
commit f87209d242
23 changed files with 746 additions and 1778 deletions

View file

@ -36,7 +36,9 @@ dependencies {
// RecyclerView
compile 'com.tonicartos:superslim:0.4.13'
compile 'com.futuremind.recyclerfastscroll:fastscroll:0.2.4'
compile 'eu.davidea:flexible-adapter:5.0.5'
compile 'eu.davidea:flexible-adapter-ui:1.0.0-b5'
compile 'eu.davidea:flexible-adapter-livedata:1.0.0-b2'
// Material Drawer
compile 'com.mikepenz:materialdrawer:5.6.0@aar'
@ -107,6 +109,7 @@ dependencies {
// Comment out the libs referenced as git submodules!
dependencyVerification {
verify = [
'eu.davidea:flexible-adapter-ui:7ed5327d15c823e5fcf7d6e1017d8a47d079d1adc7141858f3cb427517ef35cd',
'com.android.support:design:7225973f7ee03765008a9c2f17a40b154c6885169fef022276e811c926a2202c',
'com.journeyapps:zxing-android-embedded:2422d83c2c09a7b645f516c8458ececba6a7da47b94e40778d876facf495c660',
'org.sufficientlysecure:donations:2be4183afa5e35263e37346344cfea48681f3c987e6832dd4acde227c13ccad6',
@ -115,6 +118,7 @@ dependencyVerification {
'com.mikepenz:materialize:942ccf5e2aa1a46803aa884e8dc7bbaf2a9e8e9996a0cf92e3fe2f44a8592ba4',
'com.android.support:appcompat-v7:0c7808fbbc5838d831e32e3c0a6f84e1f2c981deb8f11e010650f2b57923a335',
'com.nispok:snackbar:46b5eb9d630d329e13c2ce00ee9fb115ffb66c23c72cff32ee97eedd76824c6f',
'eu.davidea:flexible-adapter:560e940e8cf0f4ed8f632f5f89527deeda7a61cce5f02f42cc0983f7c0d2de5f',
'com.android.support:recyclerview-v7:d735e4727878e99ef3980c10d15dc3468462fd509d4fb60cb8bd20b0f735085c',
'com.android.support:cardview-v7:8ed955dd037d82a7b4bbcaedb4f896523c3e4c1bf3ca698ce807c350767a2886',
'org.sufficientlysecure:html-textview:ed740adf05cae2373999c7a3047c803183d9807b2cf66162902090d7c112a832',
@ -145,6 +149,7 @@ dependencyVerification {
'org.apache.james:apache-mime4j-core:561987f604911e1870b2b4eabf0b0658d666c66cb1e65fba3e9e4bffe63acab9',
'com.splitwise:tokenautocomplete:f921f83ee26b5265f719b312c30452ef8e219557826c5ce5bf02e29647967939',
'com.cocosw:bottomsheet:85bd91fd837b02ebd7a888501cb26035c7cd985a6aa87303fca249da8231a2c3',
'eu.davidea:flexible-adapter-livedata:c8718b46ff4fbf290ea18f0c5bfe8326badeadf5fd95899a1404c561a24f48a1',
'com.mikepenz:materialdrawer:8bba1428dcef5ad7c2decf49c612ad980b38e2f1031cbd66c152a8a104793929',
'com.mikepenz:iconics-core:478d7e245098f7c28b5b20a0e6b1e5cb108ef3eaf595af7190bc60f91063aa3d',
'com.mikepenz:google-material-typeface:f27c629ba5d2a90ecfbd7f221ff98cd363e1ee6be06b099b82bae490766e14a5',

View file

@ -28,12 +28,10 @@ import android.widget.ViewAnimator;
import com.nispok.snackbar.Snackbar;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.ui.adapter.KeyAdapter.KeyItem;
import org.sufficientlysecure.keychain.ui.adapter.KeySectionedListAdapter;
import org.sufficientlysecure.keychain.ui.widget.EncryptKeyCompletionView;
import static android.support.test.espresso.matcher.ViewMatchers.hasDescendant;
@ -90,15 +88,14 @@ public abstract class CustomMatchers {
}
public static Matcher<RecyclerView.ViewHolder> withKeyHolderId(final long keyId) {
return new BoundedMatcher<RecyclerView.ViewHolder, KeySectionedListAdapter.KeyItemViewHolder>
(KeySectionedListAdapter.KeyItemViewHolder.class) {
return new BoundedMatcher<RecyclerView.ViewHolder, RecyclerView.ViewHolder>(RecyclerView.ViewHolder.class) {
@Override
public void describeTo(Description description) {
description.appendText("with ViewHolder id: " + keyId);
}
@Override
protected boolean matchesSafely(KeySectionedListAdapter.KeyItemViewHolder item) {
protected boolean matchesSafely(View item) {
return item.getItemId() == keyId;
}
};

View file

@ -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) {
}
}
}

View file

@ -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;
}
}

View file

@ -28,8 +28,8 @@ public abstract class AutocryptPeer implements AutocryptPeersModel {
}
public boolean isGossipKeyRevoked() {
Long revokedInt = gossip_key_is_revoked_int();
return revokedInt != null && revokedInt != 0;
Boolean gossip_key_is_revoked = gossip_key_is_revoked_int();
return gossip_key_is_revoked != null && gossip_key_is_revoked;
}
public boolean isGossipKeyExpired() {
@ -45,8 +45,8 @@ public abstract class AutocryptPeer implements AutocryptPeersModel {
}
public boolean isKeyRevoked() {
Long revokedInt = key_is_revoked_int();
return revokedInt != null && revokedInt != 0;
Boolean revoked = key_is_revoked_int();
return revoked != null && revoked;
}
public boolean isKeyExpired() {

View file

@ -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;
}
}
}

View file

@ -34,11 +34,15 @@ import android.database.SQLException;
import android.database.sqlite.SQLiteException;
import android.provider.BaseColumns;
import org.sufficientlysecure.keychain.ApiAppsModel;
import org.sufficientlysecure.keychain.AutocryptPeersModel;
import org.sufficientlysecure.keychain.CertsModel;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.KeyMetadataModel;
import org.sufficientlysecure.keychain.KeyRingsPublicModel;
import org.sufficientlysecure.keychain.UserPacketsModel;
import org.sufficientlysecure.keychain.model.ApiApp;
import org.sufficientlysecure.keychain.model.Certification;
import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAppsAllowedKeysColumns;
import org.sufficientlysecure.keychain.provider.KeychainContract.CertsColumns;
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRingsColumns;
@ -120,26 +124,6 @@ public class KeychainDatabase {
+ Tables.KEY_RINGS_PUBLIC + "(" + KeyRingsColumns.MASTER_KEY_ID + ") ON DELETE CASCADE"
+ ")";
private static final String CREATE_CERTS =
"CREATE TABLE IF NOT EXISTS " + Tables.CERTS + "("
+ CertsColumns.MASTER_KEY_ID + " INTEGER,"
+ CertsColumns.RANK + " INTEGER, " // rank of certified uid
+ CertsColumns.KEY_ID_CERTIFIER + " INTEGER, " // certifying key
+ CertsColumns.TYPE + " INTEGER, "
+ CertsColumns.VERIFIED + " INTEGER, "
+ CertsColumns.CREATION + " INTEGER, "
+ CertsColumns.DATA + " BLOB, "
+ "PRIMARY KEY(" + CertsColumns.MASTER_KEY_ID + ", " + CertsColumns.RANK + ", "
+ CertsColumns.KEY_ID_CERTIFIER + "), "
+ "FOREIGN KEY(" + CertsColumns.MASTER_KEY_ID + ") REFERENCES "
+ Tables.KEY_RINGS_PUBLIC + "(" + KeyRingsColumns.MASTER_KEY_ID + ") ON DELETE CASCADE,"
+ "FOREIGN KEY(" + CertsColumns.MASTER_KEY_ID + ", " + CertsColumns.RANK + ") REFERENCES "
+ Tables.USER_PACKETS + "(" + UserPacketsColumns.MASTER_KEY_ID + ", " + UserPacketsColumns.RANK + ") ON DELETE CASCADE"
+ ")";
private static final String CREATE_KEY_SIGNATURES =
"CREATE TABLE IF NOT EXISTS " + Tables.KEY_SIGNATURES + " ("
+ KeySignaturesColumns.MASTER_KEY_ID + " INTEGER NOT NULL, "
@ -212,14 +196,14 @@ public class KeychainDatabase {
db.execSQL(KeyRingsPublicModel.CREATE_TABLE);
db.execSQL(CREATE_KEYS);
db.execSQL(CREATE_USER_PACKETS);
db.execSQL(CREATE_CERTS);
db.execSQL(UserPacketsModel.CREATE_TABLE);
db.execSQL(CertsModel.CREATE_TABLE);
db.execSQL(KeyMetadataModel.CREATE_TABLE);
db.execSQL(CREATE_KEY_SIGNATURES);
db.execSQL(CREATE_API_APPS_ALLOWED_KEYS);
db.execSQL(CREATE_OVERRIDDEN_WARNINGS);
db.execSQL(AutocryptPeersModel.CREATE_TABLE);
db.execSQL(ApiApp.CREATE_TABLE);
db.execSQL(ApiAppsModel.CREATE_TABLE);
db.execSQL("CREATE INDEX keys_by_rank ON keys (" + KeysColumns.RANK + ", " + KeysColumns.MASTER_KEY_ID + ");");
db.execSQL("CREATE INDEX uids_by_rank ON user_packets (" + UserPacketsColumns.RANK + ", "

View file

@ -23,18 +23,19 @@ import java.util.List;
import android.animation.ObjectAnimator;
import android.app.Activity;
import android.arch.lifecycle.LiveData;
import android.arch.lifecycle.ViewModel;
import android.arch.lifecycle.ViewModelProviders;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.support.v4.view.MenuItemCompat;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.SearchView;
import android.text.TextUtils;
import android.view.ActionMode;
import android.view.LayoutInflater;
import android.view.Menu;
@ -46,55 +47,59 @@ import android.widget.Button;
import android.widget.ViewAnimator;
import androidx.work.WorkStatus;
import com.futuremind.recyclerviewfastscroll.FastScroller;
import com.getbase.floatingactionbutton.FloatingActionButton;
import com.getbase.floatingactionbutton.FloatingActionsMenu;
import com.tonicartos.superslim.LayoutManager;
import eu.davidea.fastscroller.FastScroller;
import eu.davidea.flexibleadapter.FlexibleAdapter;
import eu.davidea.flexibleadapter.FlexibleAdapter.OnItemClickListener;
import eu.davidea.flexibleadapter.FlexibleAdapter.OnItemLongClickListener;
import eu.davidea.flexibleadapter.SelectableAdapter.Mode;
import eu.davidea.flexibleadapter.common.FlexibleItemDecoration;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.compatibility.ClipboardReflection;
import org.sufficientlysecure.keychain.keysync.KeyserverSyncManager;
import org.sufficientlysecure.keychain.livedata.KeyRingDao;
import org.sufficientlysecure.keychain.model.Key.UnifiedKeyInfo;
import org.sufficientlysecure.keychain.operations.results.BenchmarkResult;
import org.sufficientlysecure.keychain.operations.results.OperationResult;
import org.sufficientlysecure.keychain.pgp.PgpHelper;
import org.sufficientlysecure.keychain.provider.KeychainContract;
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
import org.sufficientlysecure.keychain.provider.KeychainDatabase;
import org.sufficientlysecure.keychain.service.BenchmarkInputParcel;
import org.sufficientlysecure.keychain.ui.adapter.KeySectionedListAdapter;
import org.sufficientlysecure.keychain.ui.adapter.FlexibleKeyHeader;
import org.sufficientlysecure.keychain.ui.adapter.FlexibleKeyItem;
import org.sufficientlysecure.keychain.ui.adapter.FlexibleKeyItemFactory;
import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper;
import org.sufficientlysecure.keychain.ui.base.RecyclerFragment;
import org.sufficientlysecure.keychain.ui.keyview.ViewKeyActivity;
import org.sufficientlysecure.keychain.ui.keyview.loader.AsyncTaskLiveData;
import org.sufficientlysecure.keychain.ui.util.Notify;
import org.sufficientlysecure.keychain.ui.util.Notify.Style;
import org.sufficientlysecure.keychain.ui.util.recyclerview.DividerItemDecoration;
import org.sufficientlysecure.keychain.util.FabContainer;
import org.sufficientlysecure.keychain.util.Preferences;
import timber.log.Timber;
public class KeyListFragment extends RecyclerFragment<KeySectionedListAdapter>
implements SearchView.OnQueryTextListener,
LoaderManager.LoaderCallbacks<Cursor>, FabContainer {
public class KeyListFragment extends RecyclerFragment<FlexibleAdapter<FlexibleKeyItem>>
implements SearchView.OnQueryTextListener, OnItemClickListener, OnItemLongClickListener, FabContainer {
static final int REQUEST_ACTION = 1;
private static final int REQUEST_DELETE = 2;
private static final int REQUEST_VIEW_KEY = 3;
// saves the mode object for multiselect, needed for reset at some point
private ActionMode mActionMode = null;
private Button vSearchButton;
private ViewAnimator vSearchContainer;
private String mQuery;
private FloatingActionsMenu mFab;
// Callbacks related to listview and menu events
private final ActionMode.Callback mActionCallback
= new ActionMode.Callback() {
private final ActionMode.Callback mActionCallback = new ActionMode.Callback() {
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
getActivity().getMenuInflater().inflate(R.menu.key_list_multi, menu);
mode.getMenuInflater().inflate(R.menu.key_list_multi, menu);
return true;
}
@ -107,28 +112,17 @@ public class KeyListFragment extends RecyclerFragment<KeySectionedListAdapter>
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_key_list_multi_encrypt: {
long[] keyIds = getAdapter().getSelectedMasterKeyIds();
Intent intent = new Intent(getActivity(), EncryptFilesActivity.class);
intent.setAction(EncryptFilesActivity.ACTION_ENCRYPT_DATA);
intent.putExtra(EncryptFilesActivity.EXTRA_ENCRYPTION_KEY_IDS, keyIds);
startActivityForResult(intent, REQUEST_ACTION);
long[] keyIds = getSelectedMasterKeyIds();
multiSelectEncrypt(keyIds);
mode.finish();
break;
}
case R.id.menu_key_list_multi_delete: {
long[] keyIds = getAdapter().getSelectedMasterKeyIds();
boolean hasSecret = getAdapter().isAnySecretKeySelected();
Intent intent = new Intent(getActivity(), DeleteKeyDialogActivity.class);
intent.putExtra(DeleteKeyDialogActivity.EXTRA_DELETE_MASTER_KEY_IDS, keyIds);
intent.putExtra(DeleteKeyDialogActivity.EXTRA_HAS_SECRET, hasSecret);
if (hasSecret) {
intent.putExtra(DeleteKeyDialogActivity.EXTRA_KEYSERVER,
Preferences.getPreferences(getActivity()).getPreferredKeyserver());
}
startActivityForResult(intent, REQUEST_DELETE);
long[] keyIds = getSelectedMasterKeyIds();
boolean hasSecret = isAnySecretKeySelected();
multiSelectDelete(keyIds, hasSecret);
mode.finish();
break;
}
}
@ -139,54 +133,61 @@ public class KeyListFragment extends RecyclerFragment<KeySectionedListAdapter>
public void onDestroyActionMode(ActionMode mode) {
mActionMode = null;
if (getAdapter() != null) {
getAdapter().finishSelection();
getAdapter().clearSelection();
}
}
};
private FastScroller fastScroller;
private final KeySectionedListAdapter.KeyListListener mKeyListener
= new KeySectionedListAdapter.KeyListListener() {
@Override
public void onKeyDummyItemClicked() {
createKey();
private void multiSelectDelete(long[] keyIds, boolean hasSecret) {
Intent intent = new Intent(getActivity(), DeleteKeyDialogActivity.class);
intent.putExtra(DeleteKeyDialogActivity.EXTRA_DELETE_MASTER_KEY_IDS, keyIds);
intent.putExtra(DeleteKeyDialogActivity.EXTRA_HAS_SECRET, hasSecret);
if (hasSecret) {
intent.putExtra(DeleteKeyDialogActivity.EXTRA_KEYSERVER,
Preferences.getPreferences(getActivity()).getPreferredKeyserver());
}
startActivityForResult(intent, REQUEST_DELETE);
}
@Override
public void onKeyItemClicked(long masterKeyId) {
Intent viewIntent = new Intent(getActivity(), ViewKeyActivity.class);
viewIntent.setData(KeyRings.buildGenericKeyRingUri(masterKeyId));
startActivityForResult(viewIntent, REQUEST_VIEW_KEY);
}
private void multiSelectEncrypt(long[] keyIds) {
Intent intent = new Intent(getActivity(), EncryptFilesActivity.class);
intent.setAction(EncryptFilesActivity.ACTION_ENCRYPT_DATA);
intent.putExtra(EncryptFilesActivity.EXTRA_ENCRYPTION_KEY_IDS, keyIds);
@Override
public void onSlingerButtonClicked(long masterKeyId) {
Intent safeSlingerIntent = new Intent(getActivity(), SafeSlingerActivity.class);
safeSlingerIntent.putExtra(SafeSlingerActivity.EXTRA_MASTER_KEY_ID, masterKeyId);
startActivityForResult(safeSlingerIntent, REQUEST_ACTION);
}
startActivityForResult(intent, REQUEST_ACTION);
}
@Override
public void onSelectionStateChanged(int selectedCount) {
if (selectedCount < 1) {
if (mActionMode != null) {
mActionMode.finish();
}
} else {
if (mActionMode == null) {
mActionMode = getActivity().startActionMode(mActionCallback);
}
String keysSelected = getResources().getQuantityString(
R.plurals.key_list_selected_keys, selectedCount, selectedCount);
mActionMode.setTitle(keysSelected);
private long[] getSelectedMasterKeyIds() {
FlexibleAdapter<FlexibleKeyItem> adapter = getAdapter();
List<Integer> selectedPositions = adapter.getSelectedPositions();
long[] keyIds = new long[selectedPositions.size()];
for (int i = 0; i < selectedPositions.size(); i++) {
FlexibleKeyItem selectedItem = adapter.getItem(selectedPositions.get(i));
if (selectedItem != null) {
keyIds[i] = selectedItem.keyInfo.master_key_id();
}
}
};
return keyIds;
}
private boolean isAnySecretKeySelected() {
FlexibleAdapter<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
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.key_list_fragment, container, false);
@ -210,6 +211,11 @@ public class KeyListFragment extends RecyclerFragment<KeySectionedListAdapter>
importFile();
});
fastScroller = view.findViewById(R.id.fast_scroller);
vSearchContainer = view.findViewById(R.id.search_container);
vSearchButton = view.findViewById(R.id.search_button);
vSearchButton.setOnClickListener(v -> startSearchForQuery());
return view;
}
@ -222,7 +228,10 @@ public class KeyListFragment extends RecyclerFragment<KeySectionedListAdapter>
super.onActivityCreated(savedInstanceState);
// show app name instead of "keys" from nav drawer
final FragmentActivity activity = getActivity();
FragmentActivity activity = getActivity();
if (activity == null) {
throw new NullPointerException("Activity must be bound!");
}
activity.setTitle(R.string.app_name);
// We have a menu item to show in action bar.
@ -231,23 +240,64 @@ public class KeyListFragment extends RecyclerFragment<KeySectionedListAdapter>
// Start out with a progress indicator.
hideList(false);
// click on search button (in empty view) starts query for search string
vSearchContainer = activity.findViewById(R.id.search_container);
vSearchButton = activity.findViewById(R.id.search_button);
vSearchButton.setOnClickListener(v -> startSearchForQuery());
setLayoutManager(new LinearLayoutManager(activity));
KeySectionedListAdapter adapter = new KeySectionedListAdapter(getContext(), null);
adapter.setKeyListener(mKeyListener);
KeyListViewModel keyListViewModel = ViewModelProviders.of(this).get(KeyListViewModel.class);
keyListViewModel.getLiveData(getContext()).observe(this, this::onLoadKeyItems);
}
setAdapter(adapter);
setLayoutManager(new LayoutManager(getActivity()));
public static class KeyListViewModel extends ViewModel {
LiveData<List<FlexibleKeyItem>> liveData;
FastScroller fastScroller = getActivity().findViewById(R.id.fastscroll);
fastScroller.setRecyclerView(getRecyclerView());
LiveData<List<FlexibleKeyItem>> getLiveData(Context context) {
if (liveData == null) {
liveData = new KeyListLiveData(context);
}
return liveData;
}
}
// Prepare the loader. Either re-connect with an existing one,
// or start a new one.
getLoaderManager().initLoader(0, null, this);
public static class KeyListLiveData extends AsyncTaskLiveData<List<FlexibleKeyItem>> {
private final KeyRingDao keyRingDao;
private FlexibleKeyItemFactory flexibleKeyItemFactory;
KeyListLiveData(@NonNull Context context) {
super(context, KeyRings.CONTENT_URI);
keyRingDao = KeyRingDao.getInstance(context.getApplicationContext());
flexibleKeyItemFactory = new FlexibleKeyItemFactory(context.getResources());
}
@Override
protected List<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
@ -305,53 +355,11 @@ public class KeyListFragment extends RecyclerFragment<KeySectionedListAdapter>
}
Intent searchIntent = new Intent(activity, ImportKeysActivity.class);
searchIntent.putExtra(ImportKeysActivity.EXTRA_QUERY, mQuery);
searchIntent.putExtra(ImportKeysActivity.EXTRA_QUERY, getAdapter().getFilter(String.class));
searchIntent.setAction(ImportKeysActivity.ACTION_IMPORT_KEY_FROM_KEYSERVER);
startActivity(searchIntent);
}
@Override
public Loader<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
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
inflater.inflate(R.menu.key_list, menu);
@ -375,19 +383,13 @@ public class KeyListFragment extends RecyclerFragment<KeySectionedListAdapter>
MenuItemCompat.setOnActionExpandListener(searchItem, new MenuItemCompat.OnActionExpandListener() {
@Override
public boolean onMenuItemActionExpand(MenuItem item) {
// disable swipe-to-refresh
// mSwipeRefreshLayout.setIsLocked(true);
return true;
}
@Override
public boolean onMenuItemActionCollapse(MenuItem item) {
mQuery = null;
getLoaderManager().restartLoader(0, null, KeyListFragment.this);
// enable swipe-to-refresh
// mSwipeRefreshLayout.setIsLocked(false);
getAdapter().setFilter(null);
getAdapter().filterItems();
return true;
}
});
@ -395,6 +397,54 @@ public class KeyListFragment extends RecyclerFragment<KeySectionedListAdapter>
super.onCreateOptionsMenu(menu, inflater);
}
@Override
public boolean onItemClick(View view, int position) {
FlexibleKeyItem item = getAdapter().getItem(position);
if (item == null) {
return false;
}
if (mActionMode != null && position != RecyclerView.NO_POSITION) {
toggleSelection(position);
return true;
}
long masterKeyId = item.keyInfo.master_key_id();
Intent viewIntent = new Intent(getActivity(), ViewKeyActivity.class);
viewIntent.setData(KeyRings.buildGenericKeyRingUri(masterKeyId));
startActivityForResult(viewIntent, REQUEST_VIEW_KEY);
return false;
}
@Override
public void onItemLongClick(int position) {
if (mActionMode == null) {
FragmentActivity activity = getActivity();
if (activity != null) {
mActionMode = activity.startActionMode(mActionCallback);
}
}
toggleSelection(position);
}
private void toggleSelection(int position) {
getAdapter().toggleSelection(position);
int count = getAdapter().getSelectedItemCount();
if (count == 0) {
mActionMode.finish();
} else {
setContextTitle(count);
}
}
private void setContextTitle(int selectedCount) {
String keysSelected = getResources().getQuantityString(
R.plurals.key_list_selected_keys, selectedCount, selectedCount);
mActionMode.setTitle(keysSelected);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
@ -410,7 +460,7 @@ public class KeyListFragment extends RecyclerFragment<KeySectionedListAdapter>
try {
KeychainDatabase.debugBackup(getActivity(), true);
Notify.create(getActivity(), "Restored debug_backup.db", Notify.Style.OK).show();
getActivity().getContentResolver().notifyChange(KeychainContract.KeyRings.CONTENT_URI, null);
getActivity().getContentResolver().notifyChange(KeyRings.CONTENT_URI, null);
} catch (IOException e) {
Timber.e(e, "IO Error");
Notify.create(getActivity(), "IO Error " + e.getMessage(), Notify.Style.ERROR).show();
@ -456,20 +506,12 @@ public class KeyListFragment extends RecyclerFragment<KeySectionedListAdapter>
}
@Override
public boolean onQueryTextChange(String s) {
Timber.d("onQueryTextChange s: %s", s);
// Called when the action bar search text has changed. Update the
// search filter, and restart the loader to do a new query with this
// filter.
// If the nav drawer is opened, onQueryTextChange("") is executed.
// This hack prevents restarting the loader.
if (!s.equals(mQuery)) {
mQuery = s;
getLoaderManager().restartLoader(0, null, this);
}
public boolean onQueryTextChange(String searchText) {
getAdapter().setFilter(searchText);
getAdapter().filterItems(300);
if (s.length() > 2) {
vSearchButton.setText(getString(R.string.btn_search_for_query, mQuery));
if (searchText.length() > 2) {
vSearchButton.setText(getString(R.string.btn_search_for_query, searchText));
vSearchContainer.setDisplayedChild(1);
vSearchContainer.setVisibility(View.VISIBLE);
} else {

View file

@ -17,6 +17,10 @@
package org.sufficientlysecure.keychain.ui;
import java.io.IOException;
import java.io.OutputStream;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
@ -26,19 +30,15 @@ import android.view.MenuInflater;
import android.view.MenuItem;
import com.tonicartos.superslim.LayoutManager;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.operations.results.OperationResult;
import org.sufficientlysecure.keychain.operations.results.OperationResult.SubLogEntryParcel;
import org.sufficientlysecure.keychain.provider.TemporaryFileProvider;
import org.sufficientlysecure.keychain.ui.adapter.NestedLogAdapter;
import org.sufficientlysecure.keychain.ui.base.RecyclerFragment;
import org.sufficientlysecure.keychain.ui.dialog.ShareLogDialogFragment;
import org.sufficientlysecure.keychain.ui.util.Notify;
import org.sufficientlysecure.keychain.ui.util.Notify.Style;
import org.sufficientlysecure.keychain.ui.base.RecyclerFragment;
import java.io.IOException;
import java.io.OutputStream;
public class LogDisplayFragment extends RecyclerFragment<NestedLogAdapter>

View file

@ -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);
}
}
}

View file

@ -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);
}
}
}

View file

@ -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();
}
}

View file

@ -17,15 +17,20 @@
package org.sufficientlysecure.keychain.ui.adapter;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import android.content.Context;
import android.database.Cursor;
import android.graphics.PorterDuff;
import android.support.v4.widget.CursorAdapter;
import android.text.format.DateUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.TextView;
@ -40,12 +45,6 @@ import org.sufficientlysecure.keychain.ui.util.Highlighter;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils.State;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
public class KeyAdapter extends CursorAdapter {
protected String mQuery;
@ -105,8 +104,6 @@ public class KeyAdapter extends CursorAdapter {
public TextView mMainUserIdRest;
public TextView mCreationDate;
public ImageView mStatus;
public View mSlinger;
public ImageButton mSlingerButton;
public KeyItem mDisplayedItem;
@ -116,8 +113,6 @@ public class KeyAdapter extends CursorAdapter {
mMainUserId = view.findViewById(R.id.key_list_item_name);
mMainUserIdRest = view.findViewById(R.id.key_list_item_email);
mStatus = view.findViewById(R.id.key_list_item_status_icon);
mSlinger = view.findViewById(R.id.key_list_item_slinger_view);
mSlingerButton = view.findViewById(R.id.key_list_item_slinger_button);
mCreationDate = view.findViewById(R.id.key_list_item_creation);
}
@ -154,28 +149,17 @@ public class KeyAdapter extends CursorAdapter {
KeyFormattingUtils
.setStatusImage(context, mStatus, null, State.REVOKED, R.color.key_flag_gray);
mStatus.setVisibility(View.VISIBLE);
mSlinger.setVisibility(View.GONE);
textColor = context.getResources().getColor(R.color.key_flag_gray);
} else if (item.mIsExpired) {
KeyFormattingUtils.setStatusImage(context, mStatus, null, State.EXPIRED, R.color.key_flag_gray);
mStatus.setVisibility(View.VISIBLE);
mSlinger.setVisibility(View.GONE);
textColor = context.getResources().getColor(R.color.key_flag_gray);
} else if (!item.mIsSecure) {
KeyFormattingUtils.setStatusImage(context, mStatus, null, State.INSECURE, R.color.key_flag_gray);
mStatus.setVisibility(View.VISIBLE);
mSlinger.setVisibility(View.GONE);
textColor = context.getResources().getColor(R.color.key_flag_gray);
} else if (item.mIsSecret) {
mStatus.setVisibility(View.GONE);
if (mSlingerButton.hasOnClickListeners()) {
mSlingerButton.setColorFilter(
FormattingUtils.getColorFromAttr(context, R.attr.colorTertiaryText),
PorterDuff.Mode.SRC_IN);
mSlinger.setVisibility(View.VISIBLE);
} else {
mSlinger.setVisibility(View.GONE);
}
textColor = FormattingUtils.getColorFromAttr(context, R.attr.colorText);
} else {
// this is a public key - show if it's verified
@ -186,7 +170,6 @@ public class KeyAdapter extends CursorAdapter {
KeyFormattingUtils.setStatusImage(context, mStatus, State.UNVERIFIED);
mStatus.setVisibility(View.VISIBLE);
}
mSlinger.setVisibility(View.GONE);
textColor = FormattingUtils.getColorFromAttr(context, R.attr.colorText);
}

View file

@ -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;
}
}
}

View file

@ -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);
}
}
}

View file

@ -7,6 +7,7 @@
<item>
<selector>
<item android:state_selected="true" android:drawable="@color/pressed_gray"/>
<item android:state_activated="true" android:drawable="@color/selected_gray"/>
</selector>
</item>
</ripple>

View file

@ -34,7 +34,7 @@
android:layout_width="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_height="match_parent">
@ -43,18 +43,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingBottom="72dp"
android:paddingLeft="16dp"
android:paddingRight="32dp"
android:paddingStart="16dp" />
<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" />
android:paddingBottom="72dp" />
</RelativeLayout>
@ -149,6 +138,21 @@
</com.getbase.floatingactionbutton.FloatingActionsMenu>
</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>

View file

@ -1,16 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
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"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:colorBackground"
super:slm_headerDisplay="sticky|inline"
super:slm_section_sectionManager="linear"
tools:ignore="ResAuto">
android:paddingRight="12dp"
android:paddingLeft="12dp">
<TextView style="@style/SectionHeader"
android:id="@android:id/text1"

View file

@ -11,6 +11,8 @@
android:descendantFocusability="blocksDescendants"
android:background="@drawable/list_item_ripple"
android:focusable="false"
android:paddingLeft="12dp"
android:paddingRight="12dp"
tools:layout_marginTop="30dp">
<ImageView
@ -61,34 +63,6 @@
</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
android:id="@+id/key_list_item_status_icon"
android:layout_width="wrap_content"

View file

@ -8,8 +8,8 @@ CREATE TABLE IF NOT EXISTS certs(
creation INTEGER NOT NULL,
data BLOB NOT NULL,
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, rank) REFERENCES user_packets(master_key_id, rank) 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
);
selectVerifyingCertDetails:

View file

@ -6,4 +6,4 @@ CREATE TABLE IF NOT EXISTS keyrings_public (
selectByMasterKeyId:
SELECT *
FROM keyrings_public
WHERE master_key_id = ?;
WHERE master_key_id = ?;

View file

@ -1,21 +1,37 @@
import java.lang.Boolean;
CREATE TABLE IF NOT EXISTS keys (
master_key_id INTEGER,
rank INTEGER,
key_id INTEGER,
master_key_id INTEGER NOT NULL,
rank INTEGER NOT NULL,
key_id INTEGER NOT NULL,
key_size INTEGER,
key_curve_oid TEXT,
algorithm INTEGER,
fingerprint BLOB,
can_certify INTEGER,
can_sign INTEGER,
can_encrypt INTEGER,
can_authenticate INTEGER,
is_revoked INTEGER,
has_secret INTEGER,
is_secure INTEGER,
creation INTEGER,
algorithm INTEGER NOT NULL,
fingerprint BLOB NOT NULL,
can_certify INTEGER AS Boolean NOT NULL,
can_sign INTEGER AS Boolean NOT NULL,
can_encrypt INTEGER AS Boolean NOT NULL,
can_authenticate INTEGER AS Boolean NOT NULL,
is_revoked INTEGER AS Boolean NOT NULL,
has_secret INTEGER AS Boolean NOT NULL,
is_secure INTEGER AS Boolean NOT NULL,
creation INTEGER NOT NULL,
expiry INTEGER,
PRIMARY KEY(master_key_id, rank),
FOREIGN KEY(master_key_id) REFERENCES
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;

View file

@ -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
);