/* * Copyright (C) 2016 Tobias Erthal * Copyright (C) 2014-2016 Dominik Schürmann <dominik@dominikschuermann.de> * * 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.database.Cursor; import android.database.MatrixCursor; import android.database.MergeCursor; import android.graphics.PorterDuff; 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.Constants; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.provider.KeychainContract; 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 org.sufficientlysecure.keychain.util.Log; import java.util.ArrayList; import java.util.Arrays; 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 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 { Log.w(Constants.TAG, "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 { Log.w(Constants.TAG, "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 TextView mMainUserId; private TextView mMainUserIdRest; private TextView mCreationDate; private ImageView mStatus; private View mSlinger; private ImageButton mSlingerButton; KeyItemViewHolder(View itemView) { super(itemView); mMainUserId = (TextView) itemView.findViewById(R.id.key_list_item_name); mMainUserIdRest = (TextView) itemView.findViewById(R.id.key_list_item_email); mStatus = (ImageView) itemView.findViewById(R.id.key_list_item_status_icon); mSlinger = itemView.findViewById(R.id.key_list_item_slinger_view); mSlingerButton = (ImageButton) itemView.findViewById(R.id.key_list_item_slinger_button); mCreationDate = (TextView) itemView.findViewById(R.id.key_list_item_creation); 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) { mMainUserId.setText(highlighter.highlight(name)); } else { mMainUserId.setText(R.string.user_id_no_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()) { String dateTime = DateUtils.formatDateTime(context, keyItem.getCreationTime(), DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_ABBREV_MONTH); mCreationDate.setText(context.getString(R.string.label_key_created, dateTime)); mCreationDate.setTextColor(textColor); mCreationDate.setVisibility(View.VISIBLE); } else { mCreationDate.setVisibility(View.GONE); } } } @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 = (TextView) 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 )); 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 interface KeyListListener { void onKeyDummyItemClicked(); void onKeyItemClicked(long masterKeyId); void onSlingerButtonClicked(long masterKeyId); void onSelectionStateChanged(int selectedCount); } }