diff --git a/OpenKeychain/src/main/AndroidManifest.xml b/OpenKeychain/src/main/AndroidManifest.xml index 8d76101ae..17f364572 100644 --- a/OpenKeychain/src/main/AndroidManifest.xml +++ b/OpenKeychain/src/main/AndroidManifest.xml @@ -873,6 +873,11 @@ android:name=".remote.ui.RemoteSelectPubKeyActivity" android:exported="false" android:label="@string/app_name" /> + > { + private final KeyInfoInteractor keyInfoInteractor; + + private KeySelector keySelector; + + public KeyInfoLiveData(Context context, ContentResolver contentResolver) { + super(context, null); + + this.keyInfoInteractor = new KeyInfoInteractor(contentResolver); + } + + public void setKeySelector(KeySelector keySelector) { + this.keySelector = keySelector; + + updateDataInBackground(); + } + + @Override + protected List asyncLoadData() { + if (keySelector == null) { + return null; + } + return keyInfoInteractor.loadKeyInfo(keySelector); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/livedata/PgpKeyGenerationLiveData.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/livedata/PgpKeyGenerationLiveData.java new file mode 100644 index 000000000..d9515600d --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/livedata/PgpKeyGenerationLiveData.java @@ -0,0 +1,38 @@ +package org.sufficientlysecure.keychain.livedata; + + +import android.content.Context; + +import org.sufficientlysecure.keychain.operations.results.PgpEditKeyResult; +import org.sufficientlysecure.keychain.pgp.PgpKeyOperation; +import org.sufficientlysecure.keychain.service.SaveKeyringParcel; +import org.sufficientlysecure.keychain.ui.keyview.loader.AsyncTaskLiveData; +import org.sufficientlysecure.keychain.util.ProgressScaler; + + +public class PgpKeyGenerationLiveData extends AsyncTaskLiveData { + private SaveKeyringParcel saveKeyringParcel; + + public PgpKeyGenerationLiveData(Context context) { + super(context, null); + } + + public void setSaveKeyringParcel(SaveKeyringParcel saveKeyringParcel) { + if (this.saveKeyringParcel == saveKeyringParcel) { + return; + } + this.saveKeyringParcel = saveKeyringParcel; + + updateDataInBackground(); + } + + @Override + protected PgpEditKeyResult asyncLoadData() { + if (saveKeyringParcel == null) { + return null; + } + + PgpKeyOperation keyOperations = new PgpKeyOperation(new ProgressScaler()); + return keyOperations.createSecretKeyRing(saveKeyringParcel); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ApiPendingIntentFactory.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ApiPendingIntentFactory.java index 9634bd689..f2b6e6a94 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ApiPendingIntentFactory.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ApiPendingIntentFactory.java @@ -38,6 +38,7 @@ import org.sufficientlysecure.keychain.remote.ui.RequestKeyPermissionActivity; import org.sufficientlysecure.keychain.remote.ui.SelectSignKeyIdActivity; import org.sufficientlysecure.keychain.remote.ui.dialog.RemoteDeduplicateActivity; import org.sufficientlysecure.keychain.remote.ui.dialog.RemoteSelectAuthenticationKeyActivity; +import org.sufficientlysecure.keychain.remote.ui.dialog.RemoteSelectIdKeyActivity; import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; import org.sufficientlysecure.keychain.ui.keyview.ViewKeyActivity; @@ -89,6 +90,16 @@ public class ApiPendingIntentFactory { return createInternal(data, intent); } + PendingIntent createSelectIdentityKeyPendingIntent( + String packageName, String apiIdentity, Long currentMasterKeyId) { + Intent intent = new Intent(mContext, RemoteSelectIdKeyActivity.class); + intent.putExtra(RemoteSelectIdKeyActivity.EXTRA_PACKAGE_NAME, packageName); + intent.putExtra(RemoteSelectIdKeyActivity.EXTRA_USER_ID, apiIdentity); + intent.putExtra(RemoteSelectIdKeyActivity.EXTRA_CURRENT_MASTER_KEY_ID, currentMasterKeyId); + + return createInternal(null, intent); + } + PendingIntent createSelectPublicKeyPendingIntent(Intent data, long[] keyIdsArray, ArrayList missingEmails, ArrayList duplicateEmails, boolean noUserIdsCheck) { Intent intent = new Intent(mContext, RemoteSelectPubKeyActivity.class); @@ -134,9 +145,10 @@ public class ApiPendingIntentFactory { } PendingIntent createSelectSignKeyIdPendingIntent(Intent data, String packageName, String preferredUserId) { - Intent intent = new Intent(mContext, SelectSignKeyIdActivity.class); + Intent intent = new Intent(mContext, RemoteSelectIdKeyActivity.class); intent.setData(KeychainContract.ApiApps.buildByPackageNameUri(packageName)); - intent.putExtra(SelectSignKeyIdActivity.EXTRA_USER_ID, preferredUserId); + intent.putExtra(RemoteSelectIdKeyActivity.EXTRA_PACKAGE_NAME, packageName); + intent.putExtra(RemoteSelectIdKeyActivity.EXTRA_USER_ID, preferredUserId); return createInternal(data, intent); } @@ -182,7 +194,9 @@ public class ApiPendingIntentFactory { private PendingIntent createInternal(Intent data, Intent intent) { // re-attach "data" for pass through. It will be used later to repeat pgp operation - intent.putExtra(RemoteSecurityTokenOperationActivity.EXTRA_DATA, data); + if (data != null) { + intent.putExtra(RemoteSecurityTokenOperationActivity.EXTRA_DATA, data); + } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { //noinspection ResourceType, looks like lint is missing FLAG_IMMUTABLE diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ApiPermissionHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ApiPermissionHelper.java index 97aa7035e..445886892 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ApiPermissionHelper.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ApiPermissionHelper.java @@ -65,7 +65,7 @@ public class ApiPermissionHelper { /** Returns true iff the caller is allowed, or false on any type of problem. * This method should only be used in cases where error handling is dealt with separately. */ - protected boolean isAllowedIgnoreErrors() { + public boolean isAllowedIgnoreErrors() { try { return isCallerAllowed(); } catch (WrongPackageCertificateException e) { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService.java index 1fcba550f..3a09a54cf 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService.java @@ -196,8 +196,21 @@ public class OpenPgpService extends Service { } } + private Intent autocryptQueryImpl(Intent data) { + try { + KeyIdResult keyIdResult = mKeyIdExtractor.returnKeyIdsFromIntent(data, false, + mApiPermissionHelper.getCurrentCallingPackage()); + Intent resultIntent = getAutocryptStatusResult(keyIdResult); + + return resultIntent; + } catch (Exception e) { + Timber.d(e, "encryptAndSignImpl"); + return createErrorResultIntent(OpenPgpError.GENERIC_ERROR, e.getMessage()); + } + } + private Intent encryptAndSignImpl(Intent data, InputStream inputStream, - OutputStream outputStream, boolean sign, boolean isQueryAutocryptStatus) { + OutputStream outputStream, boolean sign) { try { PgpSignEncryptData.Builder pgpData = PgpSignEncryptData.builder() .setVersionHeader(null); @@ -225,9 +238,6 @@ public class OpenPgpService extends Service { mApiPermissionHelper.getCurrentCallingPackage()); KeyIdResultStatus keyIdResultStatus = keyIdResult.getStatus(); - if (isQueryAutocryptStatus) { - return getAutocryptStatusResult(keyIdResult); - } boolean asciiArmor = data.getBooleanExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true); pgpData.setEnableAsciiArmorOutput(asciiArmor); @@ -936,12 +946,12 @@ public class OpenPgpService extends Service { return signImpl(data, inputStream, outputStream, false); } case OpenPgpApi.ACTION_QUERY_AUTOCRYPT_STATUS: { - return encryptAndSignImpl(data, inputStream, outputStream, false, true); + return autocryptQueryImpl(data); } case OpenPgpApi.ACTION_ENCRYPT: case OpenPgpApi.ACTION_SIGN_AND_ENCRYPT: { boolean enableSign = action.equals(OpenPgpApi.ACTION_SIGN_AND_ENCRYPT); - return encryptAndSignImpl(data, inputStream, outputStream, enableSign, false); + return encryptAndSignImpl(data, inputStream, outputStream, enableSign); } case OpenPgpApi.ACTION_DECRYPT_VERIFY: { return decryptAndVerifyImpl(data, inputStream, outputStream, false, progressable); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/SelectIdentityKeyListFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/SelectIdentityKeyListFragment.java new file mode 100644 index 000000000..3b6d033c2 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/SelectIdentityKeyListFragment.java @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2015 Dominik Schürmann + * + * 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.remote.ui; + + +import android.app.Activity; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.support.v4.app.LoaderManager; +import android.support.v4.content.CursorLoader; +import android.support.v4.content.Loader; +import android.support.v7.widget.LinearLayoutManager; + +import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; +import org.sufficientlysecure.keychain.remote.ui.adapter.SelectIdentityKeyAdapter; +import org.sufficientlysecure.keychain.ui.base.RecyclerFragment; +import org.sufficientlysecure.keychain.ui.util.adapter.CursorAdapter; +import org.sufficientlysecure.keychain.ui.util.recyclerview.DividerItemDecoration; + + +public class SelectIdentityKeyListFragment extends RecyclerFragment + implements SelectIdentityKeyAdapter.SelectSignKeyListener, LoaderManager.LoaderCallbacks { + private static final String ARG_API_IDENTITY = "api_identity"; + private String apiIdentity; + private boolean listAllKeys; + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.getString(ARG_API_IDENTITY, apiIdentity); + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + if (savedInstanceState != null && apiIdentity == null) { + apiIdentity = getArguments().getString(ARG_API_IDENTITY); + } + + SelectIdentityKeyAdapter adapter = new SelectIdentityKeyAdapter(getContext(), null); + adapter.setListener(this); + + setAdapter(adapter); + LinearLayoutManager layoutManager = new LinearLayoutManager(getContext()); + DividerItemDecoration dividerItemDecoration = + new DividerItemDecoration(getContext(), layoutManager.getOrientation(), true); + setLayoutManager(layoutManager); + getRecyclerView().addItemDecoration(dividerItemDecoration); + + // Start out with a progress indicator. + hideList(false); + + // Prepare the loader. Either re-connect with an existing one, + // or start a new one. + getLoaderManager().initLoader(0, null, this); + } + + @Override + public Loader onCreateLoader(int id, Bundle args) { + // These are the rows that we will retrieve. + String[] projection = new String[]{ + KeyRings._ID, + KeyRings.MASTER_KEY_ID, + KeyRings.USER_ID, + KeyRings.IS_EXPIRED, + KeyRings.IS_REVOKED, + KeyRings.HAS_ENCRYPT, + KeyRings.VERIFIED, + KeyRings.HAS_ANY_SECRET, + KeyRings.HAS_DUPLICATE_USER_ID, + KeyRings.CREATION, + KeyRings.NAME, + KeyRings.EMAIL, + KeyRings.COMMENT, + }; + + String selection = KeyRings.HAS_ANY_SECRET + " != 0"; + Uri baseUri = listAllKeys ? KeyRings.buildUnifiedKeyRingsUri() : + KeyRings.buildUnifiedKeyRingsFindByEmailUri(apiIdentity); + + String orderBy = KeyRings.USER_ID + " ASC"; + // Now create and return a CursorLoader that will take care of + // creating a Cursor for the data being displayed. + return new CursorLoader(getActivity(), baseUri, projection, selection, null, orderBy); + } + + @Override + public void onLoadFinished(Loader loader, Cursor data) { + // Swap the new cursor in. (The framework will take care of closing the + // old cursor once we return.) + getAdapter().swapCursor(CursorAdapter.KeyCursor.wrap(data)); + + // The list should now be shown. + if (isResumed()) { + showList(true); + } else { + showList(false); + } + + boolean isEmpty = data.getCount() == 0; + getKeySelectFragmentListener().onChangeListEmptyStatus(isEmpty); + } + + @Override + public void onLoaderReset(Loader 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 onDestroy() { + getAdapter().setListener(null); + super.onDestroy(); + } + + @Override + public void onSelectKeyItemClicked(long masterKeyId) { + getKeySelectFragmentListener().onKeySelected(masterKeyId); + } + + SelectIdentityKeyFragmentListener getKeySelectFragmentListener() { + Activity activity = getActivity(); + if (activity == null) { + return null; + } + + if (!(activity instanceof SelectIdentityKeyFragmentListener)) { + throw new IllegalStateException("SelectIdentityKeyListFragment must be attached to KeySelectFragmentListener!"); + } + + return (SelectIdentityKeyFragmentListener) activity; + } + + public void setApiIdentity(String apiIdentity) { + this.apiIdentity = apiIdentity; + } + + public void setListAllKeys(boolean listAllKeys) { + this.listAllKeys = listAllKeys; + getLoaderManager().restartLoader(0, null, this); + } + + public interface SelectIdentityKeyFragmentListener { + void onKeySelected(Long masterKeyId); + void onChangeListEmptyStatus(boolean isEmpty); + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/adapter/SelectIdentityKeyAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/adapter/SelectIdentityKeyAdapter.java new file mode 100644 index 000000000..66e5baee8 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/adapter/SelectIdentityKeyAdapter.java @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2016 Tobias Erthal + * Copyright (C) 2014-2016 Dominik Schürmann + * + * 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.remote.ui.adapter; + +import android.content.Context; +import android.database.Cursor; +import android.support.v4.content.ContextCompat; +import android.support.v7.widget.RecyclerView; +import android.text.format.DateUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import org.openintents.openpgp.util.OpenPgpUtils; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.ui.adapter.KeyCursorAdapter; +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; + + +public class SelectIdentityKeyAdapter extends KeyCursorAdapter { + private SelectSignKeyListener mListener; + + public SelectIdentityKeyAdapter(Context context, Cursor cursor) { + super(context, KeyCursor.wrap(cursor)); + } + + public void setListener(SelectSignKeyListener listener) { + mListener = listener; + } + + @Override + public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + return new SignKeyItemHolder(LayoutInflater.from(parent.getContext()) + .inflate(R.layout.select_identity_key_item, parent, false)); + } + + @Override + public void onBindViewHolder(RecyclerView.ViewHolder holder, KeyCursor cursor, String query) { + ((SignKeyItemHolder) holder).bind(cursor, query); + } + + private class SignKeyItemHolder extends RecyclerView.ViewHolder + implements View.OnClickListener { + + private TextView userIdText; + private TextView creationText; + private ImageView statusIcon; + + SignKeyItemHolder(View itemView) { + super(itemView); + itemView.setClickable(true); + itemView.setOnClickListener(this); + + userIdText = (TextView) itemView.findViewById(R.id.select_key_item_name); + creationText = (TextView) itemView.findViewById(R.id.select_key_item_creation); + statusIcon = (ImageView) itemView.findViewById(R.id.select_key_item_status_icon); + } + + public void bind(KeyCursor cursor, String query) { + Context context = itemView.getContext(); + + { // set name and stuff, common to both key types + String name = cursor.getName(); + if (name != null) { + userIdText.setText(context.getString(R.string.use_key, name)); + } else { + String email = cursor.getEmail(); + userIdText.setText(context.getString(R.string.use_key, email)); + } + } + + { // set edit button and status, specific by key type. Note: order is important! + int textColor; + if (cursor.isRevoked()) { + KeyFormattingUtils.setStatusImage( + context, + statusIcon, + null, + KeyFormattingUtils.State.REVOKED, + R.color.key_flag_gray + ); + + itemView.setEnabled(false); + statusIcon.setVisibility(View.VISIBLE); + textColor = ContextCompat.getColor(context, R.color.key_flag_gray); + } else if (cursor.isExpired()) { + KeyFormattingUtils.setStatusImage( + context, + statusIcon, + null, + KeyFormattingUtils.State.EXPIRED, + R.color.key_flag_gray + ); + + itemView.setEnabled(false); + statusIcon.setVisibility(View.VISIBLE); + textColor = ContextCompat.getColor(context, R.color.key_flag_gray); + } else { + itemView.setEnabled(true); + statusIcon.setImageResource(R.drawable.ic_vpn_key_grey_24dp); + textColor = FormattingUtils.getColorFromAttr(context, R.attr.colorText); + } + + userIdText.setTextColor(textColor); + + String dateTime = DateUtils.formatDateTime(context, + cursor.getCreationTime(), + DateUtils.FORMAT_SHOW_DATE + | DateUtils.FORMAT_SHOW_TIME + | DateUtils.FORMAT_SHOW_YEAR + | DateUtils.FORMAT_ABBREV_MONTH); + creationText.setText(context.getString(R.string.label_key_created, + dateTime)); + creationText.setTextColor(textColor); + creationText.setVisibility(View.VISIBLE); + } + + } + + @Override + public void onClick(View v) { + if (mListener != null) { + mListener.onSelectKeyItemClicked(getItemId()); + } + } + } + + public interface SelectSignKeyListener { + void onSelectKeyItemClicked(long masterKeyId); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteSelectIdKeyActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteSelectIdKeyActivity.java new file mode 100644 index 000000000..71ba94cec --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteSelectIdKeyActivity.java @@ -0,0 +1,403 @@ +/* + * 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.remote.ui.dialog; + + +import java.util.List; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.app.Dialog; +import android.arch.lifecycle.ViewModelProviders; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.res.Resources; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.Drawable.ConstantState; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.transition.Fade; +import android.support.transition.Transition; +import android.support.transition.TransitionListenerAdapter; +import android.support.transition.TransitionManager; +import android.support.v4.app.DialogFragment; +import android.support.v4.app.FragmentActivity; +import android.support.v4.content.res.ResourcesCompat; +import android.support.v4.graphics.drawable.DrawableCompat; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.RecyclerView.Adapter; +import android.text.format.DateUtils; +import android.view.ContextThemeWrapper; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.mikepenz.materialdrawer.util.KeyboardUtil; +import org.openintents.openpgp.util.OpenPgpApi; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.livedata.KeyInfoInteractor.KeyInfo; +import org.sufficientlysecure.keychain.remote.ui.dialog.RemoteSelectIdentityKeyPresenter.RemoteSelectIdentityKeyView; +import org.sufficientlysecure.keychain.ui.dialog.CustomAlertDialogBuilder; +import org.sufficientlysecure.keychain.ui.util.ThemeChanger; +import org.sufficientlysecure.keychain.ui.util.recyclerview.DividerItemDecoration; +import org.sufficientlysecure.keychain.ui.util.recyclerview.RecyclerItemClickListener; +import org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator; + + +public class RemoteSelectIdKeyActivity extends FragmentActivity { + public static final String EXTRA_PACKAGE_NAME = "package_name"; + public static final String EXTRA_USER_ID = "user_id"; + public static final String EXTRA_CURRENT_MASTER_KEY_ID = "current_master_key_id"; + + + private RemoteSelectIdentityKeyPresenter presenter; + + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + RemoteSelectIdViewModel viewModel = + ViewModelProviders.of(this).get(RemoteSelectIdViewModel.class); + + presenter = new RemoteSelectIdentityKeyPresenter(getBaseContext(), viewModel, this); + + KeyboardUtil.hideKeyboard(this); + + if (savedInstanceState == null) { + RemoteSelectIdentityKeyDialogFragment frag = new RemoteSelectIdentityKeyDialogFragment(); + frag.show(getSupportFragmentManager(), "requestKeyDialog"); + } + } + + @Override + protected void onStart() { + super.onStart(); + + Intent intent = getIntent(); + String userId = intent.getStringExtra(EXTRA_USER_ID); + String packageName = intent.getStringExtra(EXTRA_PACKAGE_NAME); + + presenter.setupFromIntentData(packageName, userId); + } + + public static class RemoteSelectIdentityKeyDialogFragment extends DialogFragment { + private RemoteSelectIdentityKeyPresenter presenter; + private RemoteSelectIdentityKeyView mvpView; + + private RecyclerView keyChoiceList; + private View buttonKeyListCancel; + private View buttonNoKeysNew; + private View buttonExplBack; + private View buttonExplGotIt; + private View buttonGenOkBack; + private View buttonGenOkFinish; + private View buttonNoKeysCancel; + private View buttonNoKeysExisting; + private View buttonKeyListOther; + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final Activity activity = getActivity(); + + ContextThemeWrapper theme = ThemeChanger.getDialogThemeWrapper(activity); + CustomAlertDialogBuilder alert = new CustomAlertDialogBuilder(theme); + + LayoutInflater layoutInflater = LayoutInflater.from(theme); + @SuppressLint("InflateParams") + ViewGroup view = (ViewGroup) layoutInflater.inflate(R.layout.api_select_identity_key, null, false); + alert.setView(view); + + buttonKeyListCancel = view.findViewById(R.id.button_key_list_cancel); + buttonKeyListOther = view.findViewById(R.id.button_key_list_other); + + buttonNoKeysNew = view.findViewById(R.id.button_no_keys_new); + buttonNoKeysExisting = view.findViewById(R.id.button_no_keys_existing); + buttonNoKeysCancel = view.findViewById(R.id.button_no_keys_cancel); + + buttonExplBack = view.findViewById(R.id.button_expl_back); + buttonExplGotIt = view.findViewById(R.id.button_expl_got_it); + + buttonGenOkBack = view.findViewById(R.id.button_genok_back); + buttonGenOkFinish = view.findViewById(R.id.button_genok_finish); + + keyChoiceList = view.findViewById(R.id.identity_key_list); + keyChoiceList.setLayoutManager(new LinearLayoutManager(activity)); + keyChoiceList.addItemDecoration( + new DividerItemDecoration(activity, DividerItemDecoration.VERTICAL_LIST, true)); + + setupListenersForPresenter(); + mvpView = createMvpView(view, layoutInflater); + + return alert.create(); + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + presenter = ((RemoteSelectIdKeyActivity) getActivity()).presenter; + presenter.setView(mvpView); + } + + @Override + public void onCancel(DialogInterface dialog) { + super.onCancel(dialog); + + if (presenter != null) { + presenter.onDialogCancel(); + } + } + + @Override + public void onDismiss(DialogInterface dialog) { + super.onDismiss(dialog); + + if (presenter != null) { + presenter.setView(null); + presenter = null; + } + } + + @NonNull + private RemoteSelectIdentityKeyView createMvpView(final ViewGroup rootView, LayoutInflater layoutInflater) { + final ImageView iconClientApp = rootView.findViewById(R.id.icon_client_app); + final KeyChoiceAdapter keyChoiceAdapter = new KeyChoiceAdapter(layoutInflater, getResources()); + final TextView titleText = rootView.findViewById(R.id.text_title_select_key); + final TextView addressText = rootView.findViewById(R.id.text_user_id); + final ToolableViewAnimator layoutAnimator = rootView.findViewById(R.id.layout_animator); + keyChoiceList.setAdapter(keyChoiceAdapter); + + return new RemoteSelectIdentityKeyView() { + @Override + public void finishAndReturn(long masterKeyId) { + FragmentActivity activity = getActivity(); + if (activity == null) { + return; + } + + Intent resultData = new Intent(); + resultData.putExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, masterKeyId); + activity.setResult(RESULT_OK, resultData); + activity.finish(); + } + + @Override + public void finishAsCancelled() { + FragmentActivity activity = getActivity(); + if (activity == null) { + return; + } + + activity.setResult(RESULT_CANCELED); + activity.finish(); + } + + @Override + public void setTitleClientIconAndName(Drawable drawable, CharSequence name) { + titleText.setText(getString(R.string.title_select_key, name)); + iconClientApp.setImageDrawable(drawable); + setSelectionIcons(drawable); + } + + private void setSelectionIcons(Drawable drawable) { + ConstantState constantState = drawable.getConstantState(); + if (constantState == null) { + return; + } + + Resources resources = getResources(); + Drawable iconSelected = constantState.newDrawable(resources); + Drawable iconUnselected = constantState.newDrawable(resources); + DrawableCompat.setTint(iconUnselected.mutate(), ResourcesCompat.getColor(resources, R.color.md_grey_600, null)); + + keyChoiceAdapter.setSelectionDrawables(iconSelected, iconUnselected); + } + + @Override + public void setAddressText(String text) { + addressText.setText(text); + } + + @Override + public void showLayoutEmpty() { + layoutAnimator.setDisplayedChildId(R.id.select_key_layout_empty); + } + + @Override + public void showLayoutSelectNoKeys() { + layoutAnimator.setDisplayedChildId(R.id.select_key_layout_no_keys); + } + + @Override + public void showLayoutSelectKeyList() { + layoutAnimator.setDisplayedChildId(R.id.select_key_layout_key_list); + } + + @Override + public void showLayoutImportExplanation() { + layoutAnimator.setDisplayedChildId(R.id.select_key_layout_import_expl); + } + + @Override + public void showLayoutGenerateProgress() { + layoutAnimator.setDisplayedChildId(R.id.select_key_layout_generate_progress); + } + + @Override + public void showLayoutGenerateOk() { + layoutAnimator.setDisplayedChildId(R.id.select_key_layout_generate_ok); + } + + @Override + public void setKeyListData(List data) { + keyChoiceAdapter.setData(data); + } + + @Override + public void highlightKey(int position) { + Transition transition = new Fade().setDuration(450) + .addTarget(LinearLayout.class) + .addTarget(ImageView.class) + .addListener(new TransitionListenerAdapter() { + @Override + public void onTransitionEnd(@NonNull Transition transition) { + presenter.onHighlightFinished(); + } + }); + TransitionManager.beginDelayedTransition(rootView, transition); + + buttonKeyListOther.setVisibility(View.INVISIBLE); + buttonKeyListCancel.setVisibility(View.INVISIBLE); + keyChoiceAdapter.setActiveItem(position); + } + }; + } + + private void setupListenersForPresenter() { + buttonKeyListOther.setOnClickListener(view -> presenter.onClickKeyListOther()); + buttonKeyListCancel.setOnClickListener(view -> presenter.onClickKeyListCancel()); + + buttonNoKeysNew.setOnClickListener(view -> presenter.onClickNoKeysGenerate()); + buttonNoKeysExisting.setOnClickListener(view -> presenter.onClickNoKeysExisting()); + buttonNoKeysCancel.setOnClickListener(view -> presenter.onClickNoKeysCancel()); + + buttonExplBack.setOnClickListener(view -> presenter.onClickExplanationBack()); + buttonExplGotIt.setOnClickListener(view -> presenter.onClickExplanationGotIt()); + + buttonGenOkBack.setOnClickListener(view -> presenter.onClickGenerateOkBack()); + buttonGenOkFinish.setOnClickListener(view -> presenter.onClickGenerateOkFinish()); + + keyChoiceList.addOnItemTouchListener(new RecyclerItemClickListener(getContext(), + (view, position) -> presenter.onKeyItemClick(position))); + } + } + + private static class KeyChoiceAdapter extends Adapter { + private final LayoutInflater layoutInflater; + private final Resources resources; + private List data; + private Drawable iconUnselected; + private Drawable iconSelected; + private Integer activeItem; + + KeyChoiceAdapter(LayoutInflater layoutInflater, Resources resources) { + this.layoutInflater = layoutInflater; + this.resources = resources; + } + + @Override + public KeyChoiceViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View keyChoiceItemView = layoutInflater.inflate(R.layout.api_select_identity_item, parent, false); + return new KeyChoiceViewHolder(keyChoiceItemView); + } + + public void setActiveItem(Integer activeItem) { + if (this.activeItem != null) { + notifyItemChanged(activeItem); + } + this.activeItem = activeItem; + if (this.activeItem != null) { + notifyItemChanged(activeItem); + } + } + + @Override + public void onBindViewHolder(KeyChoiceViewHolder holder, int position) { + KeyInfo keyInfo = data.get(position); + Drawable icon = (activeItem != null && position == activeItem) ? iconSelected : iconUnselected; + holder.bind(keyInfo, icon); + } + + @Override + public int getItemCount() { + return data != null ? data.size() : 0; + } + + public void setData(List data) { + this.data = data; + notifyDataSetChanged(); + } + + void setSelectionDrawables(Drawable iconSelected, Drawable iconUnselected) { + this.iconSelected = iconSelected; + this.iconUnselected = iconUnselected; + + notifyDataSetChanged(); + } + } + + private static class KeyChoiceViewHolder extends RecyclerView.ViewHolder { + private final TextView vName; + private final TextView vCreation = (TextView) itemView.findViewById(R.id.key_list_item_creation); + private final ImageView vIcon; + + KeyChoiceViewHolder(View itemView) { + super(itemView); + + vName = itemView.findViewById(R.id.key_list_item_name); + vIcon = itemView.findViewById(R.id.key_list_item_icon); + } + + void bind(KeyInfo keyInfo, Drawable selectionIcon) { + Context context = vCreation.getContext(); + + String name = keyInfo.getName(); + if (name != null) { + vName.setText(context.getString(R.string.use_key, name)); + } else { + String email = keyInfo.getEmail(); + vName.setText(context.getString(R.string.use_key, email)); + } + + String dateTime = DateUtils.formatDateTime(context, keyInfo.getCreationDate(), + DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME | + DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_ABBREV_MONTH); + vCreation.setText(context.getString(R.string.label_key_created, dateTime)); + + vIcon.setImageDrawable(selectionIcon); + } + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteSelectIdViewModel.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteSelectIdViewModel.java new file mode 100644 index 000000000..e2325d4ae --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteSelectIdViewModel.java @@ -0,0 +1,31 @@ +package org.sufficientlysecure.keychain.remote.ui.dialog; + + +import android.arch.lifecycle.ViewModel; +import android.content.Context; + +import org.sufficientlysecure.keychain.livedata.KeyInfoLiveData; +import org.sufficientlysecure.keychain.livedata.PgpKeyGenerationLiveData; + + +public class RemoteSelectIdViewModel extends ViewModel { + + private KeyInfoLiveData keyInfo; + private PgpKeyGenerationLiveData keyGenerationData; + public long selectedMasterKeyId; + + public KeyInfoLiveData getKeyInfo(Context context) { + if (keyInfo == null) { + keyInfo = new KeyInfoLiveData(context, context.getContentResolver()); + } + return keyInfo; + } + + public PgpKeyGenerationLiveData getKeyGenerationLiveData(Context context) { + if (keyGenerationData == null) { + keyGenerationData = new PgpKeyGenerationLiveData(context); + } + return keyGenerationData; + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteSelectIdentityKeyPresenter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteSelectIdentityKeyPresenter.java new file mode 100644 index 000000000..716d3e354 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteSelectIdentityKeyPresenter.java @@ -0,0 +1,191 @@ +/* + * 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.remote.ui.dialog; + + +import java.util.List; + +import android.arch.lifecycle.LifecycleOwner; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.graphics.drawable.Drawable; + +import org.openintents.openpgp.util.OpenPgpUtils; +import org.openintents.openpgp.util.OpenPgpUtils.UserId; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.livedata.KeyInfoInteractor.KeyInfo; +import org.sufficientlysecure.keychain.livedata.KeyInfoInteractor.KeySelector; +import org.sufficientlysecure.keychain.operations.results.PgpEditKeyResult; +import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; +import org.sufficientlysecure.keychain.service.SaveKeyringParcel; +import timber.log.Timber; + + +class RemoteSelectIdentityKeyPresenter { + private final PackageManager packageManager; + private final Context context; + private final RemoteSelectIdViewModel viewModel; + + + private UserId userId; + + private RemoteSelectIdentityKeyView view; + private List keyInfoData; + private long masterKeyId; + + + RemoteSelectIdentityKeyPresenter(Context context, RemoteSelectIdViewModel viewModel, LifecycleOwner lifecycleOwner) { + this.context = context; + this.viewModel = viewModel; + + packageManager = context.getPackageManager(); + + viewModel.getKeyGenerationLiveData(context).observe(lifecycleOwner, this::onChangeKeyGeneration); + viewModel.getKeyInfo(context).observe(lifecycleOwner, this::onChangeKeyInfoData); + } + + public void setView(RemoteSelectIdentityKeyView view) { + this.view = view; + } + + void setupFromIntentData(String packageName, String rawUserId) { + try { + setPackageInfo(packageName); + } catch (NameNotFoundException e) { + Timber.e(e, "Unable to find info of calling app!"); + view.finishAsCancelled(); + return; + } + + this.userId = OpenPgpUtils.splitUserId(rawUserId); + view.setAddressText(userId.email); + + viewModel.getKeyInfo(context).setKeySelector(KeySelector.createOnlySecret( + KeyRings.buildUnifiedKeyRingsFindByUserIdUri(userId.email), null)); + } + + private void setPackageInfo(String packageName) throws NameNotFoundException { + ApplicationInfo applicationInfo = packageManager.getApplicationInfo(packageName, 0); + Drawable appIcon = packageManager.getApplicationIcon(applicationInfo); + CharSequence appLabel = packageManager.getApplicationLabel(applicationInfo); + + view.setTitleClientIconAndName(appIcon, appLabel); + } + + private void onChangeKeyInfoData(List data) { + keyInfoData = data; + goToSelectLayout(); + } + + private void goToSelectLayout() { + if (keyInfoData == null) { + view.showLayoutEmpty(); + } else if (keyInfoData.isEmpty()) { + view.showLayoutSelectNoKeys(); + } else { + view.setKeyListData(keyInfoData); + view.showLayoutSelectKeyList(); + } + } + + private void onChangeKeyGeneration(PgpEditKeyResult pgpEditKeyResult) { + viewModel.getKeyGenerationLiveData(context).setSaveKeyringParcel(null); + if (pgpEditKeyResult == null) { + return; + } + + view.showLayoutGenerateOk(); + } + + void onDialogCancel() { + view.finishAsCancelled(); + } + + void onClickKeyListOther() { + view.showLayoutImportExplanation(); + } + + void onClickKeyListCancel() { + view.finishAndReturn(Constants.key.none); + } + + void onClickNoKeysGenerate() { + view.showLayoutGenerateProgress(); + + SaveKeyringParcel.Builder builder = SaveKeyringParcel.buildNewKeyringParcel(); + Constants.addDefaultSubkeys(builder); + builder.addUserId(userId.email); + + viewModel.getKeyGenerationLiveData(context).setSaveKeyringParcel(builder.build()); + } + + void onClickNoKeysExisting() { + view.showLayoutImportExplanation(); + } + + void onClickNoKeysCancel() { + view.finishAndReturn(Constants.key.none); + } + + void onKeyItemClick(int position) { + viewModel.selectedMasterKeyId = keyInfoData.get(position).getMasterKeyId(); + view.highlightKey(position); + } + + void onClickExplanationBack() { + goToSelectLayout(); + } + + void onClickExplanationGotIt() { + view.finishAsCancelled(); + } + + void onClickGenerateOkBack() { + view.showLayoutSelectNoKeys(); + } + + void onClickGenerateOkFinish() { + // saveKey + // view.finishAndReturn + } + + void onHighlightFinished() { + view.finishAndReturn(viewModel.selectedMasterKeyId); + } + + interface RemoteSelectIdentityKeyView { + void finishAndReturn(long masterKeyId); + void finishAsCancelled(); + + void setAddressText(String text); + void setTitleClientIconAndName(Drawable drawable, CharSequence name); + + void showLayoutEmpty(); + void showLayoutSelectNoKeys(); + void showLayoutSelectKeyList(); + void showLayoutImportExplanation(); + void showLayoutGenerateProgress(); + void showLayoutGenerateOk(); + + void setKeyListData(List data); + + void highlightKey(int position); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/RecyclerFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/RecyclerFragment.java index 9cec9d778..171db0849 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/RecyclerFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/RecyclerFragment.java @@ -19,6 +19,7 @@ package org.sufficientlysecure.keychain.ui.base; import android.content.Context; import android.os.Bundle; import android.os.Handler; +import android.support.annotation.LayoutRes; import android.support.v4.app.Fragment; import android.support.v7.widget.RecyclerView; import android.view.Gravity; @@ -136,8 +137,6 @@ public class RecyclerFragment extends Fragment { RecyclerView listView = new RecyclerView(context); listView.setId(INTERNAL_LIST_VIEW_ID); - int padding = FormattingUtils.dpToPx(context, 8); - listView.setPadding(padding, 0, padding, 0); listView.setClipToPadding(false); listContainer.addView(listView, new FrameLayout.LayoutParams( diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/loader/AsyncTaskLiveData.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/loader/AsyncTaskLiveData.java index 9cfda21fb..13631e7ff 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/loader/AsyncTaskLiveData.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/loader/AsyncTaskLiveData.java @@ -23,16 +23,16 @@ public abstract class AsyncTaskLiveData extends LiveData { @Nullable private CancellationSignal cancellationSignal; - AsyncTaskLiveData(@NonNull Context context, @Nullable Uri observedUri) { + protected AsyncTaskLiveData(@NonNull Context context, @Nullable Uri observedUri) { super(); this.context = context; this.observedUri = observedUri; this.observer = new ForceLoadContentObserver(); } - abstract T asyncLoadData(); + protected abstract T asyncLoadData(); - private void loadDataInBackground() { + protected void updateDataInBackground() { new AsyncTask() { @Override protected T doInBackground(Void... params) { @@ -66,7 +66,7 @@ public abstract class AsyncTaskLiveData extends LiveData { protected void onActive() { T value = getValue(); if (value == null) { - loadDataInBackground(); + updateDataInBackground(); } if (observedUri != null) { @@ -105,7 +105,7 @@ public abstract class AsyncTaskLiveData extends LiveData { @Override public void onChange(boolean selfChange) { - loadDataInBackground(); + updateDataInBackground(); } } diff --git a/OpenKeychain/src/main/res/layout/api_select_identity_item.xml b/OpenKeychain/src/main/res/layout/api_select_identity_item.xml new file mode 100644 index 000000000..7dfcac1b6 --- /dev/null +++ b/OpenKeychain/src/main/res/layout/api_select_identity_item.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + diff --git a/OpenKeychain/src/main/res/layout/api_select_identity_key.xml b/OpenKeychain/src/main/res/layout/api_select_identity_key.xml new file mode 100644 index 000000000..73d6e2fc7 --- /dev/null +++ b/OpenKeychain/src/main/res/layout/api_select_identity_key.xml @@ -0,0 +1,431 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +