open-keychain/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java
Vincent Breitmoser 7154159a36 analytics: remove
2020-09-05 14:14:33 +02:00

695 lines
26 KiB
Java

/*
* 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;
import java.io.IOException;
import java.util.List;
import android.animation.ObjectAnimator;
import android.app.Activity;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
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.ViewGroup;
import android.widget.Button;
import android.widget.ViewAnimator;
import androidx.annotation.WorkerThread;
import androidx.appcompat.widget.SearchView;
import androidx.core.view.MenuItemCompat;
import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModelProviders;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.getbase.floatingactionbutton.FloatingActionButton;
import com.getbase.floatingactionbutton.FloatingActionsMenu;
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 org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.KeychainDatabase;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.compatibility.ClipboardReflection;
import org.sufficientlysecure.keychain.daos.DatabaseNotifyManager;
import org.sufficientlysecure.keychain.daos.KeyRepository;
import org.sufficientlysecure.keychain.keysync.KeyserverSyncManager;
import org.sufficientlysecure.keychain.model.SubKey.UnifiedKeyInfo;
import org.sufficientlysecure.keychain.operations.KeySyncParcel;
import org.sufficientlysecure.keychain.operations.results.BenchmarkResult;
import org.sufficientlysecure.keychain.operations.results.ImportKeyResult;
import org.sufficientlysecure.keychain.operations.results.OperationResult;
import org.sufficientlysecure.keychain.pgp.PgpHelper;
import org.sufficientlysecure.keychain.service.BenchmarkInputParcel;
import org.sufficientlysecure.keychain.ui.adapter.FlexibleKeyDetailsItem;
import org.sufficientlysecure.keychain.ui.adapter.FlexibleKeyDummyItem;
import org.sufficientlysecure.keychain.ui.adapter.FlexibleKeyHeader;
import org.sufficientlysecure.keychain.ui.adapter.FlexibleKeyItem;
import org.sufficientlysecure.keychain.ui.adapter.FlexibleKeyItem.FlexibleSectionableKeyItem;
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.GenericViewModel;
import org.sufficientlysecure.keychain.ui.keyview.ViewKeyActivity;
import org.sufficientlysecure.keychain.ui.util.Notify;
import org.sufficientlysecure.keychain.ui.util.Notify.Style;
import org.sufficientlysecure.keychain.util.FabContainer;
import org.sufficientlysecure.keychain.util.Preferences;
import timber.log.Timber;
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;
private ActionMode mActionMode = null;
private Button vSearchButton;
private ViewAnimator vSearchContainer;
private FloatingActionsMenu mFab;
private KeyRepository keyRepository;
private FlexibleKeyItemFactory flexibleKeyItemFactory;
private Long queuedHighlightMasterKeyId;
private final ActionMode.Callback mActionCallback = new ActionMode.Callback() {
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
mode.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 = getSelectedMasterKeyIds();
multiSelectEncrypt(keyIds);
mode.finish();
break;
}
case R.id.menu_key_list_multi_delete: {
long[] keyIds = getSelectedMasterKeyIds();
boolean hasSecret = isAnySecretKeySelected();
multiSelectDelete(keyIds, hasSecret);
mode.finish();
break;
}
}
return false;
}
@Override
public void onDestroyActionMode(ActionMode mode) {
mActionMode = null;
if (getAdapter() != null) {
getAdapter().clearSelection();
}
}
};
private FastScroller fastScroller;
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);
}
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);
startActivityForResult(intent, REQUEST_ACTION);
}
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++) {
FlexibleKeyDetailsItem selectedItem = adapter.getItem(selectedPositions.get(i), FlexibleKeyDetailsItem.class);
if (selectedItem != null) {
keyIds[i] = selectedItem.keyInfo.master_key_id();
}
}
return keyIds;
}
private boolean isAnySecretKeySelected() {
FlexibleAdapter<FlexibleKeyItem> adapter = getAdapter();
for (int position : adapter.getSelectedPositions()) {
FlexibleKeyDetailsItem item = adapter.getItem(position, FlexibleKeyDetailsItem.class);
if (item != null && item.keyInfo.has_any_secret()) {
return true;
}
}
return false;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.key_list_fragment, container, false);
mFab = view.findViewById(R.id.fab_main);
FloatingActionButton fabQrCode = view.findViewById(R.id.fab_add_qr_code);
FloatingActionButton fabCloud = view.findViewById(R.id.fab_add_cloud);
FloatingActionButton fabFile = view.findViewById(R.id.fab_add_file);
fabQrCode.setOnClickListener(v -> {
mFab.collapse();
scanQrCode();
});
fabCloud.setOnClickListener(v -> {
mFab.collapse();
searchCloud();
});
fabFile.setOnClickListener(v -> {
mFab.collapse();
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;
}
/**
* Define Adapter and Loader on create of Activity
*/
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
// show app name instead of "keys" from nav drawer
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.
setHasOptionsMenu(true);
setLayoutManager(new LinearLayoutManager(activity));
keyRepository = KeyRepository.create(requireContext());
flexibleKeyItemFactory = new FlexibleKeyItemFactory(requireContext().getResources());
Intent intent = getActivity().getIntent();
if (intent != null && intent.hasExtra(ImportKeyResult.EXTRA_RESULT)) {
ImportKeyResult importKeyResult = intent.getParcelableExtra(ImportKeyResult.EXTRA_RESULT);
long[] importedMasterKeyIds = importKeyResult.getImportedMasterKeyIds();
if (importedMasterKeyIds != null && importedMasterKeyIds.length > 0) {
queuedHighlightMasterKeyId = importedMasterKeyIds[0];
}
}
GenericViewModel viewModel = ViewModelProviders.of(this).get(GenericViewModel.class);
LiveData<List<FlexibleKeyItem>> liveData = viewModel.getGenericLiveData(requireContext(), this::loadFlexibleKeyItems);
liveData.observe(this, this::onLoadKeyItems);
}
@WorkerThread
private List<FlexibleKeyItem> loadFlexibleKeyItems() {
List<UnifiedKeyInfo> unifiedKeyInfo = keyRepository.getAllUnifiedKeyInfo();
return flexibleKeyItemFactory.mapUnifiedKeyInfoToFlexibleKeyItems(unifiedKeyInfo);
}
private void onLoadKeyItems(List<FlexibleKeyItem> flexibleKeyItems) {
FlexibleAdapter<FlexibleKeyItem> adapter = getAdapter();
if (adapter == null) {
adapter = new FlexibleAdapter<FlexibleKeyItem>(flexibleKeyItems, this, true) {
@Override
public long getItemId(int position) {
FlexibleKeyItem item = getItem(position);
if (item instanceof FlexibleKeyDetailsItem) {
return ((FlexibleKeyDetailsItem) item).keyInfo.master_key_id();
}
return super.getItemId(position);
}
};
adapter.setDisplayHeadersAtStartUp(true);
adapter.setStickyHeaders(true);
adapter.setMode(Mode.MULTI);
setAdapter(adapter);
adapter.setFastScroller(fastScroller);
fastScroller.setBubbleTextCreator(this::getBubbleText);
} else {
adapter.updateDataSet(flexibleKeyItems, true);
}
maybeHighlightKey(adapter);
}
private void maybeHighlightKey(FlexibleAdapter<FlexibleKeyItem> adapter) {
if (queuedHighlightMasterKeyId == null) {
return;
}
for (int position = 0; position < adapter.getItemCount(); position++) {
if (adapter.getItemId(position) == queuedHighlightMasterKeyId) {
adapter.smoothScrollToPosition(position);
}
}
queuedHighlightMasterKeyId = null;
}
private String getBubbleText(int position) {
FlexibleKeyItem item = getAdapter().getItem(position);
if (item == null) {
return "";
}
if (item instanceof FlexibleSectionableKeyItem) {
FlexibleKeyHeader header = ((FlexibleSectionableKeyItem) item).getHeader();
return header.getSectionTitle();
}
if (item instanceof FlexibleKeyHeader) {
return ((FlexibleKeyHeader) item).getSectionTitle();
}
return "";
}
@Override
public void onStart() {
super.onStart();
checkClipboardForPublicKeyMaterial();
}
private void checkClipboardForPublicKeyMaterial() {
CharSequence clipboardText = ClipboardReflection.getClipboardText(getActivity());
new AsyncTask<Void, Void, Boolean>() {
@Override
protected Boolean doInBackground(Void... voids) {
if (clipboardText == null) {
return false;
}
// see if it looks like a pgp thing
String publicKeyContent = PgpHelper.getPgpPublicKeyContent(clipboardText);
return publicKeyContent != null;
}
@Override
protected void onPostExecute(Boolean clipboardDataFound) {
super.onPostExecute(clipboardDataFound);
if (clipboardDataFound) {
showClipboardDataSnackbar();
}
}
}.execute();
}
private void showClipboardDataSnackbar() {
Activity activity = getActivity();
if (activity == null) {
return;
}
Notify.create(activity, R.string.snack_keylist_clipboard_title, Notify.LENGTH_INDEFINITE, Style.OK,
() -> {
Intent intentImportExisting = new Intent(getActivity(), ImportKeysActivity.class);
intentImportExisting.setAction(ImportKeysActivity.ACTION_IMPORT_KEY_FROM_CLIPBOARD);
startActivity(intentImportExisting);
}, R.string.snack_keylist_clipboard_action).show(this);
}
private void startSearchForQuery() {
Activity activity = getActivity();
if (activity == null) {
return;
}
Intent searchIntent = new Intent(activity, ImportKeysActivity.class);
searchIntent.putExtra(ImportKeysActivity.EXTRA_QUERY, getAdapter().getFilter(String.class));
searchIntent.setAction(ImportKeysActivity.ACTION_IMPORT_KEY_FROM_KEYSERVER);
startActivity(searchIntent);
}
@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_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);
menu.findItem(R.id.menu_key_list_debug_bgsync).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) {
return true;
}
@Override
public boolean onMenuItemActionCollapse(MenuItem item) {
getAdapter().setFilter(null);
getAdapter().filterItems();
return true;
}
});
super.onCreateOptionsMenu(menu, inflater);
}
@Override
public boolean onItemClick(View view, int position) {
FlexibleKeyItem item = getAdapter().getItem(position);
if (item == null) {
return false;
}
if (item instanceof FlexibleKeyDummyItem) {
createKey();
return false;
}
if (!(item instanceof FlexibleKeyDetailsItem)) {
return false;
}
if (mActionMode != null && position != RecyclerView.NO_POSITION) {
toggleSelection(position);
return true;
}
long masterKeyId = ((FlexibleKeyDetailsItem) item).keyInfo.master_key_id();
Intent viewIntent = ViewKeyActivity.getViewKeyActivityIntent(requireActivity(), masterKeyId);
startActivityForResult(viewIntent, REQUEST_VIEW_KEY);
return false;
}
@Override
public void onItemLongClick(int position) {
if (getAdapter().getItem(position) instanceof FlexibleKeyDetailsItem) {
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()) {
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();
DatabaseNotifyManager.create(requireContext()).notifyAllKeysChange();
} catch (IOException e) {
Timber.e(e, "IO Error");
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) {
Timber.e(e, "IO Error");
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_bgsync: {
KeyserverSyncManager.debugRunSyncNow(requireContext());
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 searchText) {
getAdapter().setFilter(searchText);
getAdapter().filterItems(300);
if (searchText.length() > 2) {
vSearchButton.setText(getString(R.string.btn_search_for_query, searchText));
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() {
CryptoOperationHelper.Callback<KeySyncParcel, ImportKeyResult> callback
= new CryptoOperationHelper.Callback<KeySyncParcel, ImportKeyResult>() {
@Override
public KeySyncParcel createOperationInput() {
return KeySyncParcel.createRefreshAll();
}
@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;
}
};
CryptoOperationHelper opHelper = new CryptoOperationHelper<>(3, this, callback, R.string.progress_importing);
opHelper.setProgressCancellable(true);
opHelper.cryptoOperation();
}
private void benchmark() {
CryptoOperationHelper.Callback<BenchmarkInputParcel, BenchmarkResult> callback
= new CryptoOperationHelper.Callback<BenchmarkInputParcel, BenchmarkResult>() {
@Override
public BenchmarkInputParcel createOperationInput() {
return BenchmarkInputParcel.newInstance(); // 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) {
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();
}
}