open-keychain/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java
Tobias Erthal b19f7a776f Changed all occurrences of StickyListHeaders ListView to RecyclerView.
Implemented SuperSlim in Recyclerview of ViewKeyAdvCertsFragment.
Some changes to the way data is bound to the views in the list.
2016-09-11 16:48:35 +02:00

696 lines
26 KiB
Java

/*
* Copyright (C) 2013-2015 Dominik Schürmann <dominik@dominikschuermann.de>
* Copyright (C) 2014-2015 Vincent Breitmoser <v.breitmoser@mugenguild.com>
*
* 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;
import android.animation.ObjectAnimator;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Intent;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.database.MergeCursor;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
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.SearchView;
import android.text.TextUtils;
import android.view.ActionMode;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.Button;
import android.widget.ViewAnimator;
import com.getbase.floatingactionbutton.FloatingActionButton;
import com.getbase.floatingactionbutton.FloatingActionsMenu;
import com.tonicartos.superslim.LayoutManager;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing;
import org.sufficientlysecure.keychain.operations.results.BenchmarkResult;
import org.sufficientlysecure.keychain.operations.results.ConsolidateResult;
import org.sufficientlysecure.keychain.operations.results.ImportKeyResult;
import org.sufficientlysecure.keychain.operations.results.OperationResult;
import org.sufficientlysecure.keychain.provider.KeychainContract;
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
import org.sufficientlysecure.keychain.provider.KeychainDatabase;
import org.sufficientlysecure.keychain.provider.ProviderHelper;
import org.sufficientlysecure.keychain.service.BenchmarkInputParcel;
import org.sufficientlysecure.keychain.service.ConsolidateInputParcel;
import org.sufficientlysecure.keychain.service.ImportKeyringParcel;
import org.sufficientlysecure.keychain.ui.adapter.KeyAdapter;
import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
import org.sufficientlysecure.keychain.ui.util.Notify;
import org.sufficientlysecure.keychain.ui.adapter.KeySectionedListAdapter;
import org.sufficientlysecure.keychain.ui.util.recyclerview.RecyclerFragment;
import org.sufficientlysecure.keychain.util.FabContainer;
import org.sufficientlysecure.keychain.util.Log;
import org.sufficientlysecure.keychain.util.Preferences;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
/**
* Public key list with sticky list headers. It does _not_ extend ListFragment because it uses
* StickyListHeaders library which does not extend upon ListView.
*/
public class KeyListFragment extends RecyclerFragment<KeySectionedListAdapter>
implements SearchView.OnQueryTextListener,
LoaderManager.LoaderCallbacks<Cursor>, FabContainer,
CryptoOperationHelper.Callback<ImportKeyringParcel, ImportKeyResult> {
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;
// for CryptoOperationHelper import
private ArrayList<ParcelableKeyRing> mKeyList;
private String mKeyserver;
private CryptoOperationHelper<ImportKeyringParcel, ImportKeyResult> mImportOpHelper;
// for ConsolidateOperation
private CryptoOperationHelper<ConsolidateInputParcel, ConsolidateResult> mConsolidateOpHelper;
// Callbacks related to listview and menu events
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);
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return false;
}
@Override
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);
mode.finish();
break;
}
case R.id.menu_key_list_multi_delete: {
long[] keyIds = getAdapter().getSelectedMasterKeyIds();
boolean hasSecret = getAdapter().isAnySecretKeySelected();
System.out.println(Arrays.toString(keyIds));
System.out.println(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);
break;
}
}
return false;
}
@Override
public void onDestroyActionMode(ActionMode mode) {
mActionMode = null;
if(getAdapter() != null) {
getAdapter().finishSelection();
}
}
};
private final KeySectionedListAdapter.KeyListListener mKeyListener
= new KeySectionedListAdapter.KeyListListener() {
@Override
public void onKeyDummyItemClicked() {
createKey();
}
@Override
public void onKeyItemClicked(long masterKeyId) {
Intent viewIntent = new Intent(getActivity(), ViewKeyActivity.class);
viewIntent.setData(KeyRings.buildGenericKeyRingUri(masterKeyId));
startActivityForResult(viewIntent, REQUEST_VIEW_KEY);
}
@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);
}
@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);
}
}
};
/**
* Load custom layout with StickyListView from library
*/
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.key_list_fragment, container, false);
mFab = (FloatingActionsMenu) view.findViewById(R.id.fab_main);
FloatingActionButton fabQrCode = (FloatingActionButton) view.findViewById(R.id.fab_add_qr_code);
FloatingActionButton fabCloud = (FloatingActionButton) view.findViewById(R.id.fab_add_cloud);
FloatingActionButton fabFile = (FloatingActionButton) view.findViewById(R.id.fab_add_file);
fabQrCode.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
mFab.collapse();
scanQrCode();
}
});
fabCloud.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
mFab.collapse();
searchCloud();
}
});
fabFile.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
mFab.collapse();
importFile();
}
});
return view;
}
/**
* Define Adapter and Loader on create of Activity
*/
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
// show app name instead of "keys" from nav drawer
final FragmentActivity activity = getActivity();
activity.setTitle(R.string.app_name);
// We have a menu item to show in action bar.
setHasOptionsMenu(true);
// Start out with a progress indicator.
hideList(false);
// click on search button (in empty view) starts query for search string
vSearchContainer = (ViewAnimator) activity.findViewById(R.id.search_container);
vSearchButton = (Button) activity.findViewById(R.id.search_button);
vSearchButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
startSearchForQuery();
}
});
// Create an empty adapter we will use to display the loaded data.
//mAdapter = new KeyListAdapter(activity, null, 0);
KeySectionedListAdapter adapter = new KeySectionedListAdapter(getContext(), null);
adapter.setKeyListener(mKeyListener);
setAdapter(adapter);
setLayoutManager(new LayoutManager(getActivity()));
// Prepare the loader. Either re-connect with an existing one,
// or start a new one.
getLoaderManager().initLoader(0, null, this);
}
private void startSearchForQuery() {
Activity activity = getActivity();
if (activity == null) {
return;
}
Intent searchIntent = new Intent(activity, ImportKeysActivity.class);
searchIntent.putExtra(ImportKeysActivity.EXTRA_QUERY, mQuery);
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.KeyCursor.PROJECTION, null, null,
KeySectionedListAdapter.KeyCursor.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.KeyCursor.wrap(data));
// end action mode, if any
if (mActionMode != null) {
mActionMode.finish();
}
// The list should now be shown.
if (isResumed()) {
showList(true);
} else {
showList(false);
}
}
@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);
if (Constants.DEBUG) {
menu.findItem(R.id.menu_key_list_debug_cons).setVisible(true);
menu.findItem(R.id.menu_key_list_debug_bench).setVisible(true);
menu.findItem(R.id.menu_key_list_debug_read).setVisible(true);
menu.findItem(R.id.menu_key_list_debug_write).setVisible(true);
menu.findItem(R.id.menu_key_list_debug_first_time).setVisible(true);
}
// Get the searchview
MenuItem searchItem = menu.findItem(R.id.menu_key_list_search);
SearchView searchView = (SearchView) MenuItemCompat.getActionView(searchItem);
// Execute this when searching
searchView.setOnQueryTextListener(this);
// Erase search result without focus
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);
return true;
}
});
super.onCreateOptionsMenu(menu, inflater);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_key_list_create:
createKey();
return true;
case R.id.menu_key_list_update_all_keys:
updateAllKeys();
return true;
case R.id.menu_key_list_debug_read:
try {
KeychainDatabase.debugBackup(getActivity(), true);
Notify.create(getActivity(), "Restored debug_backup.db", Notify.Style.OK).show();
getActivity().getContentResolver().notifyChange(KeychainContract.KeyRings.CONTENT_URI, null);
} catch (IOException e) {
Log.e(Constants.TAG, "IO Error", e);
Notify.create(getActivity(), "IO Error " + e.getMessage(), Notify.Style.ERROR).show();
}
return true;
case R.id.menu_key_list_debug_write:
try {
KeychainDatabase.debugBackup(getActivity(), false);
Notify.create(getActivity(), "Backup to debug_backup.db completed", Notify.Style.OK).show();
} catch (IOException e) {
Log.e(Constants.TAG, "IO Error", e);
Notify.create(getActivity(), "IO Error: " + e.getMessage(), Notify.Style.ERROR).show();
}
return true;
case R.id.menu_key_list_debug_first_time:
Preferences prefs = Preferences.getPreferences(getActivity());
prefs.setFirstTime(true);
Intent intent = new Intent(getActivity(), CreateKeyActivity.class);
intent.putExtra(CreateKeyActivity.EXTRA_FIRST_TIME, true);
startActivity(intent);
getActivity().finish();
return true;
case R.id.menu_key_list_debug_cons:
consolidate();
return true;
case R.id.menu_key_list_debug_bench:
benchmark();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
@Override
public boolean onQueryTextSubmit(String s) {
return true;
}
@Override
public boolean onQueryTextChange(String s) {
Log.d(Constants.TAG, "onQueryTextChange 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);
}
if (s.length() > 2) {
vSearchButton.setText(getString(R.string.btn_search_for_query, mQuery));
vSearchContainer.setDisplayedChild(1);
vSearchContainer.setVisibility(View.VISIBLE);
} else {
vSearchContainer.setDisplayedChild(0);
vSearchContainer.setVisibility(View.GONE);
}
return true;
}
private void searchCloud() {
Intent importIntent = new Intent(getActivity(), ImportKeysActivity.class);
importIntent.putExtra(ImportKeysActivity.EXTRA_QUERY, (String) null); // hack to show only cloud tab
startActivity(importIntent);
}
private void scanQrCode() {
Intent scanQrCode = new Intent(getActivity(), ImportKeysProxyActivity.class);
scanQrCode.setAction(ImportKeysProxyActivity.ACTION_SCAN_IMPORT);
startActivityForResult(scanQrCode, REQUEST_ACTION);
}
private void importFile() {
Intent intentImportExisting = new Intent(getActivity(), ImportKeysActivity.class);
intentImportExisting.setAction(ImportKeysActivity.ACTION_IMPORT_KEY_FROM_FILE_AND_RETURN);
startActivityForResult(intentImportExisting, REQUEST_ACTION);
}
private void createKey() {
Intent intent = new Intent(getActivity(), CreateKeyActivity.class);
startActivityForResult(intent, REQUEST_ACTION);
}
private void updateAllKeys() {
Activity activity = getActivity();
if (activity == null) {
return;
}
ProviderHelper providerHelper = new ProviderHelper(activity);
Cursor cursor = providerHelper.getContentResolver().query(
KeyRings.buildUnifiedKeyRingsUri(), new String[]{
KeyRings.FINGERPRINT
}, null, null, null
);
if (cursor == null) {
Notify.create(activity, R.string.error_loading_keys, Notify.Style.ERROR);
return;
}
ArrayList<ParcelableKeyRing> keyList = new ArrayList<>();
try {
while (cursor.moveToNext()) {
byte[] blob = cursor.getBlob(0); //fingerprint column is 0
String fingerprint = KeyFormattingUtils.convertFingerprintToHex(blob);
ParcelableKeyRing keyEntry = new ParcelableKeyRing(fingerprint, null);
keyList.add(keyEntry);
}
mKeyList = keyList;
} finally {
cursor.close();
}
// search config
mKeyserver = Preferences.getPreferences(getActivity()).getPreferredKeyserver();
mImportOpHelper = new CryptoOperationHelper<>(1, this, this, R.string.progress_updating);
mImportOpHelper.setProgressCancellable(true);
mImportOpHelper.cryptoOperation();
}
private void consolidate() {
CryptoOperationHelper.Callback<ConsolidateInputParcel, ConsolidateResult> callback
= new CryptoOperationHelper.Callback<ConsolidateInputParcel, ConsolidateResult>() {
@Override
public ConsolidateInputParcel createOperationInput() {
return new ConsolidateInputParcel(false); // we want to perform a full consolidate
}
@Override
public void onCryptoOperationSuccess(ConsolidateResult result) {
result.createNotify(getActivity()).show();
}
@Override
public void onCryptoOperationCancelled() {
}
@Override
public void onCryptoOperationError(ConsolidateResult result) {
result.createNotify(getActivity()).show();
}
@Override
public boolean onCryptoSetProgress(String msg, int progress, int max) {
return false;
}
};
mConsolidateOpHelper = new CryptoOperationHelper<>(2, this, callback, R.string.progress_importing);
mConsolidateOpHelper.cryptoOperation();
}
private void benchmark() {
CryptoOperationHelper.Callback<BenchmarkInputParcel, BenchmarkResult> callback
= new CryptoOperationHelper.Callback<BenchmarkInputParcel, BenchmarkResult>() {
@Override
public BenchmarkInputParcel createOperationInput() {
return new BenchmarkInputParcel(); // we want to perform a full consolidate
}
@Override
public void onCryptoOperationSuccess(BenchmarkResult result) {
result.createNotify(getActivity()).show();
}
@Override
public void onCryptoOperationCancelled() {
}
@Override
public void onCryptoOperationError(BenchmarkResult result) {
result.createNotify(getActivity()).show();
}
@Override
public boolean onCryptoSetProgress(String msg, int progress, int max) {
return false;
}
};
CryptoOperationHelper opHelper = new CryptoOperationHelper<>(2, this, callback, R.string.progress_importing);
opHelper.cryptoOperation();
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (mImportOpHelper != null) {
mImportOpHelper.handleActivityResult(requestCode, resultCode, data);
}
if (mConsolidateOpHelper != null) {
mConsolidateOpHelper.handleActivityResult(requestCode, resultCode, data);
}
switch (requestCode) {
case REQUEST_DELETE:
if (mActionMode != null) {
mActionMode.finish();
}
if (data != null && data.hasExtra(OperationResult.EXTRA_RESULT)) {
OperationResult result = data.getParcelableExtra(OperationResult.EXTRA_RESULT);
result.createNotify(getActivity()).show();
} else {
super.onActivityResult(requestCode, resultCode, data);
}
break;
case REQUEST_ACTION:
// if a result has been returned, display a notify
if (data != null && data.hasExtra(OperationResult.EXTRA_RESULT)) {
OperationResult result = data.getParcelableExtra(OperationResult.EXTRA_RESULT);
result.createNotify(getActivity()).show();
} else {
super.onActivityResult(requestCode, resultCode, data);
}
break;
case REQUEST_VIEW_KEY:
if (data != null && data.hasExtra(OperationResult.EXTRA_RESULT)) {
OperationResult result = data.getParcelableExtra(OperationResult.EXTRA_RESULT);
result.createNotify(getActivity()).show();
} else {
super.onActivityResult(requestCode, resultCode, data);
}
break;
}
}
@Override
public void fabMoveUp(int height) {
ObjectAnimator anim = ObjectAnimator.ofFloat(mFab, "translationY", 0, -height);
// we're a little behind, so skip 1/10 of the time
anim.setDuration(270);
anim.start();
}
@Override
public void fabRestorePosition() {
ObjectAnimator anim = ObjectAnimator.ofFloat(mFab, "translationY", 0);
// we're a little ahead, so wait a few ms
anim.setStartDelay(70);
anim.setDuration(300);
anim.start();
}
// CryptoOperationHelper.Callback methods
@Override
public ImportKeyringParcel createOperationInput() {
return new ImportKeyringParcel(mKeyList, mKeyserver);
}
@Override
public void onCryptoOperationSuccess(ImportKeyResult result) {
result.createNotify(getActivity()).show();
}
@Override
public void onCryptoOperationCancelled() {
}
@Override
public void onCryptoOperationError(ImportKeyResult result) {
result.createNotify(getActivity()).show();
}
@Override
public boolean onCryptoSetProgress(String msg, int progress, int max) {
return false;
}
}