org.sufficientlysecure.keychain.ui.DecryptListFragment.java Source code

Java tutorial

Introduction

Here is the source code for org.sufficientlysecure.keychain.ui.DecryptListFragment.java

Source

/*
 * Copyright (C) 2014 Dominik Schrmann <dominik@dominikschuermann.de>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.sufficientlysecure.keychain.ui;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;

import android.Manifest;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.ClipDescription;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.LabeledIntent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.graphics.Bitmap;
import android.graphics.Point;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.support.v4.content.ContextCompat;
import android.support.v7.widget.DefaultItemAnimator;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnLongClickListener;
import android.view.ViewGroup;
import android.webkit.MimeTypeMap;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.PopupMenu;
import android.widget.PopupMenu.OnDismissListener;
import android.widget.PopupMenu.OnMenuItemClickListener;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.ViewAnimator;

import com.cocosw.bottomsheet.BottomSheet;

import org.openintents.openpgp.OpenPgpMetadata;
import org.openintents.openpgp.OpenPgpSignatureResult;
import org.sufficientlysecure.keychain.BuildConfig;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing;
import org.sufficientlysecure.keychain.operations.results.ImportKeyResult;
import org.sufficientlysecure.keychain.operations.results.InputDataResult;
import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyInputParcel;
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
import org.sufficientlysecure.keychain.service.ImportKeyringParcel;
import org.sufficientlysecure.keychain.service.InputDataParcel;
import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper;
import org.sufficientlysecure.keychain.ui.base.QueueingCryptoOperationFragment;
// this import NEEDS to be above the ViewModel AND SubViewHolder one, or it won't compile! (as of 16.09.15)
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils.StatusHolder;
import org.sufficientlysecure.keychain.ui.DecryptListFragment.ViewHolder.SubViewHolder;
import org.sufficientlysecure.keychain.ui.DecryptListFragment.DecryptFilesAdapter.ViewModel;
import org.sufficientlysecure.keychain.ui.adapter.SpacesItemDecoration;
import org.sufficientlysecure.keychain.ui.util.FormattingUtils;
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.util.FileHelper;
import org.sufficientlysecure.keychain.util.Log;
import org.sufficientlysecure.keychain.util.ParcelableHashMap;
import org.sufficientlysecure.keychain.keyimport.ParcelableHkpKeyserver;
import org.sufficientlysecure.keychain.util.Preferences;

/**
 * Displays a list of decrypted inputs.
 * <p/>
 * This class has a complex control flow to manage its input URIs. Each URI
 * which is in mInputUris is also in exactly one of mPendingInputUris,
 * mCancelledInputUris, mCurrentInputUri, or a key in mInputDataResults.
 * <p/>
 * Processing of URIs happens using a looping approach:
 * - There is always exactly one method running which works on mCurrentInputUri
 * - Processing starts in cryptoOperation(), which pops a new mCurrentInputUri
 * from the list of mPendingInputUris.
 * - Once a mCurrentInputUri is finished processing, it should be set to null and
 * control handed back to cryptoOperation()
 * - Control flow can move through asynchronous calls, and resume in callbacks
 * like onActivityResult() or onPermissionRequestResult().
 */
public class DecryptListFragment extends QueueingCryptoOperationFragment<InputDataParcel, InputDataResult>
        implements OnMenuItemClickListener {

    public static final String ARG_INPUT_URIS = "input_uris";
    public static final String ARG_OUTPUT_URIS = "output_uris";
    public static final String ARG_CANCELLED_URIS = "cancelled_uris";
    public static final String ARG_RESULTS = "results";
    public static final String ARG_CAN_DELETE = "can_delete";

    private static final int REQUEST_CODE_OUTPUT = 0x00007007;
    private static final int REQUEST_PERMISSION_READ_EXTERNAL_STORAGE = 12;

    private ArrayList<Uri> mInputUris;
    private HashMap<Uri, InputDataResult> mInputDataResults;
    private ArrayList<Uri> mPendingInputUris;
    private ArrayList<Uri> mCancelledInputUris;

    private Uri mCurrentInputUri;
    private boolean mCanDelete;

    private DecryptFilesAdapter mAdapter;
    private Uri mCurrentSaveFileUri;

    /**
     * Creates new instance of this fragment
     */
    public static DecryptListFragment newInstance(@NonNull ArrayList<Uri> uris, boolean canDelete) {
        DecryptListFragment frag = new DecryptListFragment();

        Bundle args = new Bundle();
        args.putParcelableArrayList(ARG_INPUT_URIS, uris);
        args.putBoolean(ARG_CAN_DELETE, canDelete);
        frag.setArguments(args);

        return frag;
    }

    public DecryptListFragment() {
        super(null);
    }

    /**
     * Inflate the layout for this fragment
     */
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.decrypt_files_list_fragment, container, false);

        RecyclerView vFilesList = (RecyclerView) view.findViewById(R.id.decrypted_files_list);

        vFilesList.addItemDecoration(new SpacesItemDecoration(FormattingUtils.dpToPx(getActivity(), 4)));
        vFilesList.setHasFixedSize(true);
        vFilesList.setLayoutManager(new LinearLayoutManager(getActivity()));
        vFilesList.setItemAnimator(new DefaultItemAnimator() {
            @Override
            public boolean canReuseUpdatedViewHolder(@NonNull RecyclerView.ViewHolder viewHolder) {
                return true;
            }
        });

        mAdapter = new DecryptFilesAdapter();
        vFilesList.setAdapter(mAdapter);

        return view;
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);

        outState.putParcelableArrayList(ARG_INPUT_URIS, mInputUris);

        HashMap<Uri, InputDataResult> results = new HashMap<>(mInputUris.size());
        for (Uri uri : mInputUris) {
            if (mPendingInputUris.contains(uri)) {
                continue;
            }
            InputDataResult result = mAdapter.getItemResult(uri);
            if (result != null) {
                results.put(uri, result);
            }
        }

        outState.putParcelable(ARG_RESULTS, new ParcelableHashMap<>(results));
        outState.putParcelable(ARG_OUTPUT_URIS, new ParcelableHashMap<>(mInputDataResults));
        outState.putParcelableArrayList(ARG_CANCELLED_URIS, mCancelledInputUris);
        outState.putBoolean(ARG_CAN_DELETE, mCanDelete);

        // this does not save mCurrentInputUri - if anything is being
        // processed at fragment recreation time, the operation in
        // progress will be lost!
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);

        Bundle args = savedInstanceState != null ? savedInstanceState : getArguments();

        ArrayList<Uri> inputUris = getArguments().getParcelableArrayList(ARG_INPUT_URIS);
        ArrayList<Uri> cancelledUris = args.getParcelableArrayList(ARG_CANCELLED_URIS);
        ParcelableHashMap<Uri, InputDataResult> results = args.getParcelable(ARG_RESULTS);

        mCanDelete = args.getBoolean(ARG_CAN_DELETE, false);

        displayInputUris(inputUris, cancelledUris, results != null ? results.getMap() : null);
    }

    private void displayInputUris(ArrayList<Uri> inputUris, ArrayList<Uri> cancelledUris,
            HashMap<Uri, InputDataResult> results) {

        mInputUris = inputUris;
        mCurrentInputUri = null;
        mInputDataResults = results != null ? results : new HashMap<Uri, InputDataResult>(inputUris.size());
        mCancelledInputUris = cancelledUris != null ? cancelledUris : new ArrayList<Uri>();

        mPendingInputUris = new ArrayList<>();

        for (final Uri uri : inputUris) {
            mAdapter.add(uri);

            boolean uriIsCancelled = mCancelledInputUris.contains(uri);
            if (uriIsCancelled) {
                mAdapter.setCancelled(uri, true);
                continue;
            }

            boolean uriHasResult = results != null && results.containsKey(uri);
            if (uriHasResult) {
                processResult(uri);
                continue;
            }

            mPendingInputUris.add(uri);
        }

        // check if there are any pending input uris
        cryptoOperation();
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        switch (requestCode) {
        case REQUEST_CODE_OUTPUT: {
            // This happens after output file was selected, so start our operation
            if (resultCode == Activity.RESULT_OK && data != null) {
                Uri saveUri = data.getData();
                saveFile(saveUri);
                mCurrentInputUri = null;
            }
            return;
        }

        default: {
            super.onActivityResult(requestCode, resultCode, data);
        }
        }
    }

    @TargetApi(VERSION_CODES.KITKAT)
    private void saveFileDialog(InputDataResult result, int index) {

        Activity activity = getActivity();
        if (activity == null) {
            return;
        }

        OpenPgpMetadata metadata = result.mMetadata.get(index);
        mCurrentSaveFileUri = result.getOutputUris().get(index);

        String filename = metadata.getFilename();
        if (TextUtils.isEmpty(filename)) {
            String ext = MimeTypeMap.getSingleton().getExtensionFromMimeType(metadata.getMimeType());
            filename = "decrypted" + (ext != null ? "." + ext : "");
        }

        // requires >=kitkat
        FileHelper.saveDocument(this, filename, metadata.getMimeType(), REQUEST_CODE_OUTPUT);
    }

    private void saveFile(Uri saveUri) {
        if (mCurrentSaveFileUri == null) {
            return;
        }

        Uri decryptedFileUri = mCurrentSaveFileUri;
        mCurrentInputUri = null;

        hideKeyboard();

        Activity activity = getActivity();
        if (activity == null) {
            return;
        }

        try {
            FileHelper.copyUriData(activity, decryptedFileUri, saveUri);
            Notify.create(activity, R.string.file_saved, Style.OK).show();
        } catch (IOException e) {
            Log.e(Constants.TAG, "error saving file", e);
            Notify.create(activity, R.string.error_saving_file, Style.ERROR).show();
        }
    }

    @Override
    public boolean onCryptoSetProgress(String msg, int progress, int max) {
        mAdapter.setProgress(mCurrentInputUri, progress, max, msg);
        return true;
    }

    @Override
    public void onQueuedOperationError(InputDataResult result) {
        final Uri uri = mCurrentInputUri;
        mCurrentInputUri = null;

        Activity activity = getActivity();
        if (activity != null && "com.fsck.k9.attachmentprovider".equals(uri.getHost())) {
            Toast.makeText(getActivity(), R.string.error_reading_k9, Toast.LENGTH_LONG).show();
        }

        mAdapter.addResult(uri, result);

        cryptoOperation();
    }

    @Override
    public void onQueuedOperationSuccess(InputDataResult result) {
        Uri uri = mCurrentInputUri;
        mCurrentInputUri = null;

        Activity activity = getActivity();

        boolean isSingleInput = mInputDataResults.isEmpty() && mPendingInputUris.isEmpty();
        if (isSingleInput) {

            // there is always at least one mMetadata object, so we know this is >= 1 already
            boolean isSingleMetadata = result.mMetadata.size() == 1;
            OpenPgpMetadata metadata = result.mMetadata.get(0);
            boolean isText = "text/plain".equals(metadata.getMimeType());
            boolean isOverSized = metadata.getOriginalSize() > Constants.TEXT_LENGTH_LIMIT;

            if (isSingleMetadata && isText && !isOverSized) {
                Intent displayTextIntent = new Intent(activity, DisplayTextActivity.class)
                        .setDataAndType(result.mOutputUris.get(0), "text/plain")
                        .putExtra(DisplayTextActivity.EXTRA_RESULT, result.mDecryptVerifyResult)
                        .putExtra(DisplayTextActivity.EXTRA_METADATA, metadata);
                activity.startActivity(displayTextIntent);
                activity.finish();
                return;
            }

        }

        mInputDataResults.put(uri, result);
        processResult(uri);

        cryptoOperation();
    }

    @Override
    public void onCryptoOperationCancelled() {
        super.onCryptoOperationCancelled();

        final Uri uri = mCurrentInputUri;
        mCurrentInputUri = null;

        mCancelledInputUris.add(uri);
        mAdapter.setCancelled(uri, true);

        cryptoOperation();

    }

    HashMap<Uri, Drawable> mIconCache = new HashMap<>();

    private void processResult(final Uri uri) {

        new AsyncTask<Void, Void, Void>() {
            @Override
            protected Void doInBackground(Void... params) {

                InputDataResult result = mInputDataResults.get(uri);

                Context context = getActivity();
                if (context == null) {
                    return null;
                }

                for (int i = 0; i < result.getOutputUris().size(); i++) {

                    Uri outputUri = result.getOutputUris().get(i);
                    if (mIconCache.containsKey(outputUri)) {
                        continue;
                    }

                    OpenPgpMetadata metadata = result.mMetadata.get(i);
                    String type = metadata.getMimeType();

                    Drawable icon = null;

                    if (ClipDescription.compareMimeTypes(type, "text/plain")) {
                        // noinspection deprecation, this should be called from Context, but not available in minSdk
                        icon = getResources().getDrawable(R.drawable.ic_chat_black_24dp);
                    } else if (ClipDescription.compareMimeTypes(type, "application/octet-stream")) {
                        // icons for this are just confusing
                        // noinspection deprecation, this should be called from Context, but not available in minSdk
                        icon = getResources().getDrawable(R.drawable.ic_doc_generic_am);
                    } else if (ClipDescription.compareMimeTypes(type, Constants.MIME_TYPE_KEYS)) {
                        // noinspection deprecation, this should be called from Context, but not available in minSdk
                        icon = getResources().getDrawable(R.drawable.ic_key_plus_grey600_24dp);
                    } else if (ClipDescription.compareMimeTypes(type, "image/*")) {
                        int px = FormattingUtils.dpToPx(context, 32);
                        Bitmap bitmap = FileHelper.getThumbnail(context, outputUri, new Point(px, px));
                        icon = new BitmapDrawable(context.getResources(), bitmap);
                    }

                    if (icon == null) {
                        final Intent intent = new Intent(Intent.ACTION_VIEW);
                        intent.setDataAndType(outputUri, type);

                        final List<ResolveInfo> matches = context.getPackageManager().queryIntentActivities(intent,
                                0);
                        // noinspection LoopStatementThatDoesntLoop
                        for (ResolveInfo match : matches) {
                            icon = match.loadIcon(getActivity().getPackageManager());
                            break;
                        }
                    }

                    if (icon != null) {
                        mIconCache.put(outputUri, icon);
                    }

                }

                return null;

            }

            @Override
            protected void onPostExecute(Void v) {
                InputDataResult result = mInputDataResults.get(uri);
                mAdapter.addResult(uri, result);
            }
        }.execute();

    }

    public void retryUri(Uri uri) {

        // never interrupt running operations!
        if (mCurrentInputUri != null) {
            return;
        }

        // un-cancel this one
        mCancelledInputUris.remove(uri);
        mInputDataResults.remove(uri);
        mPendingInputUris.add(uri);
        mAdapter.resetItemData(uri);

        // check if there are any pending input uris
        cryptoOperation();
    }

    public void displayBottomSheet(final InputDataResult result, final int index) {

        Activity activity = getActivity();
        if (activity == null) {
            return;
        }

        new BottomSheet.Builder(activity).sheet(R.menu.decrypt_bottom_sheet)
                .listener(new MenuItem.OnMenuItemClickListener() {
                    @Override
                    public boolean onMenuItemClick(MenuItem item) {
                        switch (item.getItemId()) {
                        case R.id.decrypt_open:
                            displayWithViewIntent(result, index, false, true);
                            break;
                        case R.id.decrypt_share:
                            displayWithViewIntent(result, index, true, true);
                            break;
                        case R.id.decrypt_save:
                            // only inside the menu xml for Android >= 4.4
                            saveFileDialog(result, index);
                            break;
                        }
                        return false;
                    }
                }).grid().show();

    }

    public void displayWithViewIntent(InputDataResult result, int index, boolean share, boolean forceChooser) {
        Activity activity = getActivity();
        if (activity == null) {
            return;
        }

        Uri outputUri = result.getOutputUris().get(index);
        OpenPgpMetadata metadata = result.mMetadata.get(index);

        // text/plain is a special case where we extract the uri content into
        // the EXTRA_TEXT extra ourselves, and display a chooser which includes
        // OpenKeychain's internal viewer
        if ("text/plain".equals(metadata.getMimeType())) {

            if (share) {
                try {
                    String plaintext = FileHelper.readTextFromUri(activity, outputUri, null);

                    Intent intent = new Intent(Intent.ACTION_SEND);
                    intent.setType("text/plain");
                    intent.putExtra(Intent.EXTRA_TEXT, plaintext);

                    Intent chooserIntent = Intent.createChooser(intent, getString(R.string.intent_share));
                    startActivity(chooserIntent);

                } catch (IOException e) {
                    Notify.create(activity, R.string.error_preparing_data, Style.ERROR).show();
                }

                return;
            }

            Intent intent = new Intent();
            intent.setAction(Intent.ACTION_VIEW);
            intent.setDataAndType(outputUri, "text/plain");

            if (forceChooser) {

                LabeledIntent internalIntent = new LabeledIntent(
                        new Intent(intent).setClass(activity, DisplayTextActivity.class)
                                .putExtra(DisplayTextActivity.EXTRA_RESULT, result.mDecryptVerifyResult)
                                .putExtra(DisplayTextActivity.EXTRA_METADATA, metadata),
                        BuildConfig.APPLICATION_ID, R.string.view_internal, R.mipmap.ic_launcher);

                Intent chooserIntent = Intent.createChooser(intent, getString(R.string.intent_show));
                chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, new Parcelable[] { internalIntent });

                startActivity(chooserIntent);

            } else {

                intent.setClass(activity, DisplayTextActivity.class);
                intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
                intent.putExtra(DisplayTextActivity.EXTRA_RESULT, result.mDecryptVerifyResult);
                intent.putExtra(DisplayTextActivity.EXTRA_METADATA, metadata);
                startActivity(intent);

            }

        } else {

            Intent intent;
            if (share) {
                intent = new Intent(Intent.ACTION_SEND);
                intent.setType(metadata.getMimeType());
                intent.putExtra(Intent.EXTRA_STREAM, outputUri);
            } else {
                intent = new Intent(Intent.ACTION_VIEW);
                intent.setDataAndType(outputUri, metadata.getMimeType());

                if (!forceChooser && Constants.MIME_TYPE_KEYS.equals(metadata.getMimeType())) {
                    // bind Intent to this OpenKeychain, don't allow other apps to intercept here!
                    intent.setPackage(getActivity().getPackageName());
                }
            }

            intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);

            Intent chooserIntent = Intent.createChooser(intent, getString(R.string.intent_show));
            chooserIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);

            if (!share && ClipDescription.compareMimeTypes(metadata.getMimeType(), "text/*")) {
                LabeledIntent internalIntent = new LabeledIntent(
                        new Intent(intent).setClass(activity, DisplayTextActivity.class)
                                .putExtra(DisplayTextActivity.EXTRA_RESULT, result.mDecryptVerifyResult)
                                .putExtra(DisplayTextActivity.EXTRA_METADATA, metadata),
                        BuildConfig.APPLICATION_ID, R.string.view_internal, R.mipmap.ic_launcher);
                chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, new Parcelable[] { internalIntent });
            }

            startActivity(chooserIntent);
        }

    }

    @Override
    public InputDataParcel createOperationInput() {

        Activity activity = getActivity();
        if (activity == null) {
            return null;
        }

        if (mCurrentInputUri == null) {
            if (mPendingInputUris.isEmpty()) {
                // nothing left to do
                return null;
            }

            mCurrentInputUri = mPendingInputUris.remove(0);
        }

        Log.d(Constants.TAG, "mCurrentInputUri=" + mCurrentInputUri);

        if (!checkAndRequestReadPermission(activity, mCurrentInputUri)) {
            return null;
        }

        PgpDecryptVerifyInputParcel decryptInput = new PgpDecryptVerifyInputParcel()
                .setAllowSymmetricDecryption(true);
        return new InputDataParcel(mCurrentInputUri, decryptInput);

    }

    /**
     * Request READ_EXTERNAL_STORAGE permission on Android >= 6.0 to read content from "file" Uris.
     * <p/>
     * This method returns true on Android < 6, or if permission is already granted. It
     * requests the permission and returns false otherwise, taking over responsibility
     * for mCurrentInputUri.
     * <p/>
     * see https://commonsware.com/blog/2015/10/07/runtime-permissions-files-action-send.html
     */
    private boolean checkAndRequestReadPermission(Activity activity, final Uri uri) {
        if (!ContentResolver.SCHEME_FILE.equals(uri.getScheme())) {
            return true;
        }

        // Additional check due to https://commonsware.com/blog/2015/11/09/you-cannot-hold-nonexistent-permissions.html
        if (Build.VERSION.SDK_INT < VERSION_CODES.M) {
            return true;
        }

        if (ContextCompat.checkSelfPermission(activity,
                Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
            return true;
        }

        requestPermissions(new String[] { Manifest.permission.READ_EXTERNAL_STORAGE },
                REQUEST_PERMISSION_READ_EXTERNAL_STORAGE);

        return false;

    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
            @NonNull int[] grantResults) {

        if (requestCode != REQUEST_PERMISSION_READ_EXTERNAL_STORAGE) {
            super.onRequestPermissionsResult(requestCode, permissions, grantResults);
            return;
        }

        boolean permissionWasGranted = grantResults.length > 0
                && grantResults[0] == PackageManager.PERMISSION_GRANTED;

        if (permissionWasGranted) {

            // permission granted -> retry all cancelled file uris
            Iterator<Uri> it = mCancelledInputUris.iterator();
            while (it.hasNext()) {
                Uri uri = it.next();
                if (!ContentResolver.SCHEME_FILE.equals(uri.getScheme())) {
                    continue;
                }
                it.remove();
                mPendingInputUris.add(uri);
                mAdapter.setCancelled(uri, false);
            }

        } else {

            // permission denied -> cancel current, and all pending file uris
            mCancelledInputUris.add(mCurrentInputUri);
            mAdapter.setCancelled(mCurrentInputUri, true);

            mCurrentInputUri = null;
            Iterator<Uri> it = mPendingInputUris.iterator();
            while (it.hasNext()) {
                Uri uri = it.next();
                if (!ContentResolver.SCHEME_FILE.equals(uri.getScheme())) {
                    continue;
                }
                it.remove();
                mCancelledInputUris.add(uri);
                mAdapter.setCancelled(uri, true);
            }

        }

        // hand control flow back
        cryptoOperation();

    }

    @Override
    public boolean onMenuItemClick(MenuItem menuItem) {
        if (mAdapter.mMenuClickedModel == null || !mAdapter.mMenuClickedModel.hasResult()) {
            return false;
        }

        // don't process menu items until all items are done!
        if (!mPendingInputUris.isEmpty()) {
            return true;
        }

        Activity activity = getActivity();
        if (activity == null) {
            return false;
        }

        ViewModel model = mAdapter.mMenuClickedModel;
        switch (menuItem.getItemId()) {
        case R.id.view_log:
            Intent intent = new Intent(activity, LogDisplayActivity.class);
            intent.putExtra(LogDisplayFragment.EXTRA_RESULT, model.mResult);
            activity.startActivity(intent);
            return true;
        case R.id.decrypt_delete:
            deleteFile(activity, model.mInputUri);
            return true;
        }
        return false;
    }

    private void lookupUnknownKey(final Uri inputUri, long unknownKeyId) {

        final ArrayList<ParcelableKeyRing> keyList;
        final ParcelableHkpKeyserver keyserver;

        // search config
        keyserver = Preferences.getPreferences(getActivity()).getPreferredKeyserver();

        {
            ParcelableKeyRing keyEntry = new ParcelableKeyRing(null,
                    KeyFormattingUtils.convertKeyIdToHex(unknownKeyId), null, null);
            ArrayList<ParcelableKeyRing> selectedEntries = new ArrayList<>();
            selectedEntries.add(keyEntry);

            keyList = selectedEntries;
        }

        CryptoOperationHelper.Callback<ImportKeyringParcel, ImportKeyResult> callback = new CryptoOperationHelper.Callback<ImportKeyringParcel, ImportKeyResult>() {

            @Override
            public ImportKeyringParcel createOperationInput() {
                return new ImportKeyringParcel(keyList, keyserver);
            }

            @Override
            public void onCryptoOperationSuccess(ImportKeyResult result) {
                retryUri(inputUri);
            }

            @Override
            public void onCryptoOperationCancelled() {
                mAdapter.setProcessingKeyLookup(inputUri, false);
            }

            @Override
            public void onCryptoOperationError(ImportKeyResult result) {
                result.createNotify(getActivity()).show();
                mAdapter.setProcessingKeyLookup(inputUri, false);
            }

            @Override
            public boolean onCryptoSetProgress(String msg, int progress, int max) {
                return false;
            }
        };

        mAdapter.setProcessingKeyLookup(inputUri, true);

        CryptoOperationHelper importOpHelper = new CryptoOperationHelper<>(2, this, callback, null);
        importOpHelper.cryptoOperation();

    }

    private void deleteFile(Activity activity, Uri uri) {

        // we can only ever delete a file once, if we got this far either it's gone or it will never work
        mCanDelete = false;

        try {
            int deleted = FileHelper.deleteFileSecurely(activity, uri);
            if (deleted > 0) {
                Notify.create(activity, R.string.file_delete_ok, Style.OK).show();
            } else {
                Notify.create(activity, R.string.file_delete_none, Style.WARN).show();
            }
        } catch (Exception e) {
            Log.e(Constants.TAG, "exception deleting file", e);
            Notify.create(activity, R.string.file_delete_exception, Style.ERROR).show();
        }

    }

    public class DecryptFilesAdapter extends RecyclerView.Adapter<ViewHolder> {
        private ArrayList<ViewModel> mDataset;
        private ViewModel mMenuClickedModel;

        public class ViewModel {
            Uri mInputUri;
            InputDataResult mResult;

            int mProgress, mMax;
            String mProgressMsg;
            OnClickListener mCancelled;
            boolean mProcessingKeyLookup;

            ViewModel(Uri uri) {
                mInputUri = uri;
                mProgress = 0;
                mMax = 100;
                mCancelled = null;
            }

            void setResult(InputDataResult result) {
                mResult = result;
            }

            boolean hasResult() {
                return mResult != null;
            }

            void setCancelled(OnClickListener retryListener) {
                mCancelled = retryListener;
            }

            void setProgress(int progress, int max, String msg) {
                if (msg != null) {
                    mProgressMsg = msg;
                }
                mProgress = progress;
                mMax = max;
            }

            void setProcessingKeyLookup(boolean processingKeyLookup) {
                mProcessingKeyLookup = processingKeyLookup;
            }

            // Depends on inputUri only
            @Override
            public boolean equals(Object o) {
                if (this == o) {
                    return true;
                }
                if (o == null || getClass() != o.getClass()) {
                    return false;
                }
                ViewModel viewModel = (ViewModel) o;
                if (mInputUri == null) {
                    return viewModel.mInputUri == null;
                }
                return mInputUri.equals(viewModel.mInputUri);
            }

            // Depends on inputUri only
            @Override
            public int hashCode() {
                return mResult != null ? mResult.hashCode() : 0;
            }

            @Override
            public String toString() {
                return mResult.toString();
            }
        }

        // Provide a suitable constructor (depends on the kind of dataset)
        public DecryptFilesAdapter() {
            mDataset = new ArrayList<>();
        }

        // Create new views (invoked by the layout manager)
        @Override
        public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            //inflate your layout and pass it to view holder
            View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.decrypt_list_entry, parent, false);
            return new ViewHolder(v);
        }

        // Replace the contents of a view (invoked by the layout manager)
        @Override
        public void onBindViewHolder(ViewHolder holder, final int position) {
            // - get element from your dataset at this position
            // - replace the contents of the view with that element
            final ViewModel model = mDataset.get(position);

            if (model.mCancelled != null) {
                bindItemCancelled(holder, model);
                return;
            }

            if (!model.hasResult()) {
                bindItemProgress(holder, model);
                return;
            }

            if (model.mResult.success()) {
                bindItemSuccess(holder, model);
            } else {
                bindItemFailure(holder, model);
            }

        }

        private void bindItemCancelled(ViewHolder holder, ViewModel model) {
            holder.vAnimator.setDisplayedChild(3);

            holder.vCancelledRetry.setOnClickListener(model.mCancelled);
        }

        private void bindItemProgress(ViewHolder holder, ViewModel model) {
            holder.vAnimator.setDisplayedChild(0);

            holder.vProgress.setProgress(model.mProgress);
            holder.vProgress.setMax(model.mMax);
            if (model.mProgressMsg != null) {
                holder.vProgressMsg.setText(model.mProgressMsg);
            }
        }

        private void bindItemSuccess(ViewHolder holder, final ViewModel model) {
            holder.vAnimator.setDisplayedChild(1);

            KeyFormattingUtils.setStatus(getResources(), holder, model.mResult.mDecryptVerifyResult,
                    model.mProcessingKeyLookup);

            int numFiles = model.mResult.getOutputUris().size();
            holder.resizeFileList(numFiles, LayoutInflater.from(getActivity()));
            for (int i = 0; i < numFiles; i++) {

                Uri outputUri = model.mResult.getOutputUris().get(i);
                OpenPgpMetadata metadata = model.mResult.mMetadata.get(i);
                SubViewHolder fileHolder = holder.mFileHolderList.get(i);

                String filename;
                if (metadata == null) {
                    filename = getString(R.string.filename_unknown);
                } else if (!TextUtils.isEmpty(metadata.getFilename())) {
                    filename = metadata.getFilename();
                } else if (ClipDescription.compareMimeTypes(metadata.getMimeType(), Constants.MIME_TYPE_KEYS)) {
                    filename = getString(R.string.filename_keys);
                } else if (ClipDescription.compareMimeTypes(metadata.getMimeType(), "text/plain")) {
                    filename = getString(R.string.filename_unknown_text);
                } else {
                    filename = getString(R.string.filename_unknown);
                }
                fileHolder.vFilename.setText(filename);

                long size = metadata == null ? 0 : metadata.getOriginalSize();
                if (size == -1 || size == 0) {
                    fileHolder.vFilesize.setText("");
                } else {
                    fileHolder.vFilesize.setText(FileHelper.readableFileSize(size));
                }

                if (mIconCache.containsKey(outputUri)) {
                    fileHolder.vThumbnail.setImageDrawable(mIconCache.get(outputUri));
                } else {
                    fileHolder.vThumbnail.setImageResource(R.drawable.ic_doc_generic_am);
                }

                // save index closure-style :)
                final int idx = i;

                fileHolder.vFile.setOnLongClickListener(new OnLongClickListener() {
                    @Override
                    public boolean onLongClick(View view) {
                        if (model.mResult.success()) {
                            displayBottomSheet(model.mResult, idx);
                            return true;
                        }
                        return false;
                    }
                });

                fileHolder.vFile.setOnClickListener(new OnClickListener() {
                    @Override
                    public void onClick(View view) {
                        if (model.mResult.success()) {
                            displayWithViewIntent(model.mResult, idx, false, false);
                        }
                    }
                });

            }

            OpenPgpSignatureResult sigResult = model.mResult.mDecryptVerifyResult.getSignatureResult();
            if (sigResult != null) {
                final long keyId = sigResult.getKeyId();
                if (sigResult.getResult() != OpenPgpSignatureResult.RESULT_KEY_MISSING) {
                    holder.vSignatureLayout.setOnClickListener(new OnClickListener() {
                        @Override
                        public void onClick(View view) {
                            Activity activity = getActivity();
                            if (activity == null) {
                                return;
                            }
                            Intent intent = new Intent(activity, ViewKeyActivity.class);
                            intent.setData(KeyRings.buildUnifiedKeyRingUri(keyId));
                            activity.startActivity(intent);
                        }
                    });
                } else {
                    holder.vSignatureLayout.setOnClickListener(new OnClickListener() {
                        @Override
                        public void onClick(View view) {
                            lookupUnknownKey(model.mInputUri, keyId);
                        }
                    });
                }
            }

            holder.vContextMenu.setTag(model);
            holder.vContextMenu.setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View view) {
                    Activity activity = getActivity();
                    if (activity == null) {
                        return;
                    }
                    mMenuClickedModel = model;
                    PopupMenu menu = new PopupMenu(activity, view);
                    menu.inflate(R.menu.decrypt_item_context_menu);
                    menu.setOnMenuItemClickListener(DecryptListFragment.this);
                    menu.setOnDismissListener(new OnDismissListener() {
                        @Override
                        public void onDismiss(PopupMenu popupMenu) {
                            mMenuClickedModel = null;
                        }
                    });
                    menu.getMenu().findItem(R.id.decrypt_delete).setEnabled(mCanDelete);
                    menu.show();
                }
            });
        }

        private void bindItemFailure(ViewHolder holder, final ViewModel model) {
            holder.vAnimator.setDisplayedChild(2);

            holder.vErrorMsg.setText(model.mResult.getLog().getLast().mType.getMsgId());

            holder.vErrorViewLog.setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View v) {
                    Activity activity = getActivity();
                    if (activity == null) {
                        return;
                    }
                    Intent intent = new Intent(activity, LogDisplayActivity.class);
                    intent.putExtra(LogDisplayFragment.EXTRA_RESULT, model.mResult);
                    activity.startActivity(intent);
                }
            });

        }

        // Return the size of your dataset (invoked by the layout manager)
        @Override
        public int getItemCount() {
            return mDataset.size();
        }

        public InputDataResult getItemResult(Uri uri) {
            ViewModel model = new ViewModel(uri);
            int pos = mDataset.indexOf(model);
            if (pos == -1) {
                return null;
            }
            model = mDataset.get(pos);

            return model.mResult;
        }

        public void add(Uri uri) {
            ViewModel newModel = new ViewModel(uri);
            mDataset.add(newModel);
            notifyItemInserted(mDataset.size());
        }

        public void setProgress(Uri uri, int progress, int max, String msg) {
            ViewModel newModel = new ViewModel(uri);
            int pos = mDataset.indexOf(newModel);
            mDataset.get(pos).setProgress(progress, max, msg);
            notifyItemChanged(pos);
        }

        public void setCancelled(final Uri uri, boolean isCancelled) {
            ViewModel newModel = new ViewModel(uri);
            int pos = mDataset.indexOf(newModel);
            if (isCancelled) {
                mDataset.get(pos).setCancelled(new OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        retryUri(uri);
                    }
                });
            } else {
                mDataset.get(pos).setCancelled(null);
            }
            notifyItemChanged(pos);
        }

        public void setProcessingKeyLookup(Uri uri, boolean processingKeyLookup) {
            ViewModel newModel = new ViewModel(uri);
            int pos = mDataset.indexOf(newModel);
            mDataset.get(pos).setProcessingKeyLookup(processingKeyLookup);
            notifyItemChanged(pos);
        }

        public void addResult(Uri uri, InputDataResult result) {
            ViewModel model = new ViewModel(uri);
            int pos = mDataset.indexOf(model);
            model = mDataset.get(pos);
            model.setResult(result);
            notifyItemChanged(pos);
        }

        public void resetItemData(Uri uri) {
            ViewModel model = new ViewModel(uri);
            int pos = mDataset.indexOf(model);
            model = mDataset.get(pos);
            model.setResult(null);
            model.setCancelled(null);
            model.setProcessingKeyLookup(false);
            notifyItemChanged(pos);
        }

    }

    // Provide a reference to the views for each data item
    // Complex data items may need more than one view per item, and
    // you provide access to all the views for a data item in a view holder
    public static class ViewHolder extends RecyclerView.ViewHolder implements StatusHolder {
        public ViewAnimator vAnimator;

        public ProgressBar vProgress;
        public TextView vProgressMsg;

        public ImageView vEncStatusIcon;
        public TextView vEncStatusText;

        public ImageView vSigStatusIcon;
        public TextView vSigStatusText;
        public View vSignatureLayout;
        public TextView vSignatureName;
        public TextView vSignatureMail;
        public ViewAnimator vSignatureAction;
        public View vContextMenu;

        public TextView vErrorMsg;
        public ImageView vErrorViewLog;

        public ImageView vCancelledRetry;

        public LinearLayout vFileList;

        public static class SubViewHolder {
            public View vFile;
            public TextView vFilename;
            public TextView vFilesize;
            public ImageView vThumbnail;

            public SubViewHolder(View itemView) {
                vFile = itemView.findViewById(R.id.file);
                vFilename = (TextView) itemView.findViewById(R.id.filename);
                vFilesize = (TextView) itemView.findViewById(R.id.filesize);
                vThumbnail = (ImageView) itemView.findViewById(R.id.thumbnail);
            }
        }

        public ArrayList<SubViewHolder> mFileHolderList = new ArrayList<>();
        private int mCurrentFileListSize = 0;

        public ViewHolder(View itemView) {
            super(itemView);

            vAnimator = (ViewAnimator) itemView.findViewById(R.id.view_animator);

            vProgress = (ProgressBar) itemView.findViewById(R.id.progress);
            vProgressMsg = (TextView) itemView.findViewById(R.id.progress_msg);

            vEncStatusIcon = (ImageView) itemView.findViewById(R.id.result_encryption_icon);
            vEncStatusText = (TextView) itemView.findViewById(R.id.result_encryption_text);

            vSigStatusIcon = (ImageView) itemView.findViewById(R.id.result_signature_icon);
            vSigStatusText = (TextView) itemView.findViewById(R.id.result_signature_text);
            vSignatureLayout = itemView.findViewById(R.id.result_signature_layout);
            vSignatureName = (TextView) itemView.findViewById(R.id.result_signature_name);
            vSignatureMail = (TextView) itemView.findViewById(R.id.result_signature_email);
            vSignatureAction = (ViewAnimator) itemView.findViewById(R.id.result_signature_action);

            vFileList = (LinearLayout) itemView.findViewById(R.id.file_list);
            for (int i = 0; i < vFileList.getChildCount(); i++) {
                mFileHolderList.add(new SubViewHolder(vFileList.getChildAt(i)));
                mCurrentFileListSize += 1;
            }

            vContextMenu = itemView.findViewById(R.id.context_menu);

            vErrorMsg = (TextView) itemView.findViewById(R.id.result_error_msg);
            vErrorViewLog = (ImageView) itemView.findViewById(R.id.result_error_log);

            vCancelledRetry = (ImageView) itemView.findViewById(R.id.cancel_retry);

        }

        public void resizeFileList(int size, LayoutInflater inflater) {
            int childCount = vFileList.getChildCount();
            // if we require more children, create them
            while (childCount < size) {
                View v = inflater.inflate(R.layout.decrypt_list_file_item, null);
                vFileList.addView(v);
                mFileHolderList.add(new SubViewHolder(v));
                childCount += 1;
            }

            while (size < mCurrentFileListSize) {
                mCurrentFileListSize -= 1;
                vFileList.getChildAt(mCurrentFileListSize).setVisibility(View.GONE);
            }
            while (size > mCurrentFileListSize) {
                vFileList.getChildAt(mCurrentFileListSize).setVisibility(View.VISIBLE);
                mCurrentFileListSize += 1;
            }

        }

        @Override
        public ImageView getEncryptionStatusIcon() {
            return vEncStatusIcon;
        }

        @Override
        public TextView getEncryptionStatusText() {
            return vEncStatusText;
        }

        @Override
        public ImageView getSignatureStatusIcon() {
            return vSigStatusIcon;
        }

        @Override
        public TextView getSignatureStatusText() {
            return vSigStatusText;
        }

        @Override
        public View getSignatureLayout() {
            return vSignatureLayout;
        }

        @Override
        public ViewAnimator getSignatureAction() {
            return vSignatureAction;
        }

        @Override
        public TextView getSignatureUserName() {
            return vSignatureName;
        }

        @Override
        public TextView getSignatureUserEmail() {
            return vSignatureMail;
        }

        @Override
        public boolean hasEncrypt() {
            return true;
        }
    }

}