From febe9cbe922b7299aa3577978971e282dd0bf89f Mon Sep 17 00:00:00 2001 From: Vincent Breitmoser Date: Wed, 27 Jun 2018 23:51:16 +0200 Subject: [PATCH] use MaterialChipsInput for encryption recipients --- OpenKeychain/build.gradle | 3 +- .../keychain/matcher/CustomMatchers.java | 1 - .../ui/EncryptModeAsymmetricFragment.java | 94 ++++++--- .../ui/widget/EncryptKeyCompletionView.java | 179 ------------------ .../layout/encrypt_asymmetric_fragment.xml | 46 +++-- build.gradle | 1 + 6 files changed, 97 insertions(+), 227 deletions(-) delete mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/EncryptKeyCompletionView.java diff --git a/OpenKeychain/build.gradle b/OpenKeychain/build.gradle index 4e9bb106d..ae630da4f 100644 --- a/OpenKeychain/build.gradle +++ b/OpenKeychain/build.gradle @@ -28,7 +28,7 @@ dependencies { // UI compile 'org.sufficientlysecure:html-textview:3.1' - compile 'com.splitwise:tokenautocomplete:2.0.8@aar' + compile 'com.github.sikeeoh:MaterialChipsInput:1.1.1' compile 'com.jpardogo.materialtabstrip:library:1.1.1' compile 'com.getbase:floatingactionbutton:1.10.1' compile 'com.nispok:snackbar:2.11.0' @@ -150,7 +150,6 @@ dependencyVerification { 'com.squareup.okhttp3:okhttp:a0d01017a42bba26e507fc6d448bb36e536f4b6e612f7c42de30bbdac2b7785e', 'org.apache.james:apache-mime4j-dom:e18717fe6d36f32e5c5f7cbeea1a9bf04645fdabc84e7e8374d9da10fd52e78d', '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', diff --git a/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/matcher/CustomMatchers.java b/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/matcher/CustomMatchers.java index aef83afb0..d029c5974 100644 --- a/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/matcher/CustomMatchers.java +++ b/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/matcher/CustomMatchers.java @@ -32,7 +32,6 @@ 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.widget.EncryptKeyCompletionView; import static android.support.test.espresso.matcher.ViewMatchers.hasDescendant; import static android.support.test.espresso.matcher.ViewMatchers.isAssignableFrom; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptModeAsymmetricFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptModeAsymmetricFragment.java index 6e4c15675..21aadc67d 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptModeAsymmetricFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptModeAsymmetricFragment.java @@ -23,7 +23,9 @@ import java.util.Iterator; import java.util.List; import android.arch.lifecycle.LiveData; +import android.arch.lifecycle.ViewModel; import android.arch.lifecycle.ViewModelProviders; +import android.content.Context; import android.os.Bundle; import android.support.annotation.NonNull; import android.view.LayoutInflater; @@ -31,30 +33,30 @@ import android.view.View; import android.view.ViewGroup; import android.widget.ViewAnimator; -import com.tokenautocomplete.TokenCompleteTextView.TokenListener; +import com.pchmn.materialchips.ChipsInput; +import com.pchmn.materialchips.ChipsInput.ChipsListener; +import com.pchmn.materialchips.model.Chip; +import com.pchmn.materialchips.model.ChipInterface; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.livedata.GenericLiveData; import org.sufficientlysecure.keychain.model.SubKey.UnifiedKeyInfo; import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKeyRing; import org.sufficientlysecure.keychain.provider.KeyRepository; import org.sufficientlysecure.keychain.provider.KeyRepository.NotFoundException; -import org.sufficientlysecure.keychain.ui.adapter.KeyAdapter.KeyItem; -import org.sufficientlysecure.keychain.ui.keyview.GenericViewModel; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; import org.sufficientlysecure.keychain.ui.util.Notify; import org.sufficientlysecure.keychain.ui.util.Notify.Style; -import org.sufficientlysecure.keychain.ui.widget.EncryptKeyCompletionView; import org.sufficientlysecure.keychain.ui.widget.KeySpinner; import org.sufficientlysecure.keychain.util.Passphrase; import timber.log.Timber; public class EncryptModeAsymmetricFragment extends EncryptModeFragment { - KeyRepository mKeyRepository; private KeySpinner mSignKeySpinner; - private EncryptKeyCompletionView mEncryptKeyView; + private ChipsInput mEncryptKeyView; public static final String ARG_SINGATURE_KEY_ID = "signature_key_id"; public static final String ARG_ENCRYPTION_KEY_IDS = "encryption_key_ids"; @@ -80,7 +82,9 @@ public class EncryptModeAsymmetricFragment extends EncryptModeFragment { mSignKeySpinner = view.findViewById(R.id.sign_key_spinner); mEncryptKeyView = view.findViewById(R.id.recipient_list); - mEncryptKeyView.setThreshold(1); // Start working from first character + + ViewGroup filterableListAnchor = view.findViewById(R.id.anchor_dropdown_encrypt); + mEncryptKeyView.setFilterableListLayout(filterableListAnchor); final ViewAnimator vSignatureIcon = view.findViewById(R.id.result_signature_icon); mSignKeySpinner.setOnKeyChangedListener(masterKeyId -> { @@ -92,21 +96,31 @@ public class EncryptModeAsymmetricFragment extends EncryptModeFragment { mSignKeySpinner.setShowNone(R.string.cert_none); final ViewAnimator vEncryptionIcon = view.findViewById(R.id.result_encryption_icon); - mEncryptKeyView.setTokenListener(new TokenListener() { + mEncryptKeyView.addChipsListener(new ChipsListener() { @Override - public void onTokenAdded(KeyItem o) { + public void onChipAdded(ChipInterface chipInterface, int newSize) { if (vEncryptionIcon.getDisplayedChild() != 1) { vEncryptionIcon.setDisplayedChild(1); } } @Override - public void onTokenRemoved(KeyItem o) { - int child = mEncryptKeyView.getObjects().isEmpty() ? 0 : 1; + public void onChipRemoved(ChipInterface chipInterface, int newSize) { + int child = newSize == 0 ? 0 : 1; if (vEncryptionIcon.getDisplayedChild() != child) { vEncryptionIcon.setDisplayedChild(child); } } + + @Override + public void onTextChanged(CharSequence charSequence) { + + } + + @Override + public void onActionDone(CharSequence charSequence) { + + } }); return view; @@ -115,12 +129,10 @@ public class EncryptModeAsymmetricFragment extends EncryptModeFragment { @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - mKeyRepository = KeyRepository.create(requireContext()); - GenericViewModel viewModel = ViewModelProviders.of(this).get(GenericViewModel.class); - LiveData> liveData = viewModel.getGenericLiveData(requireContext(), - mKeyRepository::getAllUnifiedKeyInfoWithSecret); - liveData.observe(this, mSignKeySpinner::setData); + EncryptModeViewModel viewModel = ViewModelProviders.of(this).get(EncryptModeViewModel.class); + viewModel.getSignKeyLiveData(requireContext()).observe(this, mSignKeySpinner::setData); + viewModel.getEncryptRecipientLiveData(requireContext()).observe(this, this::onLoadEncryptRecipients); // preselect keys given, from state or arguments if (savedInstanceState == null) { @@ -131,7 +143,40 @@ public class EncryptModeAsymmetricFragment extends EncryptModeFragment { long[] encryptionKeyIds = getArguments().getLongArray(ARG_ENCRYPTION_KEY_IDS); preselectKeys(signatureKeyId, encryptionKeyIds); } + } + private void onLoadEncryptRecipients(List keyInfoChips) { + mEncryptKeyView.setFilterableList(keyInfoChips); + } + + public static class EncryptModeViewModel extends ViewModel { + private LiveData> signKeyLiveData; + private LiveData> encryptRecipientLiveData; + + LiveData> getSignKeyLiveData(Context context) { + if (signKeyLiveData == null) { + signKeyLiveData = new GenericLiveData<>(context, null, () -> { + KeyRepository keyRepository = KeyRepository.create(context); + return keyRepository.getAllUnifiedKeyInfoWithSecret(); + }); + } + return signKeyLiveData; + } + + LiveData> getEncryptRecipientLiveData(Context context) { + if (encryptRecipientLiveData == null) { + encryptRecipientLiveData = new GenericLiveData<>(context, null, () -> { + KeyRepository keyRepository = KeyRepository.create(context); + List keyInfos = keyRepository.getAllUnifiedKeyInfo(); + ArrayList result = new ArrayList<>(); + for (UnifiedKeyInfo keyInfo : keyInfos) { + result.add(new Chip(keyInfo.master_key_id(), keyInfo.name(), keyInfo.email())); + } + return result; + }); + } + return encryptRecipientLiveData; + } } /** @@ -153,7 +198,8 @@ public class EncryptModeAsymmetricFragment extends EncryptModeFragment { try { CanonicalizedPublicKeyRing ring = mKeyRepository.getCanonicalizedPublicKeyRing(preselectedId); - mEncryptKeyView.addObject(new KeyItem(ring)); + Chip infooo = new Chip(ring.getMasterKeyId(), ring.getPrimaryUserIdWithFallback(), "infooo"); + mEncryptKeyView.addChip(infooo); } catch (NotFoundException e) { Timber.e(e, "key not found for encryption!"); Notify.create(getActivity(), getString(R.string.error_preselect_encrypt_key, @@ -180,10 +226,8 @@ public class EncryptModeAsymmetricFragment extends EncryptModeFragment { @Override public long[] getAsymmetricEncryptionKeyIds() { List keyIds = new ArrayList<>(); - for (Object object : mEncryptKeyView.getObjects()) { - if (object instanceof KeyItem) { - keyIds.add(((KeyItem) object).mKeyId); - } + for (ChipInterface chip : mEncryptKeyView.getSelectedChipList()) { + keyIds.add((long) chip.getId()); } long[] keyIdsArr = new long[keyIds.size()]; @@ -197,16 +241,12 @@ public class EncryptModeAsymmetricFragment extends EncryptModeFragment { @Override public String[] getAsymmetricEncryptionUserIds() { - List userIds = new ArrayList<>(); - for (Object object : mEncryptKeyView.getObjects()) { - if (object instanceof KeyItem) { - userIds.add(((KeyItem) object).mUserIdFull); - } + for (ChipInterface chip : mEncryptKeyView.getSelectedChipList()) { + userIds.add(chip.getInfo()); } return userIds.toArray(new String[userIds.size()]); - } @Override diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/EncryptKeyCompletionView.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/EncryptKeyCompletionView.java deleted file mode 100644 index 95b68a9f6..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/EncryptKeyCompletionView.java +++ /dev/null @@ -1,179 +0,0 @@ -/* - * Copyright (C) 2017 Schürmann & Breitmoser GbR - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sufficientlysecure.keychain.ui.widget; - - -import android.content.Context; -import android.database.Cursor; -import android.graphics.Color; -import android.graphics.Rect; -import android.net.Uri; -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.app.LoaderManager.LoaderCallbacks; -import android.support.v4.content.CursorLoader; -import android.support.v4.content.Loader; -import android.text.TextUtils; -import android.util.AttributeSet; -import android.view.LayoutInflater; -import android.view.View; -import android.view.inputmethod.InputMethodManager; -import android.widget.TextView; - -import com.tokenautocomplete.TokenCompleteTextView; -import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.provider.KeychainContract; -import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; -import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables; -import org.sufficientlysecure.keychain.ui.adapter.KeyAdapter; -import org.sufficientlysecure.keychain.ui.adapter.KeyAdapter.KeyItem; -import timber.log.Timber; - - -public class EncryptKeyCompletionView extends TokenCompleteTextView - implements LoaderCallbacks { - - public static final String ARG_QUERY = "query"; - - private KeyAdapter mAdapter; - private LoaderManager mLoaderManager; - - public EncryptKeyCompletionView(Context context) { - super(context); - initView(); - } - - public EncryptKeyCompletionView(Context context, AttributeSet attrs) { - super(context, attrs); - initView(); - } - - public EncryptKeyCompletionView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - initView(); - } - - private void initView() { - allowDuplicates(false); - - mAdapter = new KeyAdapter(getContext(), null, 0); - setAdapter(mAdapter); - } - - @Override - protected View getViewForObject(KeyItem keyItem) { - LayoutInflater l = LayoutInflater.from(getContext()); - View view = l.inflate(R.layout.recipient_box_entry, null); - ((TextView) view.findViewById(android.R.id.text1)).setText(keyItem.getReadableName()); - - if (keyItem.mIsRevoked || !keyItem.mHasEncrypt || keyItem.mIsExpired) { - ((TextView) view.findViewById(android.R.id.text1)).setTextColor(Color.RED); - } - - return view; - } - - @Override - protected KeyItem defaultObject(String completionText) { - return null; - } - - @Override - protected void onAttachedToWindow() { - super.onAttachedToWindow(); - - if (getContext() instanceof FragmentActivity) { - mLoaderManager = ((FragmentActivity) getContext()).getSupportLoaderManager(); - } else { - Timber.e("EncryptKeyCompletionView must be attached to a FragmentActivity, this is " + - getContext().getClass()); - } - } - - @Override - protected void onDetachedFromWindow() { - super.onDetachedFromWindow(); - mLoaderManager = null; - } - - @Override - public Loader onCreateLoader(int id, Bundle args) { - // These are the rows that we will retrieve. - Uri baseUri = KeyRings.buildUnifiedKeyRingsUri(); - - String[] projection = KeyAdapter.getProjectionWith(new String[]{ - KeychainContract.KeyRings.HAS_ENCRYPT, - }); - - String where = KeyRings.HAS_ENCRYPT + " NOT NULL AND " - + KeyRings.IS_EXPIRED + " = 0 AND " - + Tables.KEYS + "." + KeyRings.IS_REVOKED + " = 0"; - - String query = args.getString(ARG_QUERY); - mAdapter.setSearchQuery(query); - - where += " AND " + KeyRings.USER_ID + " LIKE ?"; - - return new CursorLoader(getContext(), baseUri, projection, where, - new String[]{"%" + query + "%"}, null); - - } - - @Override - public void onLoadFinished(Loader loader, Cursor data) { - mAdapter.swapCursor(data); - } - - @Override - public void onLoaderReset(Loader loader) { - mAdapter.swapCursor(null); - } - - @Override - public void showDropDown() { - if (mAdapter == null || mAdapter.getCursor() == null || mAdapter.getCursor().isClosed()) { - return; - } - super.showDropDown(); - } - - @Override - public void onFocusChanged(boolean hasFocus, int direction, Rect previous) { - super.onFocusChanged(hasFocus, direction, previous); - if (hasFocus) { - ((InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE)) - .showSoftInput(this, InputMethodManager.SHOW_IMPLICIT); - } - } - - @Override - protected void performFiltering(@NonNull CharSequence text, int start, int end, int keyCode) { -// super.performFiltering(text, start, end, keyCode); - String query = text.subSequence(start, end).toString(); - if (TextUtils.isEmpty(query) || query.length() < 2) { - mAdapter.swapCursor(null); - return; - } - Bundle args = new Bundle(); - args.putString(ARG_QUERY, query); - mLoaderManager.restartLoader(0, args, this); - } - -} diff --git a/OpenKeychain/src/main/res/layout/encrypt_asymmetric_fragment.xml b/OpenKeychain/src/main/res/layout/encrypt_asymmetric_fragment.xml index cab667f63..b191c1b80 100644 --- a/OpenKeychain/src/main/res/layout/encrypt_asymmetric_fragment.xml +++ b/OpenKeychain/src/main/res/layout/encrypt_asymmetric_fragment.xml @@ -1,18 +1,19 @@ + > + android:layout_marginRight="16dp" + android:layout_marginLeft="16dp" + android:background="?android:attr/editTextBackground"> + android:outAnimation="@anim/fade_out" + > - + app:hint="@string/label_to" + app:chip_hasAvatarIcon="false" + app:maxRows="2" + app:chip_detailed_backgroundColor="@color/colorChipViewBackground" + /> + + + android:background="?android:attr/editTextBackground"> + android:layout_height="wrap_content"> + android:paddingLeft="8dp" + android:paddingRight="8dp" + />