diff --git a/OpenKeychain/build.gradle b/OpenKeychain/build.gradle index e2d1dd8c8..45ae01166 100644 --- a/OpenKeychain/build.gradle +++ b/OpenKeychain/build.gradle @@ -24,6 +24,7 @@ dependencies { androidTestCompile 'com.android.support.test:runner:0.3' androidTestCompile 'com.android.support.test:rules:0.3' androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2' + androidTestCompile 'com.android.support.test.espresso:espresso-intents:2.2' androidTestCompile ('com.android.support.test.espresso:espresso-contrib:2.2') { exclude group: 'com.android.support', module: 'appcompat' exclude group: 'com.android.support', module: 'support-v4' diff --git a/OpenKeychain/build/outputs/code-coverage/connected/coverage.ec b/OpenKeychain/build/outputs/code-coverage/connected/coverage.ec index cd6aee896..5f12b1340 100644 Binary files a/OpenKeychain/build/outputs/code-coverage/connected/coverage.ec and b/OpenKeychain/build/outputs/code-coverage/connected/coverage.ec differ diff --git a/OpenKeychain/src/androidTest/assets/ci.png b/OpenKeychain/src/androidTest/assets/ci.png new file mode 100644 index 000000000..3a6117082 Binary files /dev/null and b/OpenKeychain/src/androidTest/assets/ci.png differ diff --git a/OpenKeychain/src/androidTest/assets/pa.png b/OpenKeychain/src/androidTest/assets/pa.png new file mode 100644 index 000000000..3c6aa7fda Binary files /dev/null and b/OpenKeychain/src/androidTest/assets/pa.png differ diff --git a/OpenKeychain/src/androidTest/assets/re.png b/OpenKeychain/src/androidTest/assets/re.png new file mode 100644 index 000000000..a441bbc87 Binary files /dev/null and b/OpenKeychain/src/androidTest/assets/re.png differ diff --git a/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/EncryptDecryptSymmetricTests.java b/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/EncryptDecryptSymmetricTests.java deleted file mode 100644 index f07566755..000000000 --- a/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/EncryptDecryptSymmetricTests.java +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright (C) 2015 Vincent Breitmoser - * - * 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; - - -import android.content.Intent; -import android.support.test.espresso.matcher.ViewMatchers; -import android.support.test.rule.ActivityTestRule; -import android.support.test.runner.AndroidJUnit4; -import android.test.suitebuilder.annotation.LargeTest; - -import org.junit.FixMethodOrder; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.MethodSorters; -import org.sufficientlysecure.keychain.ui.MainActivity; -import org.sufficientlysecure.keychain.ui.util.Notify.Style; - -import static android.support.test.InstrumentationRegistry.getInstrumentation; -import static android.support.test.espresso.Espresso.onView; -import static android.support.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu; -import static android.support.test.espresso.Espresso.pressBack; -import static android.support.test.espresso.action.ViewActions.click; -import static android.support.test.espresso.action.ViewActions.typeText; -import static android.support.test.espresso.assertion.ViewAssertions.matches; -import static android.support.test.espresso.contrib.DrawerActions.openDrawer; -import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; -import static android.support.test.espresso.matcher.ViewMatchers.withId; -import static android.support.test.espresso.matcher.ViewMatchers.withText; -import static org.hamcrest.CoreMatchers.not; -import static org.sufficientlysecure.keychain.TestHelpers.checkSnackbar; -import static org.sufficientlysecure.keychain.TestHelpers.randomString; -import static org.sufficientlysecure.keychain.matcher.DrawableMatcher.withDrawable; - - -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -@RunWith(AndroidJUnit4.class) -@LargeTest -public class EncryptDecryptSymmetricTests { - - public static final String PASSPHRASE = randomString(5, 20); - - @Rule - public final ActivityTestRule mActivity - = new ActivityTestRule(MainActivity.class) { - @Override - protected Intent getActivityIntent() { - Intent intent = super.getActivityIntent(); - intent.putExtra(MainActivity.EXTRA_SKIP_FIRST_TIME, true); - return intent; - } - }; - - @Test - public void testSymmetricTextEncryptDecrypt() throws Exception { - - MainActivity activity = mActivity.getActivity(); - - String text = randomString(10, 30); - - // navigate to encrypt/decrypt - openDrawer(R.id.drawer_layout); - onView(ViewMatchers.withText(R.string.nav_encrypt_decrypt)).perform(click()); - onView(withId(R.id.encrypt_text)).perform(click()); - - { - onView(withId(R.id.encrypt_text_text)).perform(typeText(text)); - - openActionBarOverflowOrOptionsMenu(getInstrumentation().getTargetContext()); - onView(withText(R.string.label_symmetric)).perform(click()); - - onView(withId(R.id.passphrase)).perform(typeText(PASSPHRASE)); - - onView(withId(R.id.encrypt_copy)).perform(click()); - - checkSnackbar(Style.ERROR, R.string.passphrases_do_not_match); - - onView(withId(R.id.passphraseAgain)).perform(typeText(PASSPHRASE)); - - onView(withId(R.id.encrypt_text_text)).check(matches(withText(text))); - - onView(withId(R.id.encrypt_copy)).perform(click()); - - checkSnackbar(Style.OK, R.string.msg_se_success); - } - - // go to decrypt from clipboard view - pressBack(); - onView(withId(R.id.decrypt_from_clipboard)).perform(click()); - - { - onView(withId(R.id.passphrase_passphrase)).perform(typeText(PASSPHRASE)); - onView(withText(R.string.btn_unlock)).perform(click()); - - onView(withId(R.id.decrypt_text_plaintext)).check(matches( - withText(text))); - - // TODO write generic status verifier - - onView(withId(R.id.result_encryption_text)).check(matches( - withText(R.string.decrypt_result_encrypted))); - onView(withId(R.id.result_signature_text)).check(matches( - withText(R.string.decrypt_result_no_signature))); - onView(withId(R.id.result_signature_layout)).check(matches( - not(isDisplayed()))); - - onView(withId(R.id.result_encryption_icon)).check(matches( - withDrawable(R.drawable.status_lock_closed_24dp))); - onView(withId(R.id.result_signature_icon)).check(matches( - withDrawable(R.drawable.status_signature_unknown_cutout_24dp))); - - } - - } - -} diff --git a/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/TestHelpers.java b/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/TestHelpers.java index 7915ec5db..958c589cb 100644 --- a/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/TestHelpers.java +++ b/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/TestHelpers.java @@ -18,12 +18,27 @@ package org.sufficientlysecure.keychain; +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.util.Random; import android.content.Context; import android.support.annotation.StringRes; +import android.support.test.espresso.UiController; +import android.support.test.espresso.ViewAction; +import android.support.test.espresso.base.DefaultFailureHandler; +import android.support.test.espresso.matcher.ViewMatchers; +import android.view.View; +import com.nispok.snackbar.Snackbar; +import com.tokenautocomplete.TokenCompleteTextView; import org.hamcrest.CoreMatchers; +import org.hamcrest.Matcher; import org.sufficientlysecure.keychain.pgp.UncachedKeyRing; import org.sufficientlysecure.keychain.pgp.UncachedKeyRing.IteratorWithIOThrow; import org.sufficientlysecure.keychain.provider.KeychainDatabase; @@ -38,19 +53,39 @@ import static android.support.test.espresso.assertion.ViewAssertions.matches; import static android.support.test.espresso.matcher.ViewMatchers.hasDescendant; import static android.support.test.espresso.matcher.ViewMatchers.withClassName; import static android.support.test.espresso.matcher.ViewMatchers.withText; +import static org.hamcrest.CoreMatchers.endsWith; import static org.sufficientlysecure.keychain.matcher.CustomMatchers.withSnackbarLineColor; public class TestHelpers { + public static void dismissSnackbar() { + onView(withClassName(endsWith("Snackbar"))) + .perform(new ViewAction() { + @Override + public Matcher getConstraints() { + return ViewMatchers.isAssignableFrom(Snackbar.class); + } + + @Override + public String getDescription() { + return "dismiss snackbar"; + } + + @Override + public void perform(UiController uiController, View view) { + ((Snackbar) view).dismiss(); + } + }); + } public static void checkSnackbar(Style style, @StringRes Integer text) { - onView(withClassName(CoreMatchers.endsWith("Snackbar"))) + onView(withClassName(endsWith("Snackbar"))) .check(matches(withSnackbarLineColor(style.mLineColor))); if (text != null) { - onView(withClassName(CoreMatchers.endsWith("Snackbar"))) + onView(withClassName(endsWith("Snackbar"))) .check(matches(hasDescendant(withText(text)))); } @@ -73,6 +108,37 @@ public class TestHelpers { } + public static void copyFiles() throws IOException { + File cacheDir = getInstrumentation().getTargetContext().getFilesDir(); + byte[] buf = new byte[256]; + for (String filename : FILES) { + File outFile = new File(cacheDir, filename); + if (outFile.exists()) { + continue; + } + InputStream in = new BufferedInputStream(getInstrumentation().getContext().getAssets().open(filename)); + OutputStream out = new BufferedOutputStream(new FileOutputStream(outFile)); + int len; + while( (len = in.read(buf)) > 0) { + out.write(buf, 0, len); + } + } + } + + public static final String[] FILES = new String[] { "pa.png", "re.png", "ci.png" }; + public static File[] getImageNames() { + File cacheDir = getInstrumentation().getTargetContext().getFilesDir(); + File[] ret = new File[FILES.length]; + for (int i = 0; i < ret.length; i++) { + ret[i] = new File(cacheDir, FILES[i]); + } + return ret; + } + + public static T pickRandom(T[] haystack) { + return haystack[new Random().nextInt(haystack.length)]; + } + public static String randomString(int min, int max) { String chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456789!@#$%^&*()-_="; Random r = new Random(); diff --git a/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/actions/OrientationChangeAction.java b/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/actions/OrientationChangeAction.java new file mode 100644 index 000000000..cdded7d7f --- /dev/null +++ b/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/actions/OrientationChangeAction.java @@ -0,0 +1,74 @@ +package org.sufficientlysecure.keychain.actions; + + +import java.util.Collection; + +import android.app.Activity; +import android.content.Context; +import android.content.ContextWrapper; +import android.content.pm.ActivityInfo; +import android.support.test.espresso.UiController; +import android.support.test.espresso.ViewAction; +import android.support.test.runner.lifecycle.ActivityLifecycleMonitorRegistry; +import android.support.test.runner.lifecycle.Stage; +import android.view.View; + +import org.hamcrest.Matcher; + +import static android.support.test.espresso.matcher.ViewMatchers.isRoot; + +public class OrientationChangeAction implements ViewAction { + private final int orientation; + + private OrientationChangeAction(int orientation) { + this.orientation = orientation; + } + + @Override + public Matcher getConstraints() { + return isRoot(); + } + + @Override + public String getDescription() { + return "change orientation to " + orientation; + } + + @Override + public void perform(UiController uiController, View view) { + uiController.loopMainThreadUntilIdle(); + + final Activity activity = findActivity(view.getContext()); + if (activity == null){ + throw new IllegalStateException("Could not find the current activity"); + } + + activity.setRequestedOrientation(orientation); + + Collection resumedActivities = ActivityLifecycleMonitorRegistry + .getInstance().getActivitiesInStage(Stage.RESUMED); + + if (resumedActivities.isEmpty()) { + throw new RuntimeException("Could not change orientation"); + } + } + + private static Activity findActivity(Context context) { + if (context == null) + return null; + else if (context instanceof Activity) + return (Activity) context; + else if (context instanceof ContextWrapper) + return findActivity(((ContextWrapper) context).getBaseContext()); + + return null; + } + + public static ViewAction orientationLandscape() { + return new OrientationChangeAction(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); + } + + public static ViewAction orientationPortrait() { + return new OrientationChangeAction(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); + } +} \ No newline at end of file diff --git a/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/matcher/BitmapMatcher.java b/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/matcher/BitmapMatcher.java new file mode 100644 index 000000000..c08847065 --- /dev/null +++ b/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/matcher/BitmapMatcher.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2015 Vincent Breitmoser + * + * 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 . + * + * From the droidcon anroid espresso repository. + * https://github.com/xrigau/droidcon-android-espresso/ + * + */ + +package org.sufficientlysecure.keychain.matcher; + + +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.view.View; +import android.widget.ImageView; + +import org.hamcrest.Description; +import org.hamcrest.TypeSafeMatcher; + + +public class BitmapMatcher extends TypeSafeMatcher { + + private final Bitmap mBitmap; + + public BitmapMatcher(Bitmap bitmap) { + super(View.class); + mBitmap = bitmap; + } + + @Override + public boolean matchesSafely(View view) { + if ( !(view instanceof ImageView) ) { + return false; + } + Drawable drawable = ((ImageView) view).getDrawable(); + return drawable != null && (drawable instanceof BitmapDrawable) + && ((BitmapDrawable) drawable).getBitmap().sameAs(mBitmap); + } + + @Override + public void describeTo(Description description) { + description.appendText("with equivalent specified bitmap"); + } + + public static BitmapMatcher withBitmap(Bitmap bitmap) { + return new BitmapMatcher(bitmap); + } + +} diff --git a/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/matcher/CustomMatchers.java b/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/matcher/CustomMatchers.java index 1db902c4c..6713cd237 100644 --- a/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/matcher/CustomMatchers.java +++ b/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/matcher/CustomMatchers.java @@ -20,21 +20,45 @@ package org.sufficientlysecure.keychain.matcher; import android.support.annotation.ColorRes; +import android.support.annotation.IdRes; import android.support.test.espresso.matcher.BoundedMatcher; +import android.support.v7.widget.RecyclerView; import android.view.View; +import android.widget.ViewAnimator; import com.nispok.snackbar.Snackbar; import org.hamcrest.Description; import org.hamcrest.Matcher; -import org.sufficientlysecure.keychain.EncryptKeyCompletionViewTest; +import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.ui.adapter.KeyAdapter.KeyItem; import org.sufficientlysecure.keychain.ui.widget.EncryptKeyCompletionView; -import static android.support.test.internal.util.Checks.checkNotNull; +import static android.support.test.espresso.matcher.ViewMatchers.hasDescendant; +import static android.support.test.espresso.matcher.ViewMatchers.isAssignableFrom; +import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; +import static android.support.test.espresso.matcher.ViewMatchers.withId; +import static android.support.test.espresso.matcher.ViewMatchers.withParent; +import static android.support.test.espresso.matcher.ViewMatchers.withText; +import static org.hamcrest.CoreMatchers.allOf; +import static org.hamcrest.CoreMatchers.not; +import static org.sufficientlysecure.keychain.matcher.DrawableMatcher.withDrawable; public abstract class CustomMatchers { + public static Matcher withDisplayedChild(final int child) { + return new BoundedMatcher(ViewAnimator.class) { + public void describeTo(Description description) { + description.appendText("with displayed child: " + child); + } + + @Override + public boolean matchesSafely(ViewAnimator viewAnimator) { + return viewAnimator.getDisplayedChild() == child; + } + }; + } + public static Matcher withSnackbarLineColor(@ColorRes final int colorRes) { return new BoundedMatcher(Snackbar.class) { public void describeTo(Description description) { @@ -80,5 +104,57 @@ public abstract class CustomMatchers { }; } + public static Matcher withRecyclerView(@IdRes int viewId) { + return allOf(isAssignableFrom(RecyclerView.class), withId(viewId)); + } + + public static Matcher isRecyclerItemView(@IdRes int recyclerId, Matcher specificChildMatcher) { + return allOf(withParent(withRecyclerView(recyclerId)), specificChildMatcher); + } + + public static Matcher withEncryptionStatus(boolean encrypted) { + + if (encrypted) { + return allOf( + hasDescendant(allOf( + withId(R.id.result_encryption_text), withText(R.string.decrypt_result_encrypted))), + hasDescendant(allOf( + withId(R.id.result_encryption_icon), withDrawable(R.drawable.status_lock_closed_24dp, true))) + ); + } else { + return allOf( + hasDescendant(allOf( + withId(R.id.result_encryption_text), withText(R.string.decrypt_result_not_encrypted))), + hasDescendant(allOf( + withId(R.id.result_encryption_icon), withDrawable(R.drawable.status_lock_open_24dp, true))) + ); + } + } + + public static Matcher withSignatureNone() { + + return allOf( + hasDescendant(allOf( + withId(R.id.result_signature_text), withText(R.string.decrypt_result_no_signature))), + hasDescendant(allOf( + withId(R.id.result_signature_icon), withDrawable(R.drawable.status_signature_invalid_cutout_24dp, true))), + hasDescendant(allOf( + withId(R.id.result_signature_layout), not(isDisplayed()))) + ); + + } + + public static Matcher withSignatureMyKey() { + + return allOf( + hasDescendant(allOf( + withId(R.id.result_signature_text), withText(R.string.decrypt_result_signature_certified))), + hasDescendant(allOf( + withId(R.id.result_signature_icon), withDrawable(R.drawable.status_signature_verified_cutout_24dp, true))), + hasDescendant(allOf( + withId(R.id.result_signature_layout), isDisplayed())) + ); + + } } diff --git a/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/matcher/DrawableMatcher.java b/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/matcher/DrawableMatcher.java index 761fb8e0b..da2ff87d9 100644 --- a/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/matcher/DrawableMatcher.java +++ b/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/matcher/DrawableMatcher.java @@ -102,7 +102,7 @@ public class DrawableMatcher extends TypeSafeMatcher { } // if those are both bitmap drawables, compare their bitmaps (ignores color filters, which is what we want!) if (mIgnoreFilters && drawable instanceof BitmapDrawable && expectedDrawable instanceof BitmapDrawable) { - return ((BitmapDrawable) drawable).getBitmap().equals(((BitmapDrawable) expectedDrawable).getBitmap()); + return ((BitmapDrawable) drawable).getBitmap().sameAs((((BitmapDrawable) expectedDrawable).getBitmap())); } return expectedDrawable.getConstantState().equals(drawable.getConstantState()); } diff --git a/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/ui/AsymmetricFileOperationTests.java b/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/ui/AsymmetricFileOperationTests.java new file mode 100644 index 000000000..20ee6a8b1 --- /dev/null +++ b/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/ui/AsymmetricFileOperationTests.java @@ -0,0 +1,389 @@ +/* + * Copyright (C) 2015 Vincent Breitmoser + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui; + + +import java.io.File; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.app.Instrumentation.ActivityResult; +import android.content.Intent; +import android.net.Uri; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.support.test.espresso.intent.Intents; +import android.support.test.espresso.intent.rule.IntentsTestRule; +import android.support.test.runner.AndroidJUnit4; +import android.test.suitebuilder.annotation.LargeTest; +import android.widget.AdapterView; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.TestHelpers; +import org.sufficientlysecure.keychain.service.PassphraseCacheService; +import org.sufficientlysecure.keychain.ui.util.Notify.Style; + +import static android.support.test.InstrumentationRegistry.getInstrumentation; +import static android.support.test.espresso.Espresso.onData; +import static android.support.test.espresso.Espresso.onView; +import static android.support.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu; +import static android.support.test.espresso.Espresso.pressBack; +import static android.support.test.espresso.action.ViewActions.click; +import static android.support.test.espresso.action.ViewActions.typeText; +import static android.support.test.espresso.assertion.ViewAssertions.matches; +import static android.support.test.espresso.intent.matcher.IntentMatchers.hasAction; +import static android.support.test.espresso.intent.matcher.IntentMatchers.hasCategories; +import static android.support.test.espresso.intent.matcher.IntentMatchers.hasExtra; +import static android.support.test.espresso.intent.matcher.IntentMatchers.hasExtraWithKey; +import static android.support.test.espresso.intent.matcher.IntentMatchers.hasType; +import static android.support.test.espresso.matcher.ViewMatchers.assertThat; +import static android.support.test.espresso.matcher.ViewMatchers.hasDescendant; +import static android.support.test.espresso.matcher.ViewMatchers.isAssignableFrom; +import static android.support.test.espresso.matcher.ViewMatchers.isDescendantOfA; +import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; +import static android.support.test.espresso.matcher.ViewMatchers.withId; +import static android.support.test.espresso.matcher.ViewMatchers.withText; +import static org.hamcrest.CoreMatchers.allOf; +import static org.hamcrest.CoreMatchers.hasItem; +import static org.hamcrest.CoreMatchers.is; +import static org.sufficientlysecure.keychain.TestHelpers.checkSnackbar; +import static org.sufficientlysecure.keychain.TestHelpers.getImageNames; +import static org.sufficientlysecure.keychain.TestHelpers.importKeysFromResource; +import static org.sufficientlysecure.keychain.TestHelpers.pickRandom; +import static org.sufficientlysecure.keychain.TestHelpers.randomString; +import static org.sufficientlysecure.keychain.actions.CustomActions.tokenEncryptViewAddToken; +import static org.sufficientlysecure.keychain.matcher.CustomMatchers.isRecyclerItemView; +import static org.sufficientlysecure.keychain.matcher.CustomMatchers.withDisplayedChild; +import static org.sufficientlysecure.keychain.matcher.CustomMatchers.withEncryptionStatus; +import static org.sufficientlysecure.keychain.matcher.CustomMatchers.withKeyItemId; +import static org.sufficientlysecure.keychain.matcher.CustomMatchers.withSignatureMyKey; +import static org.sufficientlysecure.keychain.matcher.CustomMatchers.withSignatureNone; + + +@RunWith(AndroidJUnit4.class) +@LargeTest +public class AsymmetricFileOperationTests { + + @Rule + public final IntentsTestRule mActivity + = new IntentsTestRule(MainActivity.class) { + @Override + protected Intent getActivityIntent() { + Intent intent = super.getActivityIntent(); + intent.putExtra(MainActivity.EXTRA_SKIP_FIRST_TIME, true); + intent.putExtra(MainActivity.EXTRA_INIT_FRAG, MainActivity.ID_ENCRYPT_DECRYPT); + return intent; + } + }; + + @Before + public void setUp() throws Exception { + Activity activity = mActivity.getActivity(); + + TestHelpers.copyFiles(); + + // import these two, make sure they're there + importKeysFromResource(activity, "x.sec.asc"); + + // make sure no passphrases are cached + PassphraseCacheService.clearCachedPassphrases(activity); + } + + @Test + public void testFileSaveEncryptDecrypt() throws Exception { + + // navigate to 'encrypt text' + onView(withId(R.id.encrypt_files)).perform(click()); + + File file = pickRandom(getImageNames()); + File outputFile = new File(getInstrumentation().getTargetContext().getFilesDir(), "output-token.gpg"); + + { // encrypt + + // the EncryptKeyCompletionView is tested individually + onView(withId(R.id.recipient_list)).perform(tokenEncryptViewAddToken(0x9D604D2F310716A3L)); + + handleAddFileIntent(file); + onView(withId(R.id.file_list_entry_add)).perform(click()); + + handleSaveEncryptedFileIntent(outputFile); + onView(withId(R.id.encrypt_save)).perform(click()); + + assertThat("output file has been written", true, is(outputFile.exists())); + + } + + // go to decrypt from clipboard view + pressBack(); + + handleOpenFileIntentKitKat(outputFile); + onView(withId(R.id.decrypt_files)).perform(click()); + + { // decrypt + onView(withId(R.id.passphrase_passphrase)).perform(typeText("x")); + onView(withText(R.string.btn_unlock)).perform(click()); + + onView(isRecyclerItemView(R.id.decrypted_files_list, + hasDescendant(withText(file.getName())))) + .check(matches(allOf(withEncryptionStatus(true), withSignatureNone()))); + } + + { // delete original file + + // open context menu + onView(allOf(isDescendantOfA(isRecyclerItemView(R.id.decrypted_files_list, + hasDescendant(withText(file.getName())))), + withId(R.id.context_menu))).perform(click()); + + // delete file + onView(withText(R.string.btn_delete_original)).perform(click()); + + checkSnackbar(Style.OK, R.string.file_delete_ok); + assertThat("output file has been deleted", false, is(outputFile.exists())); + + // open context menu + onView(allOf(isDescendantOfA(isRecyclerItemView(R.id.decrypted_files_list, + hasDescendant(withText(file.getName())))), + withId(R.id.context_menu))).perform(click()); + + // delete file + onView(withText(R.string.btn_delete_original)).perform(click()); + + checkSnackbar(Style.WARN, R.string.file_delete_none); + + } + + { // save file (*after* deletion~) + + // open context menu + onView(allOf(isDescendantOfA(isRecyclerItemView(R.id.decrypted_files_list, + hasDescendant(withText(file.getName())))), + withId(R.id.context_menu))).perform(click()); + + File savedFile = + new File(getInstrumentation().getTargetContext().getFilesDir(), "vo.png"); + handleSaveDecryptedFileIntent(savedFile, file.getName()); + + // save decrypted content + onView(withText(R.string.btn_save)).perform(click()); + + checkSnackbar(Style.OK, R.string.file_saved); + assertThat("decrypted file has been saved", true, is(savedFile.exists())); + + // cleanup + // noinspection ResultOfMethodCallIgnored + file.delete(); + + } + + } + + private void handleAddFileIntent(File file) { + if (VERSION.SDK_INT >= VERSION_CODES.KITKAT) { + handleAddFileIntentKitKat(file); + } else { + handleAddFileIntentOlder(file); + } + } + + @TargetApi(VERSION_CODES.KITKAT) + private void handleAddFileIntentKitKat(File file) { + Intent data = new Intent(); + data.setData(Uri.fromFile(file)); + + Intents.intending(allOf( + hasAction(Intent.ACTION_OPEN_DOCUMENT), + hasType("*/*"), + hasCategories(hasItem(Intent.CATEGORY_OPENABLE)), + hasExtraWithKey(Intent.EXTRA_ALLOW_MULTIPLE) + )).respondWith( + new ActivityResult(Activity.RESULT_OK, data) + ); + } + + private void handleAddFileIntentOlder(File file) { + Intent data = new Intent(); + data.setData(Uri.fromFile(file)); + + Intents.intending(allOf( + hasAction(Intent.ACTION_GET_CONTENT), + hasType("*/*"), + hasCategories(hasItem(Intent.CATEGORY_OPENABLE)) + )).respondWith( + new ActivityResult(Activity.RESULT_OK, data) + ); + } + + @TargetApi(VERSION_CODES.KITKAT) + private void handleSaveDecryptedFileIntent(File file, String expectedTitle) { + Intent data = new Intent(); + data.setData(Uri.fromFile(file)); + + Intents.intending(allOf( + hasAction(Intent.ACTION_CREATE_DOCUMENT), + hasExtra("android.content.extra.SHOW_ADVANCED", true), + hasExtra(Intent.EXTRA_TITLE, expectedTitle), + hasCategories(hasItem(Intent.CATEGORY_OPENABLE)) + )).respondWith( + new ActivityResult(Activity.RESULT_OK, data) + ); + } + + @TargetApi(VERSION_CODES.KITKAT) + private void handleSaveEncryptedFileIntent(File file) { + + try { + //noinspection ResultOfMethodCallIgnored + file.delete(); + } catch (Exception e) { + // nvm + } + + Intent data = new Intent(); + data.setData(Uri.fromFile(file)); + + Intents.intending(allOf( + hasAction(Intent.ACTION_CREATE_DOCUMENT), + hasType("*/*"), + hasExtra("android.content.extra.SHOW_ADVANCED", true), + hasCategories(hasItem(Intent.CATEGORY_OPENABLE)) + )).respondWith( + new ActivityResult(Activity.RESULT_OK, data) + ); + } + + @TargetApi(VERSION_CODES.KITKAT) + private void handleOpenFileIntentKitKat(File file) { + Intent data = new Intent(); + data.setData(Uri.fromFile(file)); + + Intents.intending(allOf( + hasAction(Intent.ACTION_OPEN_DOCUMENT), + hasType("*/*"), + hasCategories(hasItem(Intent.CATEGORY_OPENABLE)) + // hasExtraWithKey(Intent.EXTRA_ALLOW_MULTIPLE) + )).respondWith( + new ActivityResult(Activity.RESULT_OK, data) + ); + } + + @Test + public void testSignVerify() throws Exception { + + String cleartext = randomString(10, 30); + + // navigate to 'encrypt text' + onView(withId(R.id.encrypt_text)).perform(click()); + + { // sign + + onView(withId(R.id.encrypt_copy)).perform(click()); + checkSnackbar(Style.ERROR, R.string.error_empty_text); + + onView(withId(R.id.result_signature_icon)).check(matches(withDisplayedChild(0))); + onView(withId(R.id.sign)).perform(click()); + onData(withKeyItemId(0x9D604D2F310716A3L)) + .inAdapterView(isAssignableFrom(AdapterView.class)) + .perform(click()); + onView(withId(R.id.result_signature_icon)).check(matches(withDisplayedChild(1))); + + onView(withId(R.id.encrypt_text_text)).perform(typeText(cleartext)); + + onView(withId(R.id.encrypt_copy)).perform(click()); + + onView(withId(R.id.passphrase_passphrase)).perform(typeText("x")); + onView(withText(R.string.btn_unlock)).perform(click()); + + checkSnackbar(Style.OK, R.string.msg_se_success); + + } + + // go to decrypt from clipboard view + pressBack(); + + onView(withId(R.id.decrypt_from_clipboard)).perform(click()); + + { // decrypt + + onView(isRecyclerItemView(R.id.decrypted_files_list, + hasDescendant(withText(R.string.filename_unknown)))) + .check(matches(allOf(withEncryptionStatus(false), withSignatureMyKey()))); + + // open context menu + onView(allOf(isDescendantOfA(isRecyclerItemView(R.id.decrypted_files_list, + hasDescendant(withText(R.string.filename_unknown)))), + withId(R.id.context_menu))).perform(click()); + + // check if log looks ok + onView(withText(R.string.snackbar_details)).perform(click()); + onView(withText(R.string.msg_dc_clear_signature_ok)).check(matches(isDisplayed())); + pressBack(); + + } + + } + + @Test + public void testGeneralErrorHandling() throws Exception { + + // navigate to encrypt files fragment + onView(withId(R.id.encrypt_files)).perform(click()); + + File[] files = getImageNames(); + + { // encrypt screen + + onView(withId(R.id.encrypt_share)).perform(click()); + checkSnackbar(Style.ERROR, R.string.error_no_file_selected); + + handleAddFileIntent(files[0]); + onView(withId(R.id.file_list_entry_add)).perform(click()); + + handleAddFileIntent(files[1]); + onView(withId(R.id.file_list_entry_add)).perform(click()); + + onView(withId(R.id.encrypt_share)).perform(click()); + checkSnackbar(Style.ERROR, R.string.select_encryption_key); + + onView(withId(R.id.sign)).perform(click()); + onData(withKeyItemId(0x9D604D2F310716A3L)) + .inAdapterView(isAssignableFrom(AdapterView.class)) + .perform(click()); + + onView(withId(R.id.encrypt_share)).perform(click()); + checkSnackbar(Style.ERROR, R.string.error_detached_signature); + + // the EncryptKeyCompletionView is tested individually + onView(withId(R.id.recipient_list)).perform(tokenEncryptViewAddToken(0x9D604D2F310716A3L)); + + onView(withId(R.id.encrypt_save)).perform(click()); + checkSnackbar(Style.ERROR, R.string.error_multi_files); + + openActionBarOverflowOrOptionsMenu(getInstrumentation().getTargetContext()); + onView(withText(R.string.btn_copy_encrypted_signed)).perform(click()); + checkSnackbar(Style.ERROR, R.string.error_multi_clipboard); + + } + + } + +} diff --git a/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/AsymmetricOperationTests.java b/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/ui/AsymmetricTextOperationTests.java similarity index 59% rename from OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/AsymmetricOperationTests.java rename to OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/ui/AsymmetricTextOperationTests.java index 15de32c8a..6b226142c 100644 --- a/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/AsymmetricOperationTests.java +++ b/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/ui/AsymmetricTextOperationTests.java @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package org.sufficientlysecure.keychain; +package org.sufficientlysecure.keychain.ui; import android.app.Activity; @@ -25,13 +25,12 @@ import android.support.test.runner.AndroidJUnit4; import android.test.suitebuilder.annotation.LargeTest; import android.widget.AdapterView; -import org.hamcrest.CoreMatchers; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.service.PassphraseCacheService; -import org.sufficientlysecure.keychain.ui.MainActivity; import org.sufficientlysecure.keychain.ui.util.Notify.Style; import static android.support.test.espresso.Espresso.onData; @@ -39,25 +38,31 @@ import static android.support.test.espresso.Espresso.onView; import static android.support.test.espresso.Espresso.pressBack; import static android.support.test.espresso.action.ViewActions.click; import static android.support.test.espresso.action.ViewActions.typeText; +import static android.support.test.espresso.assertion.ViewAssertions.doesNotExist; import static android.support.test.espresso.assertion.ViewAssertions.matches; import static android.support.test.espresso.contrib.DrawerActions.openDrawer; +import static android.support.test.espresso.matcher.ViewMatchers.hasDescendant; import static android.support.test.espresso.matcher.ViewMatchers.isAssignableFrom; import static android.support.test.espresso.matcher.ViewMatchers.isDescendantOfA; import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; import static android.support.test.espresso.matcher.ViewMatchers.withId; import static android.support.test.espresso.matcher.ViewMatchers.withText; import static org.hamcrest.CoreMatchers.allOf; -import static org.hamcrest.CoreMatchers.not; import static org.sufficientlysecure.keychain.TestHelpers.checkSnackbar; import static org.sufficientlysecure.keychain.TestHelpers.importKeysFromResource; import static org.sufficientlysecure.keychain.TestHelpers.randomString; import static org.sufficientlysecure.keychain.actions.CustomActions.tokenEncryptViewAddToken; +import static org.sufficientlysecure.keychain.matcher.CustomMatchers.isRecyclerItemView; +import static org.sufficientlysecure.keychain.matcher.CustomMatchers.withDisplayedChild; +import static org.sufficientlysecure.keychain.matcher.CustomMatchers.withEncryptionStatus; import static org.sufficientlysecure.keychain.matcher.CustomMatchers.withKeyItemId; -import static org.sufficientlysecure.keychain.matcher.DrawableMatcher.withDrawable; +import static org.sufficientlysecure.keychain.matcher.CustomMatchers.withSignatureMyKey; +import static org.sufficientlysecure.keychain.matcher.CustomMatchers.withSignatureNone; + @RunWith(AndroidJUnit4.class) @LargeTest -public class AsymmetricOperationTests { +public class AsymmetricTextOperationTests { @Rule public final ActivityTestRule mActivity @@ -66,6 +71,7 @@ public class AsymmetricOperationTests { protected Intent getActivityIntent() { Intent intent = super.getActivityIntent(); intent.putExtra(MainActivity.EXTRA_SKIP_FIRST_TIME, true); + intent.putExtra(MainActivity.EXTRA_INIT_FRAG, MainActivity.ID_ENCRYPT_DECRYPT); return intent; } }; @@ -85,8 +91,6 @@ public class AsymmetricOperationTests { public void testTextEncryptDecryptFromToken() throws Exception { // navigate to 'encrypt text' - openDrawer(R.id.drawer_layout); - onView(withText(R.string.nav_encrypt_decrypt)).perform(click()); onView(withId(R.id.encrypt_text)).perform(click()); String cleartext = randomString(10, 30); @@ -94,7 +98,9 @@ public class AsymmetricOperationTests { { // encrypt // the EncryptKeyCompletionView is tested individually + onView(withId(R.id.result_encryption_icon)).check(matches(withDisplayedChild(0))); onView(withId(R.id.recipient_list)).perform(tokenEncryptViewAddToken(0x9D604D2F310716A3L)); + onView(withId(R.id.result_encryption_icon)).check(matches(withDisplayedChild(1))); onView(withId(R.id.encrypt_text_text)).perform(typeText(cleartext)); @@ -109,20 +115,11 @@ public class AsymmetricOperationTests { onView(withId(R.id.passphrase_passphrase)).perform(typeText("x")); onView(withText(R.string.btn_unlock)).perform(click()); - onView(withId(R.id.decrypt_text_plaintext)).check(matches( - withText(cleartext))); - onView(withId(R.id.result_encryption_text)).check(matches( - withText(R.string.decrypt_result_encrypted))); - onView(withId(R.id.result_signature_text)).check(matches( - withText(R.string.decrypt_result_no_signature))); - onView(withId(R.id.result_signature_layout)).check(matches( - not(isDisplayed()))); + onView(isRecyclerItemView(R.id.decrypted_files_list, + hasDescendant(withText(R.string.filename_unknown_text)))) + .check(matches(allOf(withEncryptionStatus(true), withSignatureNone()))); - onView(withId(R.id.result_encryption_icon)).check(matches( - withDrawable(R.drawable.status_lock_closed_24dp))); - onView(withId(R.id.result_signature_icon)).check(matches( - withDrawable(R.drawable.status_signature_unknown_cutout_24dp))); } } @@ -132,6 +129,8 @@ public class AsymmetricOperationTests { String cleartext = randomString(10, 30); + pressBack(); + { // encrypt // navigate to edit key dialog @@ -141,60 +140,8 @@ public class AsymmetricOperationTests { .perform(click()); onView(withId(R.id.view_key_action_encrypt_text)).perform(click()); - onView(withId(R.id.encrypt_text_text)).perform(typeText(cleartext)); - - onView(withId(R.id.encrypt_copy)).perform(click()); - } - - // go to decrypt from clipboard view - pressBack(); - pressBack(); - - openDrawer(R.id.drawer_layout); - onView(withText(R.string.nav_encrypt_decrypt)).perform(click()); - onView(withId(R.id.decrypt_from_clipboard)).perform(click()); - - { // decrypt - - onView(withId(R.id.passphrase_passphrase)).perform(typeText("x")); - onView(withText(R.string.btn_unlock)).perform(click()); - - onView(withId(R.id.decrypt_text_plaintext)).check(matches( - withText(cleartext))); - - onView(withId(R.id.result_encryption_text)).check(matches( - withText(R.string.decrypt_result_encrypted))); - onView(withId(R.id.result_signature_text)).check(matches( - withText(R.string.decrypt_result_no_signature))); - onView(withId(R.id.result_signature_layout)).check(matches( - not(isDisplayed()))); - - onView(withId(R.id.result_encryption_icon)).check(matches( - withDrawable(R.drawable.status_lock_closed_24dp))); - onView(withId(R.id.result_signature_icon)).check(matches( - withDrawable(R.drawable.status_signature_unknown_cutout_24dp))); - - } - - pressBack(); - onView(withId(R.id.decrypt_from_clipboard)).perform(click()); - - { // decrypt again, passphrase should be cached - - onView(withId(R.id.decrypt_text_plaintext)).check(matches( - withText(cleartext))); - - onView(withId(R.id.result_encryption_text)).check(matches( - withText(R.string.decrypt_result_encrypted))); - onView(withId(R.id.result_signature_text)).check(matches( - withText(R.string.decrypt_result_no_signature))); - onView(withId(R.id.result_signature_layout)).check(matches( - not(isDisplayed()))); - - onView(withId(R.id.result_encryption_icon)).check(matches( - withDrawable(R.drawable.status_lock_closed_24dp))); - onView(withId(R.id.result_signature_icon)).check(matches( - withDrawable(R.drawable.status_signature_unknown_cutout_24dp))); + // make sure the encrypt is correctly set + onView(withId(R.id.result_encryption_icon)).check(matches(withDisplayedChild(1))); } @@ -206,8 +153,6 @@ public class AsymmetricOperationTests { String cleartext = randomString(10, 30); // navigate to 'encrypt text' - openDrawer(R.id.drawer_layout); - onView(withText(R.string.nav_encrypt_decrypt)).perform(click()); onView(withId(R.id.encrypt_text)).perform(click()); { // sign @@ -215,11 +160,12 @@ public class AsymmetricOperationTests { onView(withId(R.id.encrypt_copy)).perform(click()); checkSnackbar(Style.ERROR, R.string.error_empty_text); - // navigate to edit key dialog + onView(withId(R.id.result_signature_icon)).check(matches(withDisplayedChild(0))); onView(withId(R.id.sign)).perform(click()); onData(withKeyItemId(0x9D604D2F310716A3L)) .inAdapterView(isAssignableFrom(AdapterView.class)) .perform(click()); + onView(withId(R.id.result_signature_icon)).check(matches(withDisplayedChild(1))); onView(withId(R.id.encrypt_text_text)).perform(typeText(cleartext)); @@ -239,21 +185,19 @@ public class AsymmetricOperationTests { { // decrypt - onView(withId(R.id.decrypt_text_plaintext)).check(matches( - // startsWith because there may be extra newlines - withText(CoreMatchers.startsWith(cleartext)))); + onView(isRecyclerItemView(R.id.decrypted_files_list, + hasDescendant(withText(R.string.filename_unknown)))) + .check(matches(allOf(withEncryptionStatus(false), withSignatureMyKey()))); - onView(withId(R.id.result_encryption_text)).check(matches( - withText(R.string.decrypt_result_not_encrypted))); - onView(withId(R.id.result_signature_text)).check(matches( - withText(R.string.decrypt_result_signature_secret))); - onView(withId(R.id.result_signature_layout)).check(matches( - isDisplayed())); + // open context menu + onView(allOf(isDescendantOfA(isRecyclerItemView(R.id.decrypted_files_list, + hasDescendant(withText(R.string.filename_unknown)))), + withId(R.id.context_menu))).perform(click()); - onView(withId(R.id.result_encryption_icon)).check(matches( - withDrawable(R.drawable.status_lock_open_24dp))); - onView(withId(R.id.result_signature_icon)).check(matches( - withDrawable(R.drawable.status_signature_verified_cutout_24dp))); + // check if log looks ok + onView(withText(R.string.snackbar_details)).perform(click()); + onView(withText(R.string.msg_dc_clear_signature_ok)).check(matches(isDisplayed())); + pressBack(); } diff --git a/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/CreateKeyActivityTest.java b/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/ui/CreateKeyActivityTest.java similarity index 93% rename from OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/CreateKeyActivityTest.java rename to OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/ui/CreateKeyActivityTest.java index 7b7aea477..cf8e7ae12 100644 --- a/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/CreateKeyActivityTest.java +++ b/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/ui/CreateKeyActivityTest.java @@ -15,10 +15,10 @@ * along with this program. If not, see . */ -package org.sufficientlysecure.keychain; +package org.sufficientlysecure.keychain.ui; -import android.content.Intent; +import android.support.test.espresso.matcher.ViewMatchers; import android.support.test.rule.ActivityTestRule; import android.support.test.runner.AndroidJUnit4; import android.test.suitebuilder.annotation.LargeTest; @@ -28,7 +28,7 @@ import android.text.method.PasswordTransformationMethod; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import org.sufficientlysecure.keychain.ui.MainActivity; +import org.sufficientlysecure.keychain.R; import static android.support.test.espresso.Espresso.onView; import static android.support.test.espresso.action.ViewActions.click; @@ -56,15 +56,8 @@ public class CreateKeyActivityTest { public static final String SAMPLE_PASSWORD = "sample_password"; @Rule - public final ActivityTestRule mActivity - = new ActivityTestRule(MainActivity.class) { - @Override - protected Intent getActivityIntent() { - Intent intent = super.getActivityIntent(); - intent.putExtra(MainActivity.EXTRA_SKIP_FIRST_TIME, true); - return intent; - } - }; + public final ActivityTestRule mActivity + = new ActivityTestRule<>(CreateKeyActivity.class); @Test public void testCreateMyKey() { @@ -72,7 +65,7 @@ public class CreateKeyActivityTest { mActivity.getActivity(); // Clicks create my key - onView(withId(R.id.create_key_create_key_button)) + onView(ViewMatchers.withId(R.id.create_key_create_key_button)) .perform(click()); // Clicks next with empty name diff --git a/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/EditKeyTest.java b/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/ui/EditKeyTest.java similarity index 93% rename from OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/EditKeyTest.java rename to OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/ui/EditKeyTest.java index 6773a7b2d..13583818d 100644 --- a/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/EditKeyTest.java +++ b/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/ui/EditKeyTest.java @@ -15,11 +15,12 @@ * along with this program. If not, see . */ -package org.sufficientlysecure.keychain; +package org.sufficientlysecure.keychain.ui; import android.app.Activity; import android.content.Intent; +import android.support.test.espresso.matcher.ViewMatchers; import android.support.test.rule.ActivityTestRule; import android.support.test.runner.AndroidJUnit4; import android.test.suitebuilder.annotation.LargeTest; @@ -30,8 +31,8 @@ import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.MethodSorters; +import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.provider.KeychainDatabase; -import org.sufficientlysecure.keychain.ui.MainActivity; import org.sufficientlysecure.keychain.ui.util.Notify.Style; import static android.support.test.espresso.Espresso.onData; @@ -75,7 +76,7 @@ public class EditKeyTest { // navigate to edit key dialog onData(withKeyItemId(0x9D604D2F310716A3L)) .inAdapterView(allOf(isAssignableFrom(AdapterView.class), - isDescendantOfA(withId(R.id.key_list_list)))) + isDescendantOfA(ViewMatchers.withId(R.id.key_list_list)))) .perform(click()); onView(withId(R.id.menu_key_view_edit)).perform(click()); diff --git a/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/ui/MiscCryptOperationTests.java b/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/ui/MiscCryptOperationTests.java new file mode 100644 index 000000000..96d69e833 --- /dev/null +++ b/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/ui/MiscCryptOperationTests.java @@ -0,0 +1,279 @@ +/* + * Copyright (C) 2015 Vincent Breitmoser + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui; + + +import java.io.File; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.app.Instrumentation.ActivityResult; +import android.content.Intent; +import android.net.Uri; +import android.os.Build.VERSION_CODES; +import android.support.test.InstrumentationRegistry; +import android.support.test.espresso.action.ViewActions; +import android.support.test.espresso.intent.Intents; +import android.support.test.espresso.intent.rule.IntentsTestRule; +import android.support.test.runner.AndroidJUnit4; +import android.test.suitebuilder.annotation.LargeTest; +import android.widget.AdapterView; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.TestHelpers; +import org.sufficientlysecure.keychain.compatibility.ClipboardReflection; +import org.sufficientlysecure.keychain.service.PassphraseCacheService; +import org.sufficientlysecure.keychain.ui.util.Notify.Style; +import org.sufficientlysecure.keychain.util.Preferences; + +import static android.support.test.InstrumentationRegistry.getInstrumentation; +import static android.support.test.espresso.Espresso.onData; +import static android.support.test.espresso.Espresso.onView; +import static android.support.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu; +import static android.support.test.espresso.Espresso.pressBack; +import static android.support.test.espresso.action.ViewActions.click; +import static android.support.test.espresso.assertion.ViewAssertions.matches; +import static android.support.test.espresso.intent.matcher.IntentMatchers.hasAction; +import static android.support.test.espresso.intent.matcher.IntentMatchers.hasCategories; +import static android.support.test.espresso.intent.matcher.IntentMatchers.hasType; +import static android.support.test.espresso.matcher.ViewMatchers.hasDescendant; +import static android.support.test.espresso.matcher.ViewMatchers.hasSibling; +import static android.support.test.espresso.matcher.ViewMatchers.isAssignableFrom; +import static android.support.test.espresso.matcher.ViewMatchers.isChecked; +import static android.support.test.espresso.matcher.ViewMatchers.isDescendantOfA; +import static android.support.test.espresso.matcher.ViewMatchers.isNotChecked; +import static android.support.test.espresso.matcher.ViewMatchers.withChild; +import static android.support.test.espresso.matcher.ViewMatchers.withId; +import static android.support.test.espresso.matcher.ViewMatchers.withText; +import static org.hamcrest.CoreMatchers.allOf; +import static org.hamcrest.CoreMatchers.hasItem; +import static org.sufficientlysecure.keychain.TestHelpers.checkSnackbar; +import static org.sufficientlysecure.keychain.TestHelpers.dismissSnackbar; +import static org.sufficientlysecure.keychain.TestHelpers.getImageNames; +import static org.sufficientlysecure.keychain.TestHelpers.importKeysFromResource; +import static org.sufficientlysecure.keychain.TestHelpers.pickRandom; +import static org.sufficientlysecure.keychain.TestHelpers.randomString; +import static org.sufficientlysecure.keychain.matcher.CustomMatchers.isRecyclerItemView; +import static org.sufficientlysecure.keychain.matcher.CustomMatchers.withDisplayedChild; +import static org.sufficientlysecure.keychain.matcher.CustomMatchers.withKeyItemId; +import static org.sufficientlysecure.keychain.matcher.DrawableMatcher.withDrawable; + + +@RunWith(AndroidJUnit4.class) +@LargeTest +public class MiscCryptOperationTests { + + @Rule + public final IntentsTestRule mActivityRule + = new IntentsTestRule(MainActivity.class) { + @Override + protected Intent getActivityIntent() { + Intent intent = super.getActivityIntent(); + intent.putExtra(MainActivity.EXTRA_SKIP_FIRST_TIME, true); + intent.putExtra(MainActivity.EXTRA_INIT_FRAG, MainActivity.ID_ENCRYPT_DECRYPT); + return intent; + } + }; + private Activity mActivity; + + @Before + public void setUp() throws Exception { + // clear dis shit + Preferences.getPreferences(getInstrumentation().getTargetContext()).clear(); + + mActivity = mActivityRule.getActivity(); + + TestHelpers.copyFiles(); + + // import these two, make sure they're there + importKeysFromResource(mActivity, "x.sec.asc"); + + // make sure no passphrases are cached + PassphraseCacheService.clearCachedPassphrases(mActivity); + } + + @Test + public void testDecryptNonPgpFile() throws Exception { + + // decrypt any non-pgp file + File file = pickRandom(getImageNames()); + handleOpenFileIntentKitKat(file); + onView(withId(R.id.decrypt_files)).perform(click()); + + { // decrypt + + // open context menu + onView(allOf(isDescendantOfA(isRecyclerItemView(R.id.decrypted_files_list, + hasDescendant(allOf( + hasDescendant(withDrawable(R.drawable.status_signature_invalid_cutout_24dp, true)), + hasDescendant(withText(R.string.msg_dc_error_invalid_data)))))), + withId(R.id.result_error_log))).perform(click()); + + } + + } + + @Test + public void testDecryptEmptySelection() throws Exception { + + // decrypt any non-pgp file + handleOpenFileEmptyKitKat(); + onView(withId(R.id.decrypt_files)).perform(click()); + + checkSnackbar(Style.ERROR, R.string.no_file_selected); + + } + + @Test + public void testDecryptNonPgpClipboard() throws Exception { + + // decrypt any non-pgp file + ClipboardReflection.copyToClipboard(mActivity, randomString(0, 50)); + + onView(withId(R.id.decrypt_from_clipboard)).perform(click()); + + { // decrypt + + // open context menu + onView(allOf(isDescendantOfA(isRecyclerItemView(R.id.decrypted_files_list, + hasDescendant(allOf( + hasDescendant(withDrawable(R.drawable.status_signature_invalid_cutout_24dp, true)), + hasDescendant(withText(R.string.msg_dc_error_invalid_data)))))), + withId(R.id.result_error_log))).perform(click()); + + } + + } + + @TargetApi(VERSION_CODES.KITKAT) + private void handleOpenFileEmptyKitKat() { + Intent data = new Intent(); + data.setData(null); + + Intents.intending(allOf( + hasAction(Intent.ACTION_OPEN_DOCUMENT), + hasType("*/*"), + hasCategories(hasItem(Intent.CATEGORY_OPENABLE)) + // hasExtraWithKey(Intent.EXTRA_ALLOW_MULTIPLE) + )).respondWith( + new ActivityResult(Activity.RESULT_OK, data) + ); + } + + @TargetApi(VERSION_CODES.KITKAT) + private void handleOpenFileIntentKitKat(File file) { + Intent data = new Intent(); + data.setData(Uri.fromFile(file)); + + Intents.intending(allOf( + hasAction(Intent.ACTION_OPEN_DOCUMENT), + hasType("*/*"), + hasCategories(hasItem(Intent.CATEGORY_OPENABLE)) + // hasExtraWithKey(Intent.EXTRA_ALLOW_MULTIPLE) + )).respondWith( + new ActivityResult(Activity.RESULT_OK, data) + ); + } + + @Test + public void testEncryptTokenFromKeyView() throws Exception { + + // navigate to edit key dialog + onData(withKeyItemId(0x9D604D2F310716A3L)) + .inAdapterView(allOf(isAssignableFrom(AdapterView.class), + isDescendantOfA(withId(R.id.key_list_list)))) + .perform(click()); + onView(withId(R.id.view_key_action_encrypt_text)).perform(click()); + + // make sure the encrypt is correctly set + onView(withId(R.id.result_encryption_icon)).check(matches(withDisplayedChild(1))); + // TODO check token id + + } + + @Test + public void testMenuSaveDefault() throws Exception { + + onView(withId(R.id.encrypt_files)).perform(click()); + + { // save checked options + + openActionBarOverflowOrOptionsMenu(mActivity); + + // check initial button states + onView(allOf(withId(R.id.checkbox), + hasSibling(withChild(withText(R.string.label_delete_after_encryption))))) + .check(matches(isNotChecked())); + onView(allOf(withId(R.id.checkbox), hasSibling(withChild(withText(R.string.label_enable_compression))))) + .check(matches(isChecked())); + onView(allOf(withId(R.id.checkbox), hasSibling(withChild(withText(R.string.label_encrypt_filenames))))) + .check(matches(isChecked())); + onView(allOf(withId(R.id.checkbox), hasSibling(withChild(withText(R.string.label_file_ascii_armor))))) + .check(matches(isNotChecked())); + + // press some buttons + + onView(withText(R.string.label_enable_compression)).perform(click()); + checkSnackbar(Style.OK, R.string.snack_compression_off); + onView(withText(R.string.btn_save_default)).perform(click()); + checkSnackbar(Style.OK, R.string.btn_saved); + dismissSnackbar(); + + openActionBarOverflowOrOptionsMenu(mActivity); + onView(withText(R.string.label_encrypt_filenames)).perform(click()); + checkSnackbar(Style.OK, R.string.snack_encrypt_filenames_off); + onView(withText(R.string.btn_save_default)).perform(click()); + checkSnackbar(Style.OK, R.string.btn_saved); + dismissSnackbar(); + + openActionBarOverflowOrOptionsMenu(mActivity); + onView(withText(R.string.label_file_ascii_armor)).perform(click()); + checkSnackbar(Style.OK, R.string.snack_armor_on); + onView(withText(R.string.btn_save_default)).perform(click()); + checkSnackbar(Style.OK, R.string.btn_saved); + dismissSnackbar(); + + } + + pressBack(); + onView(withId(R.id.encrypt_files)).perform(click()); + + { // save checked options + + openActionBarOverflowOrOptionsMenu(mActivity); + + // check initial button states (as saved from before!) + onView(allOf(withId(R.id.checkbox), + hasSibling(withChild(withText(R.string.label_delete_after_encryption))))) + .check(matches(isNotChecked())); + onView(allOf(withId(R.id.checkbox), hasSibling(withChild(withText(R.string.label_enable_compression))))) + .check(matches(isNotChecked())); + onView(allOf(withId(R.id.checkbox), hasSibling(withChild(withText(R.string.label_encrypt_filenames))))) + .check(matches(isNotChecked())); + onView(allOf(withId(R.id.checkbox), hasSibling(withChild(withText(R.string.label_file_ascii_armor))))) + .check(matches(isChecked())); + + } + + } + +} diff --git a/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/ui/SymmetricTextOperationTests.java b/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/ui/SymmetricTextOperationTests.java new file mode 100644 index 000000000..3a34f15be --- /dev/null +++ b/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/ui/SymmetricTextOperationTests.java @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2015 Vincent Breitmoser + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui; + + +import android.app.Activity; +import android.app.Instrumentation.ActivityResult; +import android.content.Intent; +import android.support.test.espresso.intent.rule.IntentsTestRule; +import android.support.test.espresso.matcher.ViewMatchers; +import android.support.test.runner.AndroidJUnit4; +import android.test.suitebuilder.annotation.LargeTest; + +import org.junit.FixMethodOrder; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.MethodSorters; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.provider.TemporaryStorageProvider; +import org.sufficientlysecure.keychain.ui.util.Notify.Style; + +import static android.support.test.InstrumentationRegistry.getInstrumentation; +import static android.support.test.espresso.Espresso.onView; +import static android.support.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu; +import static android.support.test.espresso.Espresso.pressBack; +import static android.support.test.espresso.action.ViewActions.click; +import static android.support.test.espresso.action.ViewActions.typeText; +import static android.support.test.espresso.assertion.ViewAssertions.matches; +import static android.support.test.espresso.contrib.DrawerActions.openDrawer; +import static android.support.test.espresso.intent.Intents.intended; +import static android.support.test.espresso.intent.Intents.intending; +import static android.support.test.espresso.intent.matcher.IntentMatchers.hasAction; +import static android.support.test.espresso.intent.matcher.IntentMatchers.hasData; +import static android.support.test.espresso.intent.matcher.IntentMatchers.hasExtra; +import static android.support.test.espresso.intent.matcher.IntentMatchers.hasExtraWithKey; +import static android.support.test.espresso.intent.matcher.IntentMatchers.hasFlags; +import static android.support.test.espresso.intent.matcher.IntentMatchers.hasType; +import static android.support.test.espresso.intent.matcher.UriMatchers.hasHost; +import static android.support.test.espresso.intent.matcher.UriMatchers.hasScheme; +import static android.support.test.espresso.matcher.ViewMatchers.hasDescendant; +import static android.support.test.espresso.matcher.ViewMatchers.isDescendantOfA; +import static android.support.test.espresso.matcher.ViewMatchers.withId; +import static android.support.test.espresso.matcher.ViewMatchers.withText; +import static org.hamcrest.CoreMatchers.allOf; +import static org.hamcrest.Matchers.equalTo; +import static org.sufficientlysecure.keychain.TestHelpers.checkSnackbar; +import static org.sufficientlysecure.keychain.TestHelpers.randomString; +import static org.sufficientlysecure.keychain.matcher.CustomMatchers.isRecyclerItemView; +import static org.sufficientlysecure.keychain.matcher.CustomMatchers.withEncryptionStatus; +import static org.sufficientlysecure.keychain.matcher.CustomMatchers.withSignatureNone; + + +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +@RunWith(AndroidJUnit4.class) +@LargeTest +public class SymmetricTextOperationTests { + + public static final String PASSPHRASE = randomString(5, 20); + + @Rule + public final IntentsTestRule mActivity + = new IntentsTestRule(MainActivity.class) { + @Override + protected Intent getActivityIntent() { + Intent intent = super.getActivityIntent(); + intent.putExtra(MainActivity.EXTRA_SKIP_FIRST_TIME, true); + intent.putExtra(MainActivity.EXTRA_INIT_FRAG, MainActivity.ID_ENCRYPT_DECRYPT); + return intent; + } + }; + + @Test + public void testSymmetricCryptClipboard() throws Exception { + + mActivity.getActivity(); + + String text = randomString(10, 30); + + // navigate to encrypt/decrypt + onView(withId(R.id.encrypt_text)).perform(click()); + + { + onView(withId(R.id.encrypt_text_text)).perform(typeText(text)); + + openActionBarOverflowOrOptionsMenu(getInstrumentation().getTargetContext()); + onView(withText(R.string.label_symmetric)).perform(click()); + + onView(withId(R.id.passphrase)).perform(typeText(PASSPHRASE)); + + onView(withId(R.id.encrypt_copy)).perform(click()); + + checkSnackbar(Style.ERROR, R.string.passphrases_do_not_match); + + onView(withId(R.id.passphraseAgain)).perform(typeText(PASSPHRASE)); + + onView(withId(R.id.encrypt_text_text)).check(matches(withText(text))); + + onView(withId(R.id.encrypt_copy)).perform(click()); + + checkSnackbar(Style.OK, R.string.msg_se_success); + } + + // go to decrypt from clipboard view + pressBack(); + onView(withId(R.id.decrypt_from_clipboard)).perform(click()); + + { + onView(withId(R.id.passphrase_passphrase)).perform(typeText(PASSPHRASE)); + onView(withText(R.string.btn_unlock)).perform(click()); + + onView(isRecyclerItemView(R.id.decrypted_files_list, + hasDescendant(withText(R.string.filename_unknown_text)))) + .check(matches(allOf(withEncryptionStatus(true), withSignatureNone()))); + + intending(allOf( + hasAction("android.intent.action.CHOOSER"), + hasExtra(equalTo(Intent.EXTRA_INTENT), allOf( + hasAction(Intent.ACTION_VIEW), + hasFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION), + hasData(allOf(hasScheme("content"), hasHost(TemporaryStorageProvider.CONTENT_AUTHORITY))), + hasType("text/plain") + )) + )).respondWith(new ActivityResult(Activity.RESULT_OK, null)); + + onView(allOf(isDescendantOfA(isRecyclerItemView(R.id.decrypted_files_list, + hasDescendant(withText(R.string.filename_unknown_text)))), + withId(R.id.file))).perform(click()); + + } + + } + + @Test + public void testSymmetricCryptShare() throws Exception { + + mActivity.getActivity(); + + String text = randomString(10, 30); + + // navigate to encrypt/decrypt + onView(withId(R.id.encrypt_text)).perform(click()); + + { + onView(withId(R.id.encrypt_text_text)).perform(typeText(text)); + + openActionBarOverflowOrOptionsMenu(getInstrumentation().getTargetContext()); + onView(withText(R.string.label_symmetric)).perform(click()); + + onView(withId(R.id.passphrase)).perform(typeText(PASSPHRASE)); + + onView(withId(R.id.passphraseAgain)).perform(typeText(PASSPHRASE)); + + onView(withId(R.id.encrypt_text_text)).check(matches(withText(text))); + + intending(allOf( + hasAction("android.intent.action.CHOOSER"), + hasExtra(equalTo(Intent.EXTRA_INTENT), allOf( + hasAction(Intent.ACTION_SEND), + hasFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION), + hasExtraWithKey(Intent.EXTRA_TEXT), + hasType("text/plain") + )) + )).respondWith(new ActivityResult(Activity.RESULT_OK, null)); + + onView(withId(R.id.encrypt_share)).perform(click()); + + } + + } + + +} diff --git a/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/EncryptKeyCompletionViewTest.java b/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/ui/widget/EncryptKeyCompletionViewTest.java similarity index 93% rename from OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/EncryptKeyCompletionViewTest.java rename to OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/ui/widget/EncryptKeyCompletionViewTest.java index 40cdbd4eb..8618a0a07 100644 --- a/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/EncryptKeyCompletionViewTest.java +++ b/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/ui/widget/EncryptKeyCompletionViewTest.java @@ -15,13 +15,14 @@ * along with this program. If not, see . */ -package org.sufficientlysecure.keychain; +package org.sufficientlysecure.keychain.ui.widget; import android.app.Activity; import android.content.Intent; import android.support.test.espresso.action.ViewActions; import android.support.test.espresso.matcher.RootMatchers; +import android.support.test.espresso.matcher.ViewMatchers; import android.support.test.rule.ActivityTestRule; import android.support.test.runner.AndroidJUnit4; import android.test.suitebuilder.annotation.LargeTest; @@ -31,6 +32,7 @@ import android.widget.AdapterView; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.ui.EncryptTextActivity; import static android.support.test.espresso.Espresso.onData; @@ -67,7 +69,7 @@ public class EncryptKeyCompletionViewTest { importKeysFromResource(activity, "x.sec.asc"); // check if the element passed in from intent - onView(withId(R.id.recipient_list)).check(matches(withKeyToken(0x9D604D2F310716A3L))); + onView(ViewMatchers.withId(R.id.recipient_list)).check(matches(withKeyToken(0x9D604D2F310716A3L))); onView(withId(R.id.recipient_list)).perform(ViewActions.pressKey(KeyEvent.KEYCODE_DEL)); // type X, select from list, check if it's there diff --git a/OpenKeychain/src/main/AndroidManifest.xml b/OpenKeychain/src/main/AndroidManifest.xml index 75d94ae69..d5db3c2da 100644 --- a/OpenKeychain/src/main/AndroidManifest.xml +++ b/OpenKeychain/src/main/AndroidManifest.xml @@ -195,7 +195,7 @@ - - - - - - - - - - - - - - - - - + + + + + + + + + + - + + diff --git a/OpenKeychain/src/main/java/org/spongycastle/openpgp/operator/jcajce/CachingDataDecryptorFactory.java b/OpenKeychain/src/main/java/org/spongycastle/openpgp/operator/jcajce/CachingDataDecryptorFactory.java new file mode 100644 index 000000000..d35f1d751 --- /dev/null +++ b/OpenKeychain/src/main/java/org/spongycastle/openpgp/operator/jcajce/CachingDataDecryptorFactory.java @@ -0,0 +1,76 @@ +/** + * Copyright (c) 2013-2014 Philipp Jakubeit, Signe Rüsch, Dominik Schürmann + * + * Licensed under the Bouncy Castle License (MIT license). See LICENSE file for details. + */ + +package org.spongycastle.openpgp.operator.jcajce; + +import org.spongycastle.jcajce.util.NamedJcaJceHelper; +import org.spongycastle.openpgp.PGPException; +import org.spongycastle.openpgp.PGPPublicKeyEncryptedData; +import org.spongycastle.openpgp.operator.PGPDataDecryptor; +import org.spongycastle.openpgp.operator.PublicKeyDataDecryptorFactory; + +import java.nio.ByteBuffer; +import java.util.Map; + +public class CachingDataDecryptorFactory implements PublicKeyDataDecryptorFactory +{ + private final PublicKeyDataDecryptorFactory mWrappedDecryptor; + private final Map mSessionKeyCache; + + private OperatorHelper mOperatorHelper; + + public CachingDataDecryptorFactory(String providerName, + final Map sessionKeyCache) + { + mWrappedDecryptor = null; + mSessionKeyCache = sessionKeyCache; + + mOperatorHelper = new OperatorHelper(new NamedJcaJceHelper(providerName)); + } + + public CachingDataDecryptorFactory(PublicKeyDataDecryptorFactory wrapped, + final Map sessionKeyCache) + { + mWrappedDecryptor = wrapped; + mSessionKeyCache = sessionKeyCache; + + } + + public boolean hasCachedSessionData(PGPPublicKeyEncryptedData encData) throws PGPException { + ByteBuffer bi = ByteBuffer.wrap(encData.getSessionKey()[0]); + return mSessionKeyCache.containsKey(bi); + } + + public Map getCachedSessionKeys() { + return mSessionKeyCache; + } + + public boolean canDecrypt() { + return mWrappedDecryptor != null; + } + + @Override + public byte[] recoverSessionData(int keyAlgorithm, byte[][] secKeyData) throws PGPException { + ByteBuffer bi = ByteBuffer.wrap(secKeyData[0]); // encoded MPI + if (mSessionKeyCache.containsKey(bi)) { + return mSessionKeyCache.get(bi); + } + + byte[] sessionData = mWrappedDecryptor.recoverSessionData(keyAlgorithm, secKeyData); + mSessionKeyCache.put(bi, sessionData); + return sessionData; + } + + @Override + public PGPDataDecryptor createDataDecryptor(boolean withIntegrityPacket, int encAlgorithm, byte[] key) + throws PGPException { + if (mWrappedDecryptor != null) { + return mWrappedDecryptor.createDataDecryptor(withIntegrityPacket, encAlgorithm, key); + } + return mOperatorHelper.createDataDecryptor(withIntegrityPacket, encAlgorithm, key); + } + +} diff --git a/OpenKeychain/src/main/java/org/spongycastle/openpgp/operator/jcajce/NfcSyncPublicKeyDataDecryptorFactoryBuilder.java b/OpenKeychain/src/main/java/org/spongycastle/openpgp/operator/jcajce/NfcSyncPublicKeyDataDecryptorFactoryBuilder.java deleted file mode 100644 index 067bb3e19..000000000 --- a/OpenKeychain/src/main/java/org/spongycastle/openpgp/operator/jcajce/NfcSyncPublicKeyDataDecryptorFactoryBuilder.java +++ /dev/null @@ -1,278 +0,0 @@ -/** - * Copyright (c) 2013-2014 Philipp Jakubeit, Signe Rüsch, Dominik Schürmann - * - * Licensed under the Bouncy Castle License (MIT license). See LICENSE file for details. - */ - -package org.spongycastle.openpgp.operator.jcajce; - -import org.spongycastle.bcpg.PublicKeyAlgorithmTags; -import org.spongycastle.jcajce.util.DefaultJcaJceHelper; -import org.spongycastle.jcajce.util.NamedJcaJceHelper; -import org.spongycastle.jcajce.util.ProviderJcaJceHelper; -import org.spongycastle.openpgp.PGPException; -import org.spongycastle.openpgp.PGPPublicKey; -import org.spongycastle.openpgp.operator.PGPDataDecryptor; -import org.spongycastle.openpgp.operator.PublicKeyDataDecryptorFactory; - -import java.nio.ByteBuffer; -import java.security.Provider; -import java.util.Map; - - -/** - * This class is based on JcePublicKeyDataDecryptorFactoryBuilder - * - */ -public class NfcSyncPublicKeyDataDecryptorFactoryBuilder -{ - private OperatorHelper helper = new OperatorHelper(new DefaultJcaJceHelper()); - private OperatorHelper contentHelper = new OperatorHelper(new DefaultJcaJceHelper()); - private JcaPGPKeyConverter keyConverter = new JcaPGPKeyConverter(); -// private JcaPGPDigestCalculatorProviderBuilder digestCalculatorProviderBuilder = new JcaPGPDigestCalculatorProviderBuilder(); -// private JcaKeyFingerprintCalculator fingerprintCalculator = new JcaKeyFingerprintCalculator(); - - public static class NfcInteractionNeeded extends RuntimeException - { - public byte[] encryptedSessionKey; - - public NfcInteractionNeeded(byte[] encryptedSessionKey) - { - super("NFC interaction required!"); - this.encryptedSessionKey = encryptedSessionKey; - } - } - - public NfcSyncPublicKeyDataDecryptorFactoryBuilder() - { - } - - /** - * Set the provider object to use for creating cryptographic primitives in the resulting factory the builder produces. - * - * @param provider provider object for cryptographic primitives. - * @return the current builder. - */ - public NfcSyncPublicKeyDataDecryptorFactoryBuilder setProvider(Provider provider) - { - this.helper = new OperatorHelper(new ProviderJcaJceHelper(provider)); - keyConverter.setProvider(provider); - this.contentHelper = helper; - - return this; - } - - /** - * Set the provider name to use for creating cryptographic primitives in the resulting factory the builder produces. - * - * @param providerName the name of the provider to reference for cryptographic primitives. - * @return the current builder. - */ - public NfcSyncPublicKeyDataDecryptorFactoryBuilder setProvider(String providerName) - { - this.helper = new OperatorHelper(new NamedJcaJceHelper(providerName)); - keyConverter.setProvider(providerName); - this.contentHelper = helper; - - return this; - } - - public NfcSyncPublicKeyDataDecryptorFactoryBuilder setContentProvider(Provider provider) - { - this.contentHelper = new OperatorHelper(new ProviderJcaJceHelper(provider)); - - return this; - } - - public NfcSyncPublicKeyDataDecryptorFactoryBuilder setContentProvider(String providerName) - { - this.contentHelper = new OperatorHelper(new NamedJcaJceHelper(providerName)); - - return this; - } - - public PublicKeyDataDecryptorFactory build(final Map nfcDecryptedMap) { - return new PublicKeyDataDecryptorFactory() - { - public byte[] recoverSessionData(int keyAlgorithm, byte[][] secKeyData) - throws PGPException - { - if (keyAlgorithm == PublicKeyAlgorithmTags.ECDH) - { - throw new PGPException("ECDH not supported!"); - } - - return decryptSessionData(keyAlgorithm, secKeyData, nfcDecryptedMap); - } - - public PGPDataDecryptor createDataDecryptor(boolean withIntegrityPacket, int encAlgorithm, byte[] key) - throws PGPException - { - return contentHelper.createDataDecryptor(withIntegrityPacket, encAlgorithm, key); - } - }; - } - -// public PublicKeyDataDecryptorFactory build(final PrivateKey privKey) -// { -// return new PublicKeyDataDecryptorFactory() -// { -// public byte[] recoverSessionData(int keyAlgorithm, byte[][] secKeyData) -// throws PGPException -// { -// if (keyAlgorithm == PublicKeyAlgorithmTags.ECDH) -// { -// throw new PGPException("ECDH requires use of PGPPrivateKey for decryption"); -// } -// return decryptSessionData(keyAlgorithm, privKey, secKeyData); -// } -// -// public PGPDataDecryptor createDataDecryptor(boolean withIntegrityPacket, int encAlgorithm, byte[] key) -// throws PGPException -// { -// return contentHelper.createDataDecryptor(withIntegrityPacket, encAlgorithm, key); -// } -// }; -// } - -// public PublicKeyDataDecryptorFactory build(final PGPPrivateKey privKey, final byte[] nfcDecrypted) -// { -// return new PublicKeyDataDecryptorFactory() -// { -// public byte[] recoverSessionData(int keyAlgorithm, byte[][] secKeyData) -// throws PGPException -// { -// if (keyAlgorithm == PublicKeyAlgorithmTags.ECDH) -// { -// return decryptSessionData(privKey.getPrivateKeyDataPacket(), privKey.getPublicKeyPacket(), secKeyData); -// } -// -// return decryptSessionData(keyAlgorithm, keyConverter.getPrivateKey(privKey), secKeyData, nfcDecrypted); -// } -// -// public PGPDataDecryptor createDataDecryptor(boolean withIntegrityPacket, int encAlgorithm, byte[] key) -// throws PGPException -// { -// return contentHelper.createDataDecryptor(withIntegrityPacket, encAlgorithm, key); -// } -// }; -// } - -// private byte[] decryptSessionData(BCPGKey privateKeyPacket, PublicKeyPacket pubKeyData, byte[][] secKeyData) -// throws PGPException -// { -// ECDHPublicBCPGKey ecKey = (ECDHPublicBCPGKey)pubKeyData.getKey(); -// X9ECParameters x9Params = NISTNamedCurves.getByOID(ecKey.getCurveOID()); -// -// byte[] enc = secKeyData[0]; -// -// int pLen = ((((enc[0] & 0xff) << 8) + (enc[1] & 0xff)) + 7) / 8; -// byte[] pEnc = new byte[pLen]; -// -// System.arraycopy(enc, 2, pEnc, 0, pLen); -// -// byte[] keyEnc = new byte[enc[pLen + 2]]; -// -// System.arraycopy(enc, 2 + pLen + 1, keyEnc, 0, keyEnc.length); -// -// Cipher c = helper.createKeyWrapper(ecKey.getSymmetricKeyAlgorithm()); -// -// ECPoint S = x9Params.getCurve().decodePoint(pEnc).multiply(((ECSecretBCPGKey)privateKeyPacket).getX()).normalize(); -// -// RFC6637KDFCalculator rfc6637KDFCalculator = new RFC6637KDFCalculator(digestCalculatorProviderBuilder.build().get(ecKey.getHashAlgorithm()), ecKey.getSymmetricKeyAlgorithm()); -// Key key = new SecretKeySpec(rfc6637KDFCalculator.createKey(ecKey.getCurveOID(), S, fingerprintCalculator.calculateFingerprint(pubKeyData)), "AESWrap"); -// -// try -// { -// c.init(Cipher.UNWRAP_MODE, key); -// -// Key paddedSessionKey = c.unwrap(keyEnc, "Session", Cipher.SECRET_KEY); -// -// return PGPPad.unpadSessionData(paddedSessionKey.getEncoded()); -// } -// catch (InvalidKeyException e) -// { -// throw new PGPException("error setting asymmetric cipher", e); -// } -// catch (NoSuchAlgorithmException e) -// { -// throw new PGPException("error setting asymmetric cipher", e); -// } -// } - - private byte[] decryptSessionData(int keyAlgorithm, byte[][] secKeyData, - Map nfcDecryptedMap) - throws PGPException - { -// Cipher c1 = helper.createPublicKeyCipher(keyAlgorithm); -// -// try -// { -// c1.init(Cipher.DECRYPT_MODE, privKey); -// } -// catch (InvalidKeyException e) -// { -// throw new PGPException("error setting asymmetric cipher", e); -// } - - if (keyAlgorithm == PGPPublicKey.RSA_ENCRYPT - || keyAlgorithm == PGPPublicKey.RSA_GENERAL) - { - ByteBuffer bi = ByteBuffer.wrap(secKeyData[0]); // encoded MPI - - if (nfcDecryptedMap.containsKey(bi)) { - return nfcDecryptedMap.get(bi); - } else { - // catch this when decryptSessionData() is executed and divert digest to card, - // when doing the operation again reuse nfcDecrypted - throw new NfcInteractionNeeded(bi.array()); - } - -// c1.update(bi, 2, bi.length - 2); - } - else - { - throw new PGPException("ElGamal not supported!"); - -// ElGamalKey k = (ElGamalKey)privKey; -// int size = (k.getParameters().getP().bitLength() + 7) / 8; -// byte[] tmp = new byte[size]; -// -// byte[] bi = secKeyData[0]; // encoded MPI -// if (bi.length - 2 > size) // leading Zero? Shouldn't happen but... -// { -// c1.update(bi, 3, bi.length - 3); -// } -// else -// { -// System.arraycopy(bi, 2, tmp, tmp.length - (bi.length - 2), bi.length - 2); -// c1.update(tmp); -// } -// -// bi = secKeyData[1]; // encoded MPI -// for (int i = 0; i != tmp.length; i++) -// { -// tmp[i] = 0; -// } -// -// if (bi.length - 2 > size) // leading Zero? Shouldn't happen but... -// { -// c1.update(bi, 3, bi.length - 3); -// } -// else -// { -// System.arraycopy(bi, 2, tmp, tmp.length - (bi.length - 2), bi.length - 2); -// c1.update(tmp); -// } - } - -// try -// { -// return c1.doFinal(); -// } -// catch (Exception e) -// { -// throw new PGPException("exception decrypting session data", e); -// } - } -} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/compatibility/ClipboardReflection.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/compatibility/ClipboardReflection.java index 2f2838f70..0ac27833c 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/compatibility/ClipboardReflection.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/compatibility/ClipboardReflection.java @@ -17,6 +17,8 @@ package org.sufficientlysecure.keychain.compatibility; +import android.content.ClipData; +import android.content.ClipboardManager; import android.content.Context; import org.sufficientlysecure.keychain.Constants; @@ -28,72 +30,24 @@ public class ClipboardReflection { private static final String clipboardLabel = "Keychain"; - /** - * Wrapper around ClipboardManager based on Android version using Reflection API - * - * @param context - * @param text - */ public static void copyToClipboard(Context context, String text) { - Object clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE); - try { - if ("android.text.ClipboardManager".equals(clipboard.getClass().getName())) { - Method methodSetText = clipboard.getClass() - .getMethod("setText", CharSequence.class); - methodSetText.invoke(clipboard, text); - } else if ("android.content.ClipboardManager".equals(clipboard.getClass().getName())) { - Class classClipData = Class.forName("android.content.ClipData"); - Method methodNewPlainText = classClipData.getMethod("newPlainText", - CharSequence.class, CharSequence.class); - Object clip = methodNewPlainText.invoke(null, clipboardLabel, text); - methodNewPlainText = clipboard.getClass() - .getMethod("setPrimaryClip", classClipData); - methodNewPlainText.invoke(clipboard, clip); - } - } catch (Exception e) { - Log.e(Constants.TAG, "There was an error copying the text to the clipboard", e); - } + ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); + + ClipData clip = ClipData.newPlainText(clipboardLabel, text); + clipboard.setPrimaryClip(clip); + } - /** - * Wrapper around ClipboardManager based on Android version using Reflection API - * - * @param context - */ public static CharSequence getClipboardText(Context context) { - Object clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE); - try { - if ("android.text.ClipboardManager".equals(clipboard.getClass().getName())) { - // CharSequence text = clipboard.getText(); - Method methodGetText = clipboard.getClass().getMethod("getText"); - Object text = methodGetText.invoke(clipboard); + ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); - return (CharSequence) text; - } else if ("android.content.ClipboardManager".equals(clipboard.getClass().getName())) { - // ClipData clipData = clipboard.getPrimaryClip(); - Method methodGetPrimaryClip = clipboard.getClass().getMethod("getPrimaryClip"); - Object clipData = methodGetPrimaryClip.invoke(clipboard); - - if (clipData == null) { - return null; - } - - // ClipData.Item clipDataItem = clipData.getItemAt(0); - Method methodGetItemAt = clipData.getClass().getMethod("getItemAt", int.class); - Object clipDataItem = methodGetItemAt.invoke(clipData, 0); - - // CharSequence text = clipDataItem.coerceToText(context); - Method methodGetString = clipDataItem.getClass().getMethod("coerceToText", - Context.class); - Object text = methodGetString.invoke(clipDataItem, context); - - return (CharSequence) text; - } else { - return null; - } - } catch (Exception e) { - Log.e(Constants.TAG, "There was an error getting the text from the clipboard", e); + ClipData clip = clipboard.getPrimaryClip(); + if (clip == null || clip.getItemCount() == 0) { + Log.e(Constants.TAG, "No clipboard data!"); return null; } + + ClipData.Item item = clip.getItemAt(0); + return item.coerceToText(context); } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/CertifyResult.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/CertifyResult.java index 0a0e63330..a9f8170d9 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/CertifyResult.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/CertifyResult.java @@ -132,7 +132,7 @@ public class CertifyResult extends InputPendingResult { intent.putExtra(LogDisplayFragment.EXTRA_RESULT, CertifyResult.this); activity.startActivity(intent); } - }, R.string.view_log); + }, R.string.snackbar_details); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/DecryptVerifyResult.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/DecryptVerifyResult.java index ac571390a..25a86f137 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/DecryptVerifyResult.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/DecryptVerifyResult.java @@ -22,6 +22,7 @@ import android.os.Parcel; import org.openintents.openpgp.OpenPgpMetadata; import org.openintents.openpgp.OpenPgpSignatureResult; +import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; import org.sufficientlysecure.keychain.util.Passphrase; @@ -36,6 +37,8 @@ public class DecryptVerifyResult extends InputPendingResult { // https://tools.ietf.org/html/rfc4880#page56 String mCharset; + CryptoInputParcel mCachedCryptoInputParcel; + byte[] mOutputBytes; public DecryptVerifyResult(int result, OperationLog log) { @@ -50,6 +53,7 @@ public class DecryptVerifyResult extends InputPendingResult { super(source); mSignatureResult = source.readParcelable(OpenPgpSignatureResult.class.getClassLoader()); mDecryptMetadata = source.readParcelable(OpenPgpMetadata.class.getClassLoader()); + mCachedCryptoInputParcel = source.readParcelable(CryptoInputParcel.class.getClassLoader()); } @@ -65,6 +69,14 @@ public class DecryptVerifyResult extends InputPendingResult { mSignatureResult = signatureResult; } + public CryptoInputParcel getCachedCryptoInputParcel() { + return mCachedCryptoInputParcel; + } + + public void setCachedCryptoInputParcel(CryptoInputParcel cachedCryptoInputParcel) { + mCachedCryptoInputParcel = cachedCryptoInputParcel; + } + public OpenPgpMetadata getDecryptMetadata() { return mDecryptMetadata; } @@ -97,6 +109,7 @@ public class DecryptVerifyResult extends InputPendingResult { super.writeToParcel(dest, flags); dest.writeParcelable(mSignatureResult, 0); dest.writeParcelable(mDecryptMetadata, 0); + dest.writeParcelable(mCachedCryptoInputParcel, 0); } public static final Creator CREATOR = new Creator() { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/DeleteResult.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/DeleteResult.java index 50f49add2..52ff8bf44 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/DeleteResult.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/DeleteResult.java @@ -124,7 +124,7 @@ public class DeleteResult extends OperationResult { intent.putExtra(LogDisplayFragment.EXTRA_RESULT, DeleteResult.this); activity.startActivity(intent); } - }, R.string.view_log); + }, R.string.snackbar_details); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/ImportKeyResult.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/ImportKeyResult.java index 1438ad698..2a032cef2 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/ImportKeyResult.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/ImportKeyResult.java @@ -190,7 +190,7 @@ public class ImportKeyResult extends OperationResult { intent.putExtra(LogDisplayFragment.EXTRA_RESULT, ImportKeyResult.this); activity.startActivity(intent); } - }, R.string.view_log); + }, R.string.snackbar_details); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/OperationResult.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/OperationResult.java index f110b9186..c9e427462 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/OperationResult.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/OperationResult.java @@ -253,7 +253,7 @@ public abstract class OperationResult implements Parcelable { intent.putExtra(LogDisplayFragment.EXTRA_RESULT, OperationResult.this); activity.startActivity(intent); } - }, R.string.view_log); + }, R.string.snackbar_details); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedSecretKey.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedSecretKey.java index 17d342341..31a3925da 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedSecretKey.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedSecretKey.java @@ -21,23 +21,18 @@ package org.sufficientlysecure.keychain.pgp; import org.spongycastle.bcpg.S2K; import org.spongycastle.openpgp.PGPException; import org.spongycastle.openpgp.PGPPrivateKey; -import org.spongycastle.openpgp.PGPPublicKey; -import org.spongycastle.openpgp.PGPPublicKeyRing; import org.spongycastle.openpgp.PGPSecretKey; import org.spongycastle.openpgp.PGPSignature; import org.spongycastle.openpgp.PGPSignatureGenerator; import org.spongycastle.openpgp.PGPSignatureSubpacketGenerator; -import org.spongycastle.openpgp.PGPSignatureSubpacketVector; -import org.spongycastle.openpgp.PGPUserAttributeSubpacketVector; import org.spongycastle.openpgp.operator.PBESecretKeyDecryptor; import org.spongycastle.openpgp.operator.PGPContentSignerBuilder; -import org.spongycastle.openpgp.operator.PublicKeyDataDecryptorFactory; +import org.spongycastle.openpgp.operator.jcajce.CachingDataDecryptorFactory; import org.spongycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder; import org.spongycastle.openpgp.operator.jcajce.JcaPGPKeyConverter; import org.spongycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder; import org.spongycastle.openpgp.operator.jcajce.JcePublicKeyDataDecryptorFactoryBuilder; import org.spongycastle.openpgp.operator.jcajce.NfcSyncPGPContentSignerBuilder; -import org.spongycastle.openpgp.operator.jcajce.NfcSyncPublicKeyDataDecryptorFactoryBuilder; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; @@ -51,7 +46,6 @@ import java.security.interfaces.RSAPrivateCrtKey; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; -import java.util.List; import java.util.Map; @@ -270,19 +264,20 @@ public class CanonicalizedSecretKey extends CanonicalizedPublicKey { } } - public PublicKeyDataDecryptorFactory getDecryptorFactory(CryptoInputParcel cryptoInput) { + public CachingDataDecryptorFactory getCachingDecryptorFactory(CryptoInputParcel cryptoInput) { if (mPrivateKeyState == PRIVATE_KEY_STATE_LOCKED) { throw new PrivateKeyNotUnlockedException(); } if (mPrivateKeyState == PRIVATE_KEY_STATE_DIVERT_TO_CARD) { - return new NfcSyncPublicKeyDataDecryptorFactoryBuilder() - .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build( - cryptoInput.getCryptoData() - ); + return new CachingDataDecryptorFactory( + Constants.BOUNCY_CASTLE_PROVIDER_NAME, + cryptoInput.getCryptoData()); } else { - return new JcePublicKeyDataDecryptorFactoryBuilder() - .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(mPrivateKey); + return new CachingDataDecryptorFactory( + new JcePublicKeyDataDecryptorFactoryBuilder() + .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(mPrivateKey), + cryptoInput.getCryptoData()); } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerify.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerify.java index e3059defb..6a85ce251 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerify.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerify.java @@ -40,11 +40,10 @@ import org.spongycastle.openpgp.PGPUtil; import org.spongycastle.openpgp.jcajce.JcaPGPObjectFactory; import org.spongycastle.openpgp.operator.PBEDataDecryptorFactory; import org.spongycastle.openpgp.operator.PGPDigestCalculatorProvider; -import org.spongycastle.openpgp.operator.PublicKeyDataDecryptorFactory; +import org.spongycastle.openpgp.operator.jcajce.CachingDataDecryptorFactory; import org.spongycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider; import org.spongycastle.openpgp.operator.jcajce.JcaPGPDigestCalculatorProviderBuilder; import org.spongycastle.openpgp.operator.jcajce.JcePBEDataDecryptorFactoryBuilder; -import org.spongycastle.openpgp.operator.jcajce.NfcSyncPublicKeyDataDecryptorFactoryBuilder; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.operations.BaseOperation; @@ -538,24 +537,33 @@ public class PgpDecryptVerify extends BaseOperation currentProgress += 2; updateProgress(R.string.progress_preparing_streams, currentProgress, 100); - try { - PublicKeyDataDecryptorFactory decryptorFactory - = secretEncryptionKey.getDecryptorFactory(cryptoInput); - try { - clear = encryptedDataAsymmetric.getDataStream(decryptorFactory); - } catch (PGPKeyValidationException | ArrayIndexOutOfBoundsException e) { - log.add(LogType.MSG_DC_ERROR_CORRUPT_DATA, indent + 1); - return new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log); - } + CachingDataDecryptorFactory decryptorFactory + = secretEncryptionKey.getCachingDecryptorFactory(cryptoInput); + + // special case: if the decryptor does not have a session key cached for this encrypted + // data, and can't actually decrypt on its own, return a pending intent + if (!decryptorFactory.canDecrypt() + && !decryptorFactory.hasCachedSessionData(encryptedDataAsymmetric)) { - symmetricEncryptionAlgo = encryptedDataAsymmetric.getSymmetricAlgorithm(decryptorFactory); - } catch (NfcSyncPublicKeyDataDecryptorFactoryBuilder.NfcInteractionNeeded e) { log.add(LogType.MSG_DC_PENDING_NFC, indent + 1); return new DecryptVerifyResult(log, RequiredInputParcel.createNfcDecryptOperation( secretEncryptionKey.getRing().getMasterKeyId(), - secretEncryptionKey.getKeyId(), e.encryptedSessionKey + secretEncryptionKey.getKeyId(), encryptedDataAsymmetric.getSessionKey()[0] )); + } + + try { + clear = encryptedDataAsymmetric.getDataStream(decryptorFactory); + } catch (PGPKeyValidationException | ArrayIndexOutOfBoundsException e) { + log.add(LogType.MSG_DC_ERROR_CORRUPT_DATA, indent + 1); + return new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log); + } + + symmetricEncryptionAlgo = encryptedDataAsymmetric.getSymmetricAlgorithm(decryptorFactory); + + cryptoInput.addCryptoData(decryptorFactory.getCachedSessionKeys()); + encryptedData = encryptedDataAsymmetric; } else { // there wasn't even any useful data @@ -662,9 +670,6 @@ public class PgpDecryptVerify extends BaseOperation PGPLiteralData literalData = (PGPLiteralData) dataChunk; - // reported size may be null if partial packets are involved (highly unlikely though) - Long originalSize = literalData.getDataLengthIfAvailable(); - String originalFilename = literalData.getFileName(); String mimeType = null; if (literalData.getFormat() == PGPLiteralData.TEXT @@ -687,12 +692,6 @@ public class PgpDecryptVerify extends BaseOperation } } - metadata = new OpenPgpMetadata( - originalFilename, - mimeType, - literalData.getModificationTime().getTime(), - originalSize == null ? 0 : originalSize); - if (!"".equals(originalFilename)) { log.add(LogType.MSG_DC_CLEAR_META_FILE, indent + 1, originalFilename); } @@ -700,15 +699,26 @@ public class PgpDecryptVerify extends BaseOperation mimeType); log.add(LogType.MSG_DC_CLEAR_META_TIME, indent + 1, new Date(literalData.getModificationTime().getTime()).toString()); - if (originalSize != null) { - log.add(LogType.MSG_DC_CLEAR_META_SIZE, indent + 1, - Long.toString(originalSize)); - } else { - log.add(LogType.MSG_DC_CLEAR_META_SIZE_UNKNOWN, indent + 1); - } // return here if we want to decrypt the metadata only if (input.isDecryptMetadataOnly()) { + + // this operation skips the entire stream to find the data length! + Long originalSize = literalData.findDataLength(); + + if (originalSize != null) { + log.add(LogType.MSG_DC_CLEAR_META_SIZE, indent + 1, + Long.toString(originalSize)); + } else { + log.add(LogType.MSG_DC_CLEAR_META_SIZE_UNKNOWN, indent + 1); + } + + metadata = new OpenPgpMetadata( + originalFilename, + mimeType, + literalData.getModificationTime().getTime(), + originalSize == null ? 0 : originalSize); + log.add(LogType.MSG_DC_OK_META_ONLY, indent); DecryptVerifyResult result = new DecryptVerifyResult(DecryptVerifyResult.RESULT_OK, log); @@ -824,6 +834,7 @@ public class PgpDecryptVerify extends BaseOperation // Return a positive result, with metadata and verification info DecryptVerifyResult result = new DecryptVerifyResult(DecryptVerifyResult.RESULT_OK, log); + result.setCachedCryptoInputParcel(cryptoInput); result.setDecryptMetadata(metadata); result.setSignatureResult(signatureResultBuilder.build()); result.setCharset(charset); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpHelper.java index d8b86a18c..32718fb4e 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpHelper.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpHelper.java @@ -21,6 +21,7 @@ package org.sufficientlysecure.keychain.pgp; import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager.NameNotFoundException; +import android.text.TextUtils; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; @@ -31,6 +32,7 @@ import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; import java.security.SecureRandom; +import java.util.regex.Matcher; import java.util.regex.Pattern; public class PgpHelper { @@ -52,9 +54,6 @@ public class PgpHelper { *

* TODO: Does this really help on flash storage? * - * @param context - * @param progressable - * @param file * @throws IOException */ public static void deleteFileSecurely(Context context, Progressable progressable, File file) @@ -78,4 +77,72 @@ public class PgpHelper { raf.close(); file.delete(); } + + /** + * Fixing broken PGP MESSAGE Strings coming from GMail/AOSP Mail + */ + public static String fixPgpMessage(String message) { + // windows newline -> unix newline + message = message.replaceAll("\r\n", "\n"); + // Mac OS before X newline -> unix newline + message = message.replaceAll("\r", "\n"); + + // remove whitespaces before newline + message = message.replaceAll(" +\n", "\n"); + // only two consecutive newlines are allowed + message = message.replaceAll("\n\n+", "\n\n"); + + // replace non breakable spaces + message = message.replaceAll("\\xa0", " "); + + return message; + } + + /** + * Fixing broken PGP SIGNED MESSAGE Strings coming from GMail/AOSP Mail + */ + public static String fixPgpCleartextSignature(CharSequence input) { + if (!TextUtils.isEmpty(input)) { + String text = input.toString(); + + // windows newline -> unix newline + text = text.replaceAll("\r\n", "\n"); + // Mac OS before X newline -> unix newline + text = text.replaceAll("\r", "\n"); + + return text; + } else { + return null; + } + } + + public static String getPgpContent(CharSequence input) { + // only decrypt if clipboard content is available and a pgp message or cleartext signature + if (!TextUtils.isEmpty(input)) { + Log.dEscaped(Constants.TAG, "input: " + input); + + Matcher matcher = PgpHelper.PGP_MESSAGE.matcher(input); + if (matcher.matches()) { + String text = matcher.group(1); + text = fixPgpMessage(text); + + Log.dEscaped(Constants.TAG, "input fixed: " + text); + return text; + } else { + matcher = PgpHelper.PGP_CLEARTEXT_SIGNATURE.matcher(input); + if (matcher.matches()) { + String text = matcher.group(1); + text = fixPgpCleartextSignature(text); + + Log.dEscaped(Constants.TAG, "input fixed: " + text); + return text; + } else { + return null; + } + } + } else { + return null; + } + } + } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/TemporaryStorageProvider.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/TemporaryStorageProvider.java index 2000a6525..b1f1a7d9e 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/TemporaryStorageProvider.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/TemporaryStorageProvider.java @@ -18,14 +18,24 @@ package org.sufficientlysecure.keychain.provider; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.UUID; + +import android.content.ClipDescription; import android.content.ContentProvider; import android.content.ContentValues; import android.content.Context; +import android.content.res.AssetFileDescriptor; import android.database.Cursor; import android.database.MatrixCursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.net.Uri; +import android.os.Bundle; +import android.os.CancellationSignal; import android.os.ParcelFileDescriptor; import android.provider.OpenableColumns; @@ -33,11 +43,6 @@ import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.util.DatabaseUtil; import org.sufficientlysecure.keychain.util.Log; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.util.UUID; - public class TemporaryStorageProvider extends ContentProvider { private static final String DB_NAME = "tempstorage.db"; @@ -45,18 +50,37 @@ public class TemporaryStorageProvider extends ContentProvider { private static final String COLUMN_ID = "id"; private static final String COLUMN_NAME = "name"; private static final String COLUMN_TIME = "time"; + private static final String COLUMN_TYPE = "mimetype"; public static final String CONTENT_AUTHORITY = Constants.TEMPSTORAGE_AUTHORITY; private static final Uri BASE_URI = Uri.parse("content://" + CONTENT_AUTHORITY); - private static final int DB_VERSION = 2; + private static final int DB_VERSION = 3; private static File cacheDir; + public static Uri createFile(Context context, String targetName, String mimeType) { + ContentValues contentValues = new ContentValues(); + contentValues.put(COLUMN_NAME, targetName); + contentValues.put(COLUMN_TYPE, mimeType); + return context.getContentResolver().insert(BASE_URI, contentValues); + } + public static Uri createFile(Context context, String targetName) { ContentValues contentValues = new ContentValues(); contentValues.put(COLUMN_NAME, targetName); return context.getContentResolver().insert(BASE_URI, contentValues); } + public static Uri createFile(Context context) { + ContentValues contentValues = new ContentValues(); + return context.getContentResolver().insert(BASE_URI, contentValues); + } + + public static int setMimeType(Context context, Uri uri, String mimetype) { + ContentValues values = new ContentValues(); + values.put(COLUMN_TYPE, mimetype); + return context.getContentResolver().update(uri, values, null, null); + } + public static int cleanUp(Context context) { return context.getContentResolver().delete(BASE_URI, COLUMN_TIME + "< ?", new String[]{Long.toString(System.currentTimeMillis() - Constants.TEMPFILE_TTL)}); @@ -73,6 +97,7 @@ public class TemporaryStorageProvider extends ContentProvider { db.execSQL("CREATE TABLE IF NOT EXISTS " + TABLE_FILES + " (" + COLUMN_ID + " TEXT PRIMARY KEY, " + COLUMN_NAME + " TEXT, " + + COLUMN_TYPE + " TEXT, " + COLUMN_TIME + " INTEGER" + ");"); } @@ -89,6 +114,8 @@ public class TemporaryStorageProvider extends ContentProvider { COLUMN_NAME + " TEXT, " + COLUMN_TIME + " INTEGER" + ");"); + case 2: + db.execSQL("ALTER TABLE files ADD COLUMN " + COLUMN_TYPE + " TEXT"); } } } @@ -139,11 +166,32 @@ public class TemporaryStorageProvider extends ContentProvider { @Override public String getType(Uri uri) { - // Note: If we can find a files mime type, we can decrypt it to temp storage and open it after - // encryption. The mime type is needed, else UI really sucks and some apps break. + Cursor cursor = db.getReadableDatabase().query(TABLE_FILES, + new String[]{ COLUMN_TYPE }, COLUMN_ID + "=?", + new String[]{ uri.getLastPathSegment() }, null, null, null); + if (cursor != null) { + try { + if (cursor.moveToNext()) { + if (!cursor.isNull(0)) { + return cursor.getString(0); + } + } + } finally { + cursor.close(); + } + } return "*/*"; } + @Override + public String[] getStreamTypes(Uri uri, String mimeTypeFilter) { + String type = getType(uri); + if (ClipDescription.compareMimeTypes(type, mimeTypeFilter)) { + return new String[] { type }; + } + return null; + } + @Override public Uri insert(Uri uri, ContentValues values) { if (!values.containsKey(COLUMN_TIME)) { @@ -162,10 +210,13 @@ public class TemporaryStorageProvider extends ContentProvider { @Override public int delete(Uri uri, String selection, String[] selectionArgs) { - if (uri.getLastPathSegment() != null) { - selection = DatabaseUtil.concatenateWhere(selection, COLUMN_ID + "=?"); - selectionArgs = DatabaseUtil.appendSelectionArgs(selectionArgs, new String[]{uri.getLastPathSegment()}); + if (uri == null || uri.getLastPathSegment() == null) { + return 0; } + + selection = DatabaseUtil.concatenateWhere(selection, COLUMN_ID + "=?"); + selectionArgs = DatabaseUtil.appendSelectionArgs(selectionArgs, new String[]{uri.getLastPathSegment()}); + Cursor files = db.getReadableDatabase().query(TABLE_FILES, new String[]{COLUMN_ID}, selection, selectionArgs, null, null, null); if (files != null) { @@ -180,11 +231,19 @@ public class TemporaryStorageProvider extends ContentProvider { @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { - throw new UnsupportedOperationException("Update not supported"); + if (values.size() != 1 || !values.containsKey(COLUMN_TYPE)) { + throw new UnsupportedOperationException("Update supported only for type field!"); + } + if (selection != null || selectionArgs != null) { + throw new UnsupportedOperationException("Update supported only for plain uri!"); + } + return db.getWritableDatabase().update(TABLE_FILES, values, + COLUMN_ID + " = ?", new String[]{ uri.getLastPathSegment() }); } @Override public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { return openFileHelper(uri, mode); } + } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ServiceProgressHandler.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ServiceProgressHandler.java index 76aa1a618..989b0c4bd 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ServiceProgressHandler.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ServiceProgressHandler.java @@ -17,6 +17,7 @@ package org.sufficientlysecure.keychain.service; + import android.app.ProgressDialog; import android.os.Bundle; import android.os.Handler; @@ -97,24 +98,15 @@ public class ServiceProgressHandler extends Handler { public void handleMessage(Message message) { Bundle data = message.getData(); - ProgressDialogFragment progressDialogFragment = - (ProgressDialogFragment) mActivity.getSupportFragmentManager() - .findFragmentByTag("progressDialog"); - - if (progressDialogFragment == null) { - Log.e(Constants.TAG, "Progress has not been updated because mProgressDialogFragment was null!"); - return; - } - MessageStatus status = MessageStatus.fromInt(message.arg1); switch (status) { case OKAY: - progressDialogFragment.dismissAllowingStateLoss(); + dismissAllowingStateLoss(); break; case EXCEPTION: - progressDialogFragment.dismissAllowingStateLoss(); + dismissAllowingStateLoss(); // show error from service if (data.containsKey(DATA_ERROR)) { @@ -128,23 +120,25 @@ public class ServiceProgressHandler extends Handler { case UPDATE_PROGRESS: if (data.containsKey(DATA_PROGRESS) && data.containsKey(DATA_PROGRESS_MAX)) { + String msg = null; + int progress = data.getInt(DATA_PROGRESS); + int max = data.getInt(DATA_PROGRESS_MAX); + // update progress from service if (data.containsKey(DATA_MESSAGE)) { - progressDialogFragment.setProgress(data.getString(DATA_MESSAGE), - data.getInt(DATA_PROGRESS), data.getInt(DATA_PROGRESS_MAX)); + msg = data.getString(DATA_MESSAGE); } else if (data.containsKey(DATA_MESSAGE_ID)) { - progressDialogFragment.setProgress(data.getInt(DATA_MESSAGE_ID), - data.getInt(DATA_PROGRESS), data.getInt(DATA_PROGRESS_MAX)); - } else { - progressDialogFragment.setProgress(data.getInt(DATA_PROGRESS), - data.getInt(DATA_PROGRESS_MAX)); + msg = mActivity.getString(data.getInt(DATA_MESSAGE_ID)); } + + onSetProgress(msg, progress, max); + } break; case PREVENT_CANCEL: - progressDialogFragment.setPreventCancel(true); + setPreventCancel(true); break; default: @@ -152,4 +146,48 @@ public class ServiceProgressHandler extends Handler { break; } } + + private void setPreventCancel(boolean preventCancel) { + ProgressDialogFragment progressDialogFragment = + (ProgressDialogFragment) mActivity.getSupportFragmentManager() + .findFragmentByTag("progressDialog"); + + if (progressDialogFragment == null) { + return; + } + + progressDialogFragment.setPreventCancel(preventCancel); + } + + protected void dismissAllowingStateLoss() { + ProgressDialogFragment progressDialogFragment = + (ProgressDialogFragment) mActivity.getSupportFragmentManager() + .findFragmentByTag("progressDialog"); + + if (progressDialogFragment == null) { + return; + } + + progressDialogFragment.dismissAllowingStateLoss(); + } + + + protected void onSetProgress(String msg, int progress, int max) { + + ProgressDialogFragment progressDialogFragment = + (ProgressDialogFragment) mActivity.getSupportFragmentManager() + .findFragmentByTag("progressDialog"); + + if (progressDialogFragment == null) { + return; + } + + if (msg != null) { + progressDialogFragment.setProgress(msg, progress, max); + } else { + progressDialogFragment.setProgress(progress, max); + } + + } + } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/input/CryptoInputParcel.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/input/CryptoInputParcel.java index 3d1ccaca1..ee7caf2d8 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/input/CryptoInputParcel.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/input/CryptoInputParcel.java @@ -97,8 +97,12 @@ public class CryptoInputParcel implements Parcelable { mCryptoData.put(ByteBuffer.wrap(hash), signedHash); } + public void addCryptoData(Map cachedSessionKeys) { + mCryptoData.putAll(cachedSessionKeys); + } + public Map getCryptoData() { - return Collections.unmodifiableMap(mCryptoData); + return mCryptoData; } public Date getSignatureTime() { @@ -138,4 +142,5 @@ public class CryptoInputParcel implements Parcelable { b.append("}"); return b.toString(); } + } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptActivity.java new file mode 100644 index 000000000..4375be740 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptActivity.java @@ -0,0 +1,199 @@ +/* + * Copyright (C) 2014 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.ui; + + +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; + +import android.app.Activity; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentTransaction; +import android.view.View; +import android.widget.Toast; + +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.intents.OpenKeychainIntents; +import org.sufficientlysecure.keychain.provider.TemporaryStorageProvider; +import org.sufficientlysecure.keychain.ui.base.BaseActivity; + + +public class DecryptActivity extends BaseActivity { + + /* Intents */ + public static final String ACTION_DECRYPT_FROM_CLIPBOARD = "DECRYPT_DATA_CLIPBOARD"; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setFullScreenDialogClose(new View.OnClickListener() { + @Override + public void onClick(View v) { + setResult(Activity.RESULT_CANCELED); + finish(); + } + }, false); + + // Handle intent actions + handleActions(savedInstanceState, getIntent()); + } + + @Override + protected void initLayout() { + setContentView(R.layout.decrypt_files_activity); + } + + /** + * Handles all actions with this intent + */ + private void handleActions(Bundle savedInstanceState, Intent intent) { + + // No need to initialize fragments if we are just being restored + if (savedInstanceState != null) { + return; + } + + ArrayList uris = new ArrayList<>(); + + String action = intent.getAction(); + + if (action == null) { + Toast.makeText(this, "Error: No action specified!", Toast.LENGTH_LONG).show(); + setResult(Activity.RESULT_CANCELED); + finish(); + return; + } + + try { + + switch (action) { + case Intent.ACTION_SEND: { + // When sending to Keychain Decrypt via share menu + // Binary via content provider (could also be files) + // override uri to get stream from send + if (intent.hasExtra(Intent.EXTRA_STREAM)) { + uris.add(intent.getParcelableExtra(Intent.EXTRA_STREAM)); + } else if (intent.hasExtra(Intent.EXTRA_TEXT)) { + String text = intent.getStringExtra(Intent.EXTRA_TEXT); + Uri uri = readToTempFile(text); + uris.add(uri); + } + + break; + } + + case Intent.ACTION_SEND_MULTIPLE: { + if (intent.hasExtra(Intent.EXTRA_STREAM)) { + uris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM); + } else if (intent.hasExtra(Intent.EXTRA_TEXT)) { + for (String text : intent.getStringArrayListExtra(Intent.EXTRA_TEXT)) { + Uri uri = readToTempFile(text); + uris.add(uri); + } + } + + break; + } + + case ACTION_DECRYPT_FROM_CLIPBOARD: { + ClipboardManager clipMan = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clip = clipMan.getPrimaryClip(); + + // check if data is available as uri + Uri uri = null; + for (int i = 0; i < clip.getItemCount(); i++) { + ClipData.Item item = clip.getItemAt(i); + Uri itemUri = item.getUri(); + if (itemUri != null) { + uri = itemUri; + break; + } + } + + // otherwise, coerce to text (almost always possible) and work from there + if (uri == null) { + String text = clip.getItemAt(0).coerceToText(this).toString(); + uri = readToTempFile(text); + } + uris.add(uri); + + break; + } + + // for everything else, just work on the intent data + case OpenKeychainIntents.DECRYPT_DATA: + case Intent.ACTION_VIEW: + default: + uris.add(intent.getData()); + + } + + + } catch (IOException e) { + Toast.makeText(this, R.string.error_reading_text, Toast.LENGTH_LONG).show(); + finish(); + return; + } + + // Definitely need a data uri with the decrypt_data intent + if (uris.isEmpty()) { + Toast.makeText(this, "No data to decrypt!", Toast.LENGTH_LONG).show(); + setResult(Activity.RESULT_CANCELED); + finish(); + return; + } + + displayListFragment(uris); + + } + + public Uri readToTempFile(String text) throws IOException { + Uri tempFile = TemporaryStorageProvider.createFile(this); + OutputStream outStream = getContentResolver().openOutputStream(tempFile); + outStream.write(text.getBytes()); + outStream.close(); + return tempFile; + } + + public void displayListFragment(ArrayList inputUris) { + + DecryptListFragment frag = DecryptListFragment.newInstance(inputUris); + + FragmentManager fragMan = getSupportFragmentManager(); + + FragmentTransaction trans = fragMan.beginTransaction(); + trans.replace(R.id.decrypt_files_fragment_container, frag); + + // if there already is a fragment, allow going back to that. otherwise, we're top level! + if (fragMan.getFragments() != null && !fragMan.getFragments().isEmpty()) { + trans.addToBackStack("list"); + } + + trans.commit(); + + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptFilesFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptFilesFragment.java deleted file mode 100644 index bc7705233..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptFilesFragment.java +++ /dev/null @@ -1,225 +0,0 @@ -/* - * Copyright (C) 2014 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.ui; - -import android.annotation.SuppressLint; -import android.app.Activity; -import android.app.ProgressDialog; -import android.content.Intent; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.os.Message; -import android.os.Messenger; -import android.text.TextUtils; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.CheckBox; -import android.widget.TextView; - -import org.sufficientlysecure.keychain.Constants; -import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.operations.results.DecryptVerifyResult; -import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyInputParcel; -import org.sufficientlysecure.keychain.service.KeychainService; -import org.sufficientlysecure.keychain.service.ServiceProgressHandler; -import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; -import org.sufficientlysecure.keychain.ui.dialog.DeleteFileDialogFragment; -import org.sufficientlysecure.keychain.ui.util.Notify; -import org.sufficientlysecure.keychain.util.FileHelper; -import org.sufficientlysecure.keychain.util.Log; - -import java.io.File; - -public class DecryptFilesFragment extends DecryptFragment { - public static final String ARG_URI = "uri"; - public static final String ARG_OPEN_DIRECTLY = "open_directly"; - - private static final int REQUEST_CODE_INPUT = 0x00007003; - private static final int REQUEST_CODE_OUTPUT = 0x00007007; - - // view - private TextView mFilename; - private CheckBox mDeleteAfter; - private View mDecryptButton; - - // model - private Uri mInputUri = null; - private Uri mOutputUri = null; - - /** - * Creates new instance of this fragment - */ - public static DecryptFilesFragment newInstance(Uri uri, boolean openDirectly) { - DecryptFilesFragment frag = new DecryptFilesFragment(); - - Bundle args = new Bundle(); - args.putParcelable(ARG_URI, uri); - args.putBoolean(ARG_OPEN_DIRECTLY, openDirectly); - - frag.setArguments(args); - - return frag; - } - - /** - * Inflate the layout for this fragment - */ - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.decrypt_files_fragment, container, false); - - mFilename = (TextView) view.findViewById(R.id.decrypt_files_filename); - mDeleteAfter = (CheckBox) view.findViewById(R.id.decrypt_files_delete_after_decryption); - mDecryptButton = view.findViewById(R.id.decrypt_files_action_decrypt); - view.findViewById(R.id.decrypt_files_browse).setOnClickListener(new View.OnClickListener() { - public void onClick(View v) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - FileHelper.openDocument(DecryptFilesFragment.this, "*/*", REQUEST_CODE_INPUT); - } else { - FileHelper.openFile(DecryptFilesFragment.this, mInputUri, "*/*", - REQUEST_CODE_INPUT); - } - } - }); - mDecryptButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - decryptAction(); - } - }); - - return view; - } - - @Override - public void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - - outState.putParcelable(ARG_URI, mInputUri); - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - - Bundle state = savedInstanceState != null ? savedInstanceState : getArguments(); - setInputUri(state.getParcelable(ARG_URI)); - - // should only come from args - if (state.getBoolean(ARG_OPEN_DIRECTLY, false)) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - FileHelper.openDocument(DecryptFilesFragment.this, "*/*", REQUEST_CODE_INPUT); - } else { - FileHelper.openFile(DecryptFilesFragment.this, mInputUri, "*/*", REQUEST_CODE_INPUT); - } - } - } - - private void setInputUri(Uri inputUri) { - if (inputUri == null) { - mInputUri = null; - mFilename.setText(""); - return; - } - - mInputUri = inputUri; - mFilename.setText(FileHelper.getFilename(getActivity(), mInputUri)); - } - - private void decryptAction() { - if (mInputUri == null) { - Notify.create(getActivity(), R.string.no_file_selected, Notify.Style.ERROR).show(); - return; - } - - cryptoOperation(); - } - - private String removeEncryptedAppend(String name) { - if (name.endsWith(Constants.FILE_EXTENSION_ASC) - || name.endsWith(Constants.FILE_EXTENSION_PGP_MAIN) - || name.endsWith(Constants.FILE_EXTENSION_PGP_ALTERNATE)) { - return name.substring(0, name.length() - 4); - } - return name; - } - - private void askForOutputFilename(String originalFilename) { - if (TextUtils.isEmpty(originalFilename)) { - originalFilename = removeEncryptedAppend(FileHelper.getFilename(getActivity(), mInputUri)); - } - - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { - File file = new File(mInputUri.getPath()); - File parentDir = file.exists() ? file.getParentFile() : Constants.Path.APP_DIR; - File targetFile = new File(parentDir, originalFilename); - FileHelper.saveFile(this, getString(R.string.title_decrypt_to_file), - getString(R.string.specify_file_to_decrypt_to), targetFile, REQUEST_CODE_OUTPUT); - } else { - FileHelper.saveDocument(this, "*/*", originalFilename, REQUEST_CODE_OUTPUT); - } - } - - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - switch (requestCode) { - case REQUEST_CODE_INPUT: { - if (resultCode == Activity.RESULT_OK && data != null) { - setInputUri(data.getData()); - } - return; - } - - case REQUEST_CODE_OUTPUT: { - // This happens after output file was selected, so start our operation - if (resultCode == Activity.RESULT_OK && data != null) { - mOutputUri = data.getData(); - cryptoOperation(); - } - return; - } - - default: { - super.onActivityResult(requestCode, resultCode, data); - } - } - } - - @Override - protected void onVerifyLoaded(boolean hideErrorOverlay) { - - } - - @Override - protected PgpDecryptVerifyInputParcel createOperationInput() { - return new PgpDecryptVerifyInputParcel(mInputUri, mOutputUri).setAllowSymmetricDecryption(true); - } - - @Override - protected void onCryptoOperationSuccess(DecryptVerifyResult result) { - - // display signature result in activity - loadVerifyResult(result); - - // TODO delete after decrypt not implemented! - - } - -} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptFragment.java index 0626326fc..c8ae867b2 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptFragment.java @@ -19,21 +19,25 @@ package org.sufficientlysecure.keychain.ui; import java.util.ArrayList; +import android.app.Activity; import android.content.Intent; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.os.Message; import android.os.Messenger; +import android.support.v4.app.Fragment; import android.support.v4.app.LoaderManager; import android.support.v4.content.CursorLoader; import android.support.v4.content.Loader; import android.util.Log; import android.view.View; +import android.view.View.OnClickListener; import android.widget.Button; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; +import android.widget.ViewAnimator; import org.openintents.openpgp.OpenPgpSignatureResult; import org.sufficientlysecure.keychain.Constants; @@ -43,24 +47,19 @@ import org.sufficientlysecure.keychain.operations.results.DecryptVerifyResult; import org.sufficientlysecure.keychain.operations.results.ImportKeyResult; import org.sufficientlysecure.keychain.operations.results.OperationResult; import org.sufficientlysecure.keychain.pgp.KeyRing; -import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyInputParcel; import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; import org.sufficientlysecure.keychain.provider.KeychainContract; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; import org.sufficientlysecure.keychain.provider.ProviderHelper; import org.sufficientlysecure.keychain.service.KeychainService; import org.sufficientlysecure.keychain.service.ServiceProgressHandler; -import org.sufficientlysecure.keychain.ui.base.CachingCryptoOperationFragment; -import org.sufficientlysecure.keychain.ui.base.CryptoOperationFragment; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils.State; import org.sufficientlysecure.keychain.ui.util.Notify; import org.sufficientlysecure.keychain.ui.util.Notify.Style; import org.sufficientlysecure.keychain.util.Preferences; -public abstract class DecryptFragment - extends CachingCryptoOperationFragment - implements LoaderManager.LoaderCallbacks { +public abstract class DecryptFragment extends Fragment implements LoaderManager.LoaderCallbacks { public static final int LOADER_ID_UNIFIED = 0; public static final String ARG_DECRYPT_VERIFY_RESULT = "decrypt_verify_result"; @@ -75,11 +74,9 @@ public abstract class DecryptFragment protected TextView mSignatureEmail; protected TextView mSignatureAction; - private LinearLayout mContentLayout; - private LinearLayout mErrorOverlayLayout; - private OpenPgpSignatureResult mSignatureResult; private DecryptVerifyResult mDecryptVerifyResult; + private ViewAnimator mOverlayAnimator; @Override public void onViewCreated(View view, Bundle savedInstanceState) { @@ -98,18 +95,23 @@ public abstract class DecryptFragment mSignatureAction = (TextView) getActivity().findViewById(R.id.result_signature_action); // Overlay - mContentLayout = (LinearLayout) view.findViewById(R.id.decrypt_content); - mErrorOverlayLayout = (LinearLayout) view.findViewById(R.id.decrypt_error_overlay); + mOverlayAnimator = (ViewAnimator) view; Button vErrorOverlayButton = (Button) view.findViewById(R.id.decrypt_error_overlay_button); - vErrorOverlayButton.setOnClickListener(new View.OnClickListener() { + vErrorOverlayButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { - mErrorOverlayLayout.setVisibility(View.GONE); - mContentLayout.setVisibility(View.VISIBLE); + mOverlayAnimator.setDisplayedChild(0); } }); } + private void showErrorOverlay(boolean overlay) { + int child = overlay ? 1 : 0; + if (mOverlayAnimator.getDisplayedChild() != child) { + mOverlayAnimator.setDisplayedChild(child); + } + } + @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); @@ -151,7 +153,10 @@ public abstract class DecryptFragment final ImportKeyResult result = returnData.getParcelable(OperationResult.EXTRA_RESULT); - result.createNotify(getActivity()).show(); + Activity activity = getActivity(); + if (result != null && activity != null) { + result.createNotify(activity).show(); + } getLoaderManager().restartLoader(LOADER_ID_UNIFIED, null, DecryptFragment.this); } @@ -205,9 +210,6 @@ public abstract class DecryptFragment } } - /** - * @return returns false if signature is invalid, key is revoked or expired. - */ protected void loadVerifyResult(DecryptVerifyResult decryptVerifyResult) { mDecryptVerifyResult = decryptVerifyResult; @@ -227,8 +229,7 @@ public abstract class DecryptFragment getLoaderManager().destroyLoader(LOADER_ID_UNIFIED); - mErrorOverlayLayout.setVisibility(View.GONE); - mContentLayout.setVisibility(View.VISIBLE); + showErrorOverlay(false); onVerifyLoaded(true); @@ -330,10 +331,7 @@ public abstract class DecryptFragment setSignatureLayoutVisibility(View.VISIBLE); setShowAction(signatureKeyId); - mErrorOverlayLayout.setVisibility(View.VISIBLE); - mContentLayout.setVisibility(View.GONE); - - onVerifyLoaded(false); + onVerifyLoaded(true); } else if (isExpired) { mSignatureText.setText(R.string.decrypt_result_signature_expired_key); @@ -342,8 +340,7 @@ public abstract class DecryptFragment setSignatureLayoutVisibility(View.VISIBLE); setShowAction(signatureKeyId); - mErrorOverlayLayout.setVisibility(View.GONE); - mContentLayout.setVisibility(View.VISIBLE); + showErrorOverlay(false); onVerifyLoaded(true); @@ -355,8 +352,7 @@ public abstract class DecryptFragment setSignatureLayoutVisibility(View.VISIBLE); setShowAction(signatureKeyId); - mErrorOverlayLayout.setVisibility(View.GONE); - mContentLayout.setVisibility(View.VISIBLE); + showErrorOverlay(false); onVerifyLoaded(true); @@ -367,8 +363,7 @@ public abstract class DecryptFragment setSignatureLayoutVisibility(View.VISIBLE); setShowAction(signatureKeyId); - mErrorOverlayLayout.setVisibility(View.GONE); - mContentLayout.setVisibility(View.VISIBLE); + showErrorOverlay(false); onVerifyLoaded(true); @@ -379,8 +374,7 @@ public abstract class DecryptFragment setSignatureLayoutVisibility(View.VISIBLE); setShowAction(signatureKeyId); - mErrorOverlayLayout.setVisibility(View.GONE); - mContentLayout.setVisibility(View.VISIBLE); + showErrorOverlay(false); onVerifyLoaded(true); } @@ -438,8 +432,7 @@ public abstract class DecryptFragment } }); - mErrorOverlayLayout.setVisibility(View.GONE); - mContentLayout.setVisibility(View.VISIBLE); + showErrorOverlay(false); onVerifyLoaded(true); @@ -452,8 +445,7 @@ public abstract class DecryptFragment setSignatureLayoutVisibility(View.GONE); - mErrorOverlayLayout.setVisibility(View.VISIBLE); - mContentLayout.setVisibility(View.GONE); + showErrorOverlay(true); onVerifyLoaded(false); break; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptListFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptListFragment.java new file mode 100644 index 000000000..d2bff8336 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptListFragment.java @@ -0,0 +1,881 @@ +/* + * Copyright (C) 2014 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.ui; + + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import android.app.Activity; +import android.content.ClipDescription; +import android.content.Context; +import android.content.Intent; +import android.content.pm.LabeledIntent; +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.Bundle; +import android.os.Parcelable; +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.ViewGroup; +import android.widget.ImageView; +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.ViewAnimator; + +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.operations.results.DecryptVerifyResult; +import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyInputParcel; +import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; +import org.sufficientlysecure.keychain.provider.TemporaryStorageProvider; +import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; +// this import NEEDS to be above the ViewModel one, or it won't compile! (as of 06/06/15) +import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils.StatusHolder; +import org.sufficientlysecure.keychain.ui.DecryptListFragment.DecryptFilesAdapter.ViewModel; +import org.sufficientlysecure.keychain.ui.adapter.SpacesItemDecoration; +import org.sufficientlysecure.keychain.ui.base.CryptoOperationFragment; +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; + + +public class DecryptListFragment + extends CryptoOperationFragment + 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_RESULTS = "results"; + + private static final int REQUEST_CODE_OUTPUT = 0x00007007; + + private ArrayList mInputUris; + private HashMap mOutputUris; + private ArrayList mPendingInputUris; + + private Uri mCurrentInputUri; + + private DecryptFilesAdapter mAdapter; + + /** + * Creates new instance of this fragment + */ + public static DecryptListFragment newInstance(ArrayList uris) { + DecryptListFragment frag = new DecryptListFragment(); + + Bundle args = new Bundle(); + args.putParcelableArrayList(ARG_INPUT_URIS, uris); + frag.setArguments(args); + + return frag; + } + + /** + * 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); + // TODO make this a grid, for tablets! + vFilesList.setLayoutManager(new LinearLayoutManager(getActivity())); + vFilesList.setItemAnimator(new DefaultItemAnimator()); + + mAdapter = new DecryptFilesAdapter(getActivity(), this); + vFilesList.setAdapter(mAdapter); + + return view; + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + + outState.putParcelableArrayList(ARG_INPUT_URIS, mInputUris); + + HashMap results = new HashMap<>(mInputUris.size()); + for (Uri uri : mInputUris) { + if (mPendingInputUris.contains(uri)) { + continue; + } + DecryptVerifyResult 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<>(mOutputUris)); + + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + Bundle args = savedInstanceState != null ? savedInstanceState : getArguments(); + + ArrayList inputUris = getArguments().getParcelableArrayList(ARG_INPUT_URIS); + ParcelableHashMap outputUris = args.getParcelable(ARG_OUTPUT_URIS); + ParcelableHashMap results = args.getParcelable(ARG_RESULTS); + + displayInputUris(inputUris, + outputUris != null ? outputUris.getMap() : null, + results != null ? results.getMap() : null + ); + } + + private void displayInputUris(ArrayList inputUris, HashMap outputUris, + HashMap results) { + + mInputUris = inputUris; + mOutputUris = outputUris != null ? outputUris : new HashMap(inputUris.size()); + + mPendingInputUris = new ArrayList<>(); + + for (Uri uri : inputUris) { + mAdapter.add(uri); + if (results != null && results.containsKey(uri)) { + processResult(uri, results.get(uri)); + } else { + mOutputUris.put(uri, TemporaryStorageProvider.createFile(getActivity())); + mPendingInputUris.add(uri); + } + } + + cryptoOperation(); + } + + private String removeEncryptedAppend(String name) { + if (name.endsWith(Constants.FILE_EXTENSION_ASC) + || name.endsWith(Constants.FILE_EXTENSION_PGP_MAIN) + || name.endsWith(Constants.FILE_EXTENSION_PGP_ALTERNATE)) { + return name.substring(0, name.length() - 4); + } + return name; + } + + private void askForOutputFilename(Uri inputUri, String originalFilename, String mimeType) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { + File file = new File(inputUri.getPath()); + File parentDir = file.exists() ? file.getParentFile() : Constants.Path.APP_DIR; + File targetFile = new File(parentDir, originalFilename); + FileHelper.saveFile(this, getString(R.string.title_decrypt_to_file), + getString(R.string.specify_file_to_decrypt_to), targetFile, REQUEST_CODE_OUTPUT); + } else { + FileHelper.saveDocument(this, mimeType, originalFilename, REQUEST_CODE_OUTPUT); + } + } + + @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 decryptedFileUri = mOutputUris.get(mCurrentInputUri); + Uri saveUri = data.getData(); + saveFile(decryptedFileUri, saveUri); + mCurrentInputUri = null; + } + return; + } + + default: { + super.onActivityResult(requestCode, resultCode, data); + } + } + } + + private void saveFile(Uri decryptedFileUri, Uri saveUri) { + 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 + protected void cryptoOperation(CryptoInputParcel cryptoInput) { + super.cryptoOperation(cryptoInput, false); + } + + @Override + protected boolean onCryptoSetProgress(String msg, int progress, int max) { + mAdapter.setProgress(mCurrentInputUri, progress, max, msg); + return true; + } + + @Override + protected void dismissProgress() { + // progress shown inline, so never mind + } + + @Override + protected void onCryptoOperationError(DecryptVerifyResult result) { + final Uri uri = mCurrentInputUri; + mCurrentInputUri = null; + + mAdapter.addResult(uri, result, null, null, null); + + cryptoOperation(); + } + + @Override + protected void onCryptoOperationSuccess(DecryptVerifyResult result) { + Uri uri = mCurrentInputUri; + mCurrentInputUri = null; + + processResult(uri, result); + + cryptoOperation(); + } + + private void processResult(final Uri uri, final DecryptVerifyResult result) { + + new AsyncTask() { + @Override + protected Drawable doInBackground(Void... params) { + + Context context = getActivity(); + if (result.getDecryptMetadata() == null || context == null) { + return null; + } + + String type = result.getDecryptMetadata().getMimeType(); + Uri outputUri = mOutputUris.get(uri); + if (type == null || outputUri == null) { + return null; + } + + TemporaryStorageProvider.setMimeType(context, outputUri, type); + + if (ClipDescription.compareMimeTypes(type, "image/*")) { + int px = FormattingUtils.dpToPx(context, 48); + Bitmap bitmap = FileHelper.getThumbnail(context, outputUri, new Point(px, px)); + return new BitmapDrawable(context.getResources(), bitmap); + } + + final Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setType(type); + + final List matches = + context.getPackageManager().queryIntentActivities(intent, 0); + //noinspection LoopStatementThatDoesntLoop + for (ResolveInfo match : matches) { + return match.loadIcon(getActivity().getPackageManager()); + } + + return null; + + } + + @Override + protected void onPostExecute(Drawable icon) { + processResult(uri, result, icon); + } + }.execute(); + + } + + private void processResult(final Uri uri, DecryptVerifyResult result, Drawable icon) { + + OnClickListener onFileClick = null, onKeyClick = null; + + OpenPgpSignatureResult sigResult = result.getSignatureResult(); + if (sigResult != null) { + final long keyId = sigResult.getKeyId(); + if (sigResult.getStatus() != OpenPgpSignatureResult.SIGNATURE_KEY_MISSING) { + onKeyClick = 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); + } + }; + } + } + + if (result.success() && result.getDecryptMetadata() != null) { + onFileClick = new OnClickListener() { + @Override + public void onClick(View view) { + displayWithViewIntent(uri); + } + }; + } + + mAdapter.addResult(uri, result, icon, onFileClick, onKeyClick); + + } + + public void displayWithViewIntent(final Uri uri) { + Activity activity = getActivity(); + if (activity == null || mCurrentInputUri != null) { + return; + } + + final Uri outputUri = mOutputUris.get(uri); + final DecryptVerifyResult result = mAdapter.getItemResult(uri); + if (outputUri == null || result == null) { + return; + } + + final OpenPgpMetadata metadata = result.getDecryptMetadata(); + + // 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())) { + + // this is a significant i/o operation, use an asynctask + new AsyncTask() { + + @Override + protected Intent doInBackground(Void... params) { + + Activity activity = getActivity(); + if (activity == null) { + return null; + } + + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setDataAndType(outputUri, "text/plain"); + intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + return intent; + } + + @Override + protected void onPostExecute(Intent intent) { + // for result so we can possibly get a snackbar error from internal viewer + Activity activity = getActivity(); + if (intent == null || activity == null) { + return; + } + + LabeledIntent internalIntent = new LabeledIntent( + new Intent(intent) + .setClass(activity, DisplayTextActivity.class) + .putExtra(DisplayTextActivity.EXTRA_METADATA, result), + BuildConfig.APPLICATION_ID, R.string.view_internal, R.drawable.ic_launcher); + + Intent chooserIntent = Intent.createChooser(intent, getString(R.string.intent_show)); + chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, + new Parcelable[] { internalIntent }); + + activity.startActivity(chooserIntent); + } + + }.execute(); + + } else { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setDataAndType(outputUri, metadata.getMimeType()); + 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); + activity.startActivity(chooserIntent); + } + + } + + @Override + protected PgpDecryptVerifyInputParcel createOperationInput() { + + if (mCurrentInputUri == null) { + if (mPendingInputUris.isEmpty()) { + // nothing left to do + return null; + } + + mCurrentInputUri = mPendingInputUris.remove(0); + } + + Uri currentOutputUri = mOutputUris.get(mCurrentInputUri); + Log.d(Constants.TAG, "mInputUri=" + mCurrentInputUri + ", mOutputUri=" + currentOutputUri); + + return new PgpDecryptVerifyInputParcel(mCurrentInputUri, currentOutputUri) + .setAllowSymmetricDecryption(true); + + } + + @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; + DecryptVerifyResult result = model.mResult; + switch (menuItem.getItemId()) { + case R.id.view_log: + Intent intent = new Intent(activity, LogDisplayActivity.class); + intent.putExtra(LogDisplayFragment.EXTRA_RESULT, result); + activity.startActivity(intent); + return true; + case R.id.decrypt_save: + OpenPgpMetadata metadata = result.getDecryptMetadata(); + if (metadata == null) { + return true; + } + mCurrentInputUri = model.mInputUri; + askForOutputFilename(model.mInputUri, metadata.getFilename(), metadata.getMimeType()); + return true; + case R.id.decrypt_delete: + deleteFile(activity, model.mInputUri); + return true; + } + return false; + } + + private void deleteFile(Activity activity, Uri uri) { + + if ("file".equals(uri.getScheme())) { + File file = new File(uri.getPath()); + if (file.delete()) { + Notify.create(activity, R.string.file_delete_ok, Style.OK).show(); + } else { + Notify.create(activity, R.string.file_delete_none, Style.WARN).show(); + } + return; + } + + if ("content".equals(uri.getScheme())) { + try { + int deleted = activity.getContentResolver().delete(uri, null, null); + 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(); + } + return; + } + + Notify.create(activity, R.string.file_delete_exception, Style.ERROR).show(); + + } + + public static class DecryptFilesAdapter extends RecyclerView.Adapter { + private Context mContext; + private ArrayList mDataset; + private OnMenuItemClickListener mMenuItemClickListener; + private ViewModel mMenuClickedModel; + + public class ViewModel { + Context mContext; + Uri mInputUri; + DecryptVerifyResult mResult; + Drawable mIcon; + + OnClickListener mOnFileClickListener; + OnClickListener mOnKeyClickListener; + + int mProgress, mMax; + String mProgressMsg; + + ViewModel(Context context, Uri uri) { + mContext = context; + mInputUri = uri; + mProgress = 0; + mMax = 100; + } + + void addResult(DecryptVerifyResult result) { + mResult = result; + } + + void addIcon(Drawable icon) { + mIcon = icon; + } + + void setOnClickListeners(OnClickListener onFileClick, OnClickListener onKeyClick) { + mOnFileClickListener = onFileClick; + mOnKeyClickListener = onKeyClick; + } + + boolean hasResult() { + return mResult != null; + } + + void setProgress(int progress, int max, String msg) { + if (msg != null) { + mProgressMsg = msg; + } + mProgress = progress; + mMax = max; + } + + // 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; + return !(mInputUri != null ? !mInputUri.equals(viewModel.mInputUri) + : viewModel.mInputUri != null); + } + + // 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(Context context, OnMenuItemClickListener menuItemClickListener) { + mContext = context; + mMenuItemClickListener = menuItemClickListener; + 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.hasResult()) { + bindItemProgress(holder, model); + return; + } + + if (model.mResult.success()) { + bindItemSuccess(holder, model); + } else { + bindItemFailure(holder, model); + } + + } + + private void bindItemProgress(ViewHolder holder, ViewModel model) { + if (holder.vAnimator.getDisplayedChild() != 0) { + 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) { + if (holder.vAnimator.getDisplayedChild() != 1) { + holder.vAnimator.setDisplayedChild(1); + } + + KeyFormattingUtils.setStatus(mContext, holder, model.mResult); + + final OpenPgpMetadata metadata = model.mResult.getDecryptMetadata(); + + String filename; + if (metadata == null) { + filename = mContext.getString(R.string.filename_unknown); + } else if (TextUtils.isEmpty(metadata.getFilename())) { + filename = mContext.getString("text/plain".equals(metadata.getMimeType()) + ? R.string.filename_unknown_text : R.string.filename_unknown); + } else { + filename = metadata.getFilename(); + } + holder.vFilename.setText(filename); + + long size = metadata == null ? 0 : metadata.getOriginalSize(); + if (size == -1 || size == 0) { + holder.vFilesize.setText(""); + } else { + holder.vFilesize.setText(FileHelper.readableFileSize(size)); + } + + if (model.mIcon != null) { + holder.vThumbnail.setImageDrawable(model.mIcon); + } else { + holder.vThumbnail.setImageResource(R.drawable.ic_doc_generic_am); + } + + holder.vFile.setOnClickListener(model.mOnFileClickListener); + holder.vSignatureLayout.setOnClickListener(model.mOnKeyClickListener); + + holder.vContextMenu.setTag(model); + holder.vContextMenu.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View view) { + mMenuClickedModel = model; + PopupMenu menu = new PopupMenu(mContext, view); + menu.inflate(R.menu.decrypt_item_context_menu); + menu.setOnMenuItemClickListener(mMenuItemClickListener); + menu.setOnDismissListener(new OnDismissListener() { + @Override + public void onDismiss(PopupMenu popupMenu) { + mMenuClickedModel = null; + } + }); + menu.show(); + } + }); + } + + private void bindItemFailure(ViewHolder holder, final ViewModel model) { + if (holder.vAnimator.getDisplayedChild() != 2) { + holder.vAnimator.setDisplayedChild(2); + } + + holder.vErrorMsg.setText(model.mResult.getLog().getLast().mType.getMsgId()); + + holder.vErrorViewLog.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + Intent intent = new Intent(mContext, LogDisplayActivity.class); + intent.putExtra(LogDisplayFragment.EXTRA_RESULT, model.mResult); + mContext.startActivity(intent); + } + }); + + } + + // Return the size of your dataset (invoked by the layout manager) + @Override + public int getItemCount() { + return mDataset.size(); + } + + public DecryptVerifyResult getItemResult(Uri uri) { + ViewModel model = new ViewModel(mContext, 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(mContext, uri); + mDataset.add(newModel); + notifyItemInserted(mDataset.size()); + } + + public void setProgress(Uri uri, int progress, int max, String msg) { + ViewModel newModel = new ViewModel(mContext, uri); + int pos = mDataset.indexOf(newModel); + mDataset.get(pos).setProgress(progress, max, msg); + notifyItemChanged(pos); + } + + public void addResult(Uri uri, DecryptVerifyResult result, Drawable icon, + OnClickListener onFileClick, OnClickListener onKeyClick) { + + ViewModel model = new ViewModel(mContext, uri); + int pos = mDataset.indexOf(model); + model = mDataset.get(pos); + + model.addResult(result); + if (icon != null) { + model.addIcon(icon); + } + model.setOnClickListeners(onFileClick, onKeyClick); + + 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 View vFile; + public TextView vFilename; + public TextView vFilesize; + public ImageView vThumbnail; + + public ImageView vEncStatusIcon; + public TextView vEncStatusText; + + public ImageView vSigStatusIcon; + public TextView vSigStatusText; + public View vSignatureLayout; + public TextView vSignatureName; + public TextView vSignatureMail; + public TextView vSignatureAction; + public View vContextMenu; + + public TextView vErrorMsg; + public ImageView vErrorViewLog; + + 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); + + 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); + + 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 = (TextView) itemView.findViewById(R.id.result_signature_action); + + 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); + + } + + @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 TextView getSignatureAction() { + return vSignatureAction; + } + + @Override + public TextView getSignatureUserName() { + return vSignatureName; + } + + @Override + public TextView getSignatureUserEmail() { + return vSignatureMail; + } + + @Override + public boolean hasEncrypt() { + return true; + } + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptTextActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptTextActivity.java deleted file mode 100644 index 0c463c2cd..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptTextActivity.java +++ /dev/null @@ -1,228 +0,0 @@ -/* - * Copyright (C) 2012-2015 Dominik Schürmann - * Copyright (C) 2010-2014 Thialfihar - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sufficientlysecure.keychain.ui; - -import android.app.Activity; -import android.content.Intent; -import android.os.Bundle; -import android.support.v4.app.Fragment; -import android.text.TextUtils; -import android.view.View; -import android.widget.Toast; - -import org.sufficientlysecure.keychain.Constants; -import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.compatibility.ClipboardReflection; -import org.sufficientlysecure.keychain.intents.OpenKeychainIntents; -import org.sufficientlysecure.keychain.operations.results.OperationResult; -import org.sufficientlysecure.keychain.operations.results.SingletonResult; -import org.sufficientlysecure.keychain.pgp.PgpHelper; -import org.sufficientlysecure.keychain.ui.base.BaseActivity; -import org.sufficientlysecure.keychain.util.Log; - -import java.util.regex.Matcher; - -public class DecryptTextActivity extends BaseActivity { - - /* Intents */ - public static final String ACTION_DECRYPT_TEXT = OpenKeychainIntents.DECRYPT_TEXT; - public static final String EXTRA_TEXT = OpenKeychainIntents.DECRYPT_EXTRA_TEXT; - - // intern - public static final String ACTION_DECRYPT_FROM_CLIPBOARD = Constants.INTENT_PREFIX + "DECRYPT_TEXT_FROM_CLIPBOARD"; - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - setFullScreenDialogClose(new View.OnClickListener() { - @Override - public void onClick(View v) { - setResult(Activity.RESULT_CANCELED); - finish(); - } - }, false); - - // Handle intent actions - handleActions(savedInstanceState, getIntent()); - } - - @Override - protected void initLayout() { - setContentView(R.layout.decrypt_text_activity); - } - - /** - * Fixing broken PGP MESSAGE Strings coming from GMail/AOSP Mail - */ - private String fixPgpMessage(String message) { - // windows newline -> unix newline - message = message.replaceAll("\r\n", "\n"); - // Mac OS before X newline -> unix newline - message = message.replaceAll("\r", "\n"); - - // remove whitespaces before newline - message = message.replaceAll(" +\n", "\n"); - // only two consecutive newlines are allowed - message = message.replaceAll("\n\n+", "\n\n"); - - // replace non breakable spaces - message = message.replaceAll("\\xa0", " "); - - return message; - } - - /** - * Fixing broken PGP SIGNED MESSAGE Strings coming from GMail/AOSP Mail - */ - private String fixPgpCleartextSignature(CharSequence input) { - if (!TextUtils.isEmpty(input)) { - String text = input.toString(); - - // windows newline -> unix newline - text = text.replaceAll("\r\n", "\n"); - // Mac OS before X newline -> unix newline - text = text.replaceAll("\r", "\n"); - - return text; - } else { - return null; - } - } - - private String getPgpContent(CharSequence input) { - // only decrypt if clipboard content is available and a pgp message or cleartext signature - if (!TextUtils.isEmpty(input)) { - Log.dEscaped(Constants.TAG, "input: " + input); - - Matcher matcher = PgpHelper.PGP_MESSAGE.matcher(input); - if (matcher.matches()) { - String text = matcher.group(1); - text = fixPgpMessage(text); - - Log.dEscaped(Constants.TAG, "input fixed: " + text); - return text; - } else { - matcher = PgpHelper.PGP_CLEARTEXT_SIGNATURE.matcher(input); - if (matcher.matches()) { - String text = matcher.group(1); - text = fixPgpCleartextSignature(text); - - Log.dEscaped(Constants.TAG, "input fixed: " + text); - return text; - } else { - return null; - } - } - } else { - return null; - } - } - - /** - * Handles all actions with this intent - */ - private void handleActions(Bundle savedInstanceState, Intent intent) { - String action = intent.getAction(); - Bundle extras = intent.getExtras(); - String type = intent.getType(); - - if (extras == null) { - extras = new Bundle(); - } - - if (savedInstanceState != null) { - return; - } - - if (Intent.ACTION_SEND.equals(action) && type != null) { - Log.d(Constants.TAG, "ACTION_SEND"); - Log.logDebugBundle(extras, "SEND extras"); - - // When sending to Keychain Decrypt via share menu - if ("text/plain".equals(type)) { - String sharedText = extras.getString(Intent.EXTRA_TEXT); - sharedText = getPgpContent(sharedText); - - if (sharedText != null) { - loadFragment(sharedText); - } else { - Log.e(Constants.TAG, "EXTRA_TEXT does not contain PGP content!"); - Toast.makeText(this, R.string.error_invalid_data, Toast.LENGTH_LONG).show(); - finish(); - } - } else { - Log.e(Constants.TAG, "ACTION_SEND received non-plaintext, this should not happen in this activity!"); - Toast.makeText(this, R.string.error_invalid_data, Toast.LENGTH_LONG).show(); - finish(); - } - } else if (ACTION_DECRYPT_TEXT.equals(action)) { - Log.d(Constants.TAG, "ACTION_DECRYPT_TEXT"); - - String extraText = extras.getString(EXTRA_TEXT); - extraText = getPgpContent(extraText); - - if (extraText != null) { - loadFragment(extraText); - } else { - Log.e(Constants.TAG, "EXTRA_TEXT does not contain PGP content!"); - Toast.makeText(this, R.string.error_invalid_data, Toast.LENGTH_LONG).show(); - finish(); - } - } else if (ACTION_DECRYPT_FROM_CLIPBOARD.equals(action)) { - Log.d(Constants.TAG, "ACTION_DECRYPT_FROM_CLIPBOARD"); - - CharSequence clipboardText = ClipboardReflection.getClipboardText(this); - String text = getPgpContent(clipboardText); - - if (text != null) { - loadFragment(text); - } else { - returnInvalidResult(); - } - } else if (ACTION_DECRYPT_TEXT.equals(action)) { - Log.e(Constants.TAG, "Include the extra 'text' in your Intent!"); - Toast.makeText(this, R.string.error_invalid_data, Toast.LENGTH_LONG).show(); - finish(); - } - } - - private void returnInvalidResult() { - SingletonResult result = new SingletonResult( - SingletonResult.RESULT_ERROR, OperationResult.LogType.MSG_NO_VALID_ENC); - Intent intent = new Intent(); - intent.putExtra(SingletonResult.EXTRA_RESULT, result); - setResult(RESULT_OK, intent); - finish(); - } - - private void loadFragment(String ciphertext) { - // Create an instance of the fragment - Fragment frag = DecryptTextFragment.newInstance(ciphertext); - - // Add the fragment to the 'fragment_container' FrameLayout - // NOTE: We use commitAllowingStateLoss() to prevent weird crashes! - getSupportFragmentManager().beginTransaction() - .replace(R.id.decrypt_text_fragment_container, frag) - .commitAllowingStateLoss(); - // do it immediately! - getSupportFragmentManager().executePendingTransactions(); - } - -} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptFilesActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DisplayTextActivity.java similarity index 51% rename from OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptFilesActivity.java rename to OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DisplayTextActivity.java index 81fb6a392..be21cdde1 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptFilesActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DisplayTextActivity.java @@ -1,5 +1,6 @@ /* - * Copyright (C) 2014 Dominik Schürmann + * Copyright (C) 2012-2015 Dominik Schürmann + * Copyright (C) 2010-2014 Thialfihar * * 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 @@ -17,27 +18,24 @@ package org.sufficientlysecure.keychain.ui; + +import java.io.IOException; + import android.app.Activity; import android.content.Intent; -import android.net.Uri; import android.os.Bundle; import android.support.v4.app.Fragment; import android.view.View; import android.widget.Toast; -import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.intents.OpenKeychainIntents; +import org.sufficientlysecure.keychain.operations.results.DecryptVerifyResult; import org.sufficientlysecure.keychain.ui.base.BaseActivity; -import org.sufficientlysecure.keychain.util.Log; +import org.sufficientlysecure.keychain.util.FileHelper; -public class DecryptFilesActivity extends BaseActivity { +public class DisplayTextActivity extends BaseActivity { - /* Intents */ - public static final String ACTION_DECRYPT_DATA = OpenKeychainIntents.DECRYPT_DATA; - - // intern - public static final String ACTION_DECRYPT_DATA_OPEN = Constants.INTENT_PREFIX + "DECRYPT_DATA_OPEN"; + public static final String EXTRA_METADATA = "metadata"; @Override public void onCreate(Bundle savedInstanceState) { @@ -57,51 +55,46 @@ public class DecryptFilesActivity extends BaseActivity { @Override protected void initLayout() { - setContentView(R.layout.decrypt_files_activity); + setContentView(R.layout.decrypt_text_activity); } /** * Handles all actions with this intent */ private void handleActions(Bundle savedInstanceState, Intent intent) { - String action = intent.getAction(); - String type = intent.getType(); - Uri uri = intent.getData(); - - if (Intent.ACTION_SEND.equals(action) && type != null) { - // When sending to Keychain Decrypt via share menu - // Binary via content provider (could also be files) - // override uri to get stream from send - uri = intent.getParcelableExtra(Intent.EXTRA_STREAM); - action = ACTION_DECRYPT_DATA; - } else if (Intent.ACTION_VIEW.equals(action)) { - // Android's Action when opening file associated to Keychain (see AndroidManifest.xml) - - // override action - action = ACTION_DECRYPT_DATA; - } - - // No need to initialize fragments if we are being restored if (savedInstanceState != null) { return; } - // Definitely need a data uri with the decrypt_data intent - if (ACTION_DECRYPT_DATA.equals(action) && uri == null) { - Toast.makeText(this, "No data to decrypt!", Toast.LENGTH_LONG).show(); - setResult(Activity.RESULT_CANCELED); - finish(); + DecryptVerifyResult result = intent.getParcelableExtra(EXTRA_METADATA); + + String plaintext; + try { + plaintext = FileHelper.readTextFromUri(this, intent.getData(), result.getCharset()); + } catch (IOException e) { + Toast.makeText(this, R.string.error_preparing_data, Toast.LENGTH_LONG).show(); + return; } - boolean showOpenDialog = ACTION_DECRYPT_DATA_OPEN.equals(action); - DecryptFilesFragment frag = DecryptFilesFragment.newInstance(uri, showOpenDialog); + if (plaintext != null) { + loadFragment(plaintext, result); + } else { + Toast.makeText(this, R.string.error_invalid_data, Toast.LENGTH_LONG).show(); + finish(); + } + } + + private void loadFragment(String plaintext, DecryptVerifyResult result) { + // Create an instance of the fragment + Fragment frag = DisplayTextFragment.newInstance(plaintext, result); // Add the fragment to the 'fragment_container' FrameLayout // NOTE: We use commitAllowingStateLoss() to prevent weird crashes! getSupportFragmentManager().beginTransaction() - .replace(R.id.decrypt_files_fragment_container, frag) + .replace(R.id.decrypt_text_fragment_container, frag) .commitAllowingStateLoss(); - + // do it immediately! + getSupportFragmentManager().executePendingTransactions(); } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptTextFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DisplayTextFragment.java similarity index 65% rename from OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptTextFragment.java rename to OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DisplayTextFragment.java index 051da5d6b..7b3af48cc 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptTextFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DisplayTextFragment.java @@ -31,45 +31,31 @@ import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.compatibility.ClipboardReflection; import org.sufficientlysecure.keychain.operations.results.DecryptVerifyResult; -import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyInputParcel; import org.sufficientlysecure.keychain.ui.util.Notify; import org.sufficientlysecure.keychain.util.ShareHelper; -import java.io.UnsupportedEncodingException; +public class DisplayTextFragment extends DecryptFragment { -public class DecryptTextFragment extends DecryptFragment { - public static final String ARG_CIPHERTEXT = "ciphertext"; - public static final String ARG_SHOW_MENU = "show_menu"; + public static final String ARG_PLAINTEXT = "plaintext"; // view private TextView mText; - // model - private String mCiphertext; - private boolean mShowMenuOptions; + // model (no state to persist though, that's all in arguments!) + private boolean mShowMenuOptions = false; - public static DecryptTextFragment newInstance(String ciphertext) { - DecryptTextFragment frag = new DecryptTextFragment(); + public static DisplayTextFragment newInstance(String plaintext, DecryptVerifyResult result) { + DisplayTextFragment frag = new DisplayTextFragment(); Bundle args = new Bundle(); - args.putString(ARG_CIPHERTEXT, ciphertext); + args.putString(ARG_PLAINTEXT, plaintext); + args.putParcelable(ARG_DECRYPT_VERIFY_RESULT, result); frag.setArguments(args); return frag; } - /** - * Inflate the layout for this fragment - */ - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.decrypt_text_fragment, container, false); - mText = (TextView) view.findViewById(R.id.decrypt_text_plaintext); - - return view; - } - /** * Create Intent Chooser but exclude decrypt activites */ @@ -79,7 +65,7 @@ public class DecryptTextFragment extends DecryptFragment { // we don't want to decrypt the decrypted, no inception ;) String[] blacklist = new String[]{ - Constants.PACKAGE_NAME + ".ui.DecryptTextActivity", + Constants.PACKAGE_NAME + ".ui.DecryptActivity", "org.thialfihar.android.apg.ui.DecryptActivity" }; @@ -103,25 +89,34 @@ public class DecryptTextFragment extends DecryptFragment { super.onCreate(savedInstanceState); setHasOptionsMenu(true); + } - Bundle args = savedInstanceState == null ? getArguments() : savedInstanceState; - mCiphertext = args.getString(ARG_CIPHERTEXT); - mShowMenuOptions = args.getBoolean(ARG_SHOW_MENU, false); + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.decrypt_text_fragment, container, false); + mText = (TextView) view.findViewById(R.id.decrypt_text_plaintext); + return view; + } - if (savedInstanceState == null) { - cryptoOperation(); - } + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + Bundle args = getArguments(); + + String plaintext = args.getString(ARG_PLAINTEXT); + DecryptVerifyResult result = args.getParcelable(ARG_DECRYPT_VERIFY_RESULT); + + // display signature result in activity + mText.setText(plaintext); + loadVerifyResult(result); } @Override - public void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - - outState.putString(ARG_CIPHERTEXT, mCiphertext); - outState.putBoolean(ARG_SHOW_MENU, mShowMenuOptions); - // no need to save the decrypted text, it's in the textview - + protected void onVerifyLoaded(boolean hideErrorOverlay) { + mShowMenuOptions = hideErrorOverlay; + getActivity().supportInvalidateOptionsMenu(); } @Override @@ -151,39 +146,4 @@ public class DecryptTextFragment extends DecryptFragment { return true; } - @Override - protected PgpDecryptVerifyInputParcel createOperationInput() { - PgpDecryptVerifyInputParcel input = new PgpDecryptVerifyInputParcel(mCiphertext.getBytes()); - input.setAllowSymmetricDecryption(true); - return input; - } - - @Override - protected void onVerifyLoaded(boolean hideErrorOverlay) { - mShowMenuOptions = hideErrorOverlay; - getActivity().supportInvalidateOptionsMenu(); - } - - @Override - protected void onCryptoOperationSuccess(DecryptVerifyResult result) { - - byte[] decryptedMessage = result.getOutputBytes(); - String displayMessage; - if (result.getCharset() != null) { - try { - displayMessage = new String(decryptedMessage, result.getCharset()); - } catch (UnsupportedEncodingException e) { - // if we can't decode properly, just fall back to utf-8 - displayMessage = new String(decryptedMessage); - } - } else { - displayMessage = new String(decryptedMessage); - } - mText.setText(displayMessage); - - // display signature result in activity - loadVerifyResult(result); - - } - } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptDecryptOverviewFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptDecryptOverviewFragment.java index a6fad8881..590d02c6f 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptDecryptOverviewFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptDecryptOverviewFragment.java @@ -18,8 +18,14 @@ package org.sufficientlysecure.keychain.ui; + +import java.util.regex.Matcher; + +import android.app.Activity; import android.content.Intent; +import android.net.Uri; import android.os.AsyncTask; +import android.os.Build; import android.os.Bundle; import android.support.annotation.Nullable; import android.support.v4.app.Fragment; @@ -29,16 +35,17 @@ import android.view.ViewGroup; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.compatibility.ClipboardReflection; -import org.sufficientlysecure.keychain.operations.results.OperationResult; import org.sufficientlysecure.keychain.pgp.PgpHelper; +import org.sufficientlysecure.keychain.ui.util.Notify; import org.sufficientlysecure.keychain.ui.util.SubtleAttentionSeeker; - -import java.util.regex.Matcher; +import org.sufficientlysecure.keychain.util.FileHelper; public class EncryptDecryptOverviewFragment extends Fragment { View mClipboardIcon; + private static final int REQUEST_CODE_INPUT = 0x00007003; + @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); @@ -74,17 +81,19 @@ public class EncryptDecryptOverviewFragment extends Fragment { mDecryptFile.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - Intent filesDecrypt = new Intent(getActivity(), DecryptFilesActivity.class); - filesDecrypt.setAction(DecryptFilesActivity.ACTION_DECRYPT_DATA_OPEN); - startActivity(filesDecrypt); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + FileHelper.openDocument(EncryptDecryptOverviewFragment.this, "*/*", REQUEST_CODE_INPUT); + } else { + FileHelper.openFile(EncryptDecryptOverviewFragment.this, null, "*/*", REQUEST_CODE_INPUT); + } } }); mDecryptFromClipboard.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - Intent clipboardDecrypt = new Intent(getActivity(), DecryptTextActivity.class); - clipboardDecrypt.setAction(DecryptTextActivity.ACTION_DECRYPT_FROM_CLIPBOARD); + Intent clipboardDecrypt = new Intent(getActivity(), DecryptActivity.class); + clipboardDecrypt.setAction(DecryptActivity.ACTION_DECRYPT_FROM_CLIPBOARD); startActivityForResult(clipboardDecrypt, 0); } }); @@ -135,12 +144,23 @@ public class EncryptDecryptOverviewFragment extends Fragment { @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { - // if a result has been returned, display a notify - if (data != null && data.hasExtra(OperationResult.EXTRA_RESULT)) { - OperationResult result = data.getParcelableExtra(OperationResult.EXTRA_RESULT); - result.createNotify(getActivity()).show(); - } else { - super.onActivityResult(requestCode, resultCode, data); + if (requestCode != REQUEST_CODE_INPUT) { + return; + } + + if (resultCode == Activity.RESULT_OK && data != null) { + Uri uri = data.getData(); + if (uri == null) { + Notify.create(getActivity(), R.string.no_file_selected, Notify.Style.ERROR).show(); + return; + } + + Intent intent = new Intent(getActivity(), DecryptActivity.class); + intent.setAction(Intent.ACTION_VIEW); + intent.setData(uri); + startActivity(intent); + } } + } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptFilesActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptFilesActivity.java index 4d23ba9f8..d51b6e0ff 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptFilesActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptFilesActivity.java @@ -50,14 +50,9 @@ public class EncryptFilesActivity extends EncryptActivity { Intent intent = getIntent(); String action = intent.getAction(); - Bundle extras = intent.getExtras(); String type = intent.getType(); ArrayList uris = new ArrayList<>(); - if (extras == null) { - extras = new Bundle(); - } - if (intent.getData() != null) { uris.add(intent.getData()); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptFilesFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptFilesFragment.java index d290c57a6..d7c6b2049 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptFilesFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptFilesFragment.java @@ -17,7 +17,17 @@ package org.sufficientlysecure.keychain.ui; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + import android.app.Activity; +import android.content.ClipData; +import android.content.ClipboardManager; import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; @@ -59,13 +69,6 @@ import org.sufficientlysecure.keychain.util.Passphrase; import org.sufficientlysecure.keychain.util.Preferences; import org.sufficientlysecure.keychain.util.ShareHelper; -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - public class EncryptFilesFragment extends CachingCryptoOperationFragment { @@ -75,7 +78,7 @@ public class EncryptFilesFragment public static final String ARG_USE_ASCII_ARMOR = "use_ascii_armor"; public static final String ARG_URIS = "uris"; - private static final int REQUEST_CODE_INPUT = 0x00007003; + public static final int REQUEST_CODE_INPUT = 0x00007003; private static final int REQUEST_CODE_OUTPUT = 0x00007007; private boolean mUseArmor; @@ -84,13 +87,15 @@ public class EncryptFilesFragment private boolean mEncryptFilenames; private boolean mHiddenRecipients = false; - private boolean mShareAfterEncrypt; + private AfterEncryptAction mAfterEncryptAction; + private enum AfterEncryptAction { + SAVE, SHARE, COPY; + } private ArrayList mOutputUris; private RecyclerView mSelectedFiles; - ArrayList mFilesModels; FilesAdapter mFilesAdapter; /** @@ -128,8 +133,7 @@ public class EncryptFilesFragment mSelectedFiles.setLayoutManager(new LinearLayoutManager(getActivity())); mSelectedFiles.setItemAnimator(new DefaultItemAnimator()); - mFilesModels = new ArrayList<>(); - mFilesAdapter = new FilesAdapter(getActivity(), mFilesModels, new View.OnClickListener() { + mFilesAdapter = new FilesAdapter(getActivity(), new View.OnClickListener() { @Override public void onClick(View v) { addInputUri(); @@ -193,8 +197,8 @@ public class EncryptFilesFragment if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { FileHelper.openDocument(EncryptFilesFragment.this, "*/*", true, REQUEST_CODE_INPUT); } else { - FileHelper.openFile(EncryptFilesFragment.this, mFilesModels.isEmpty() ? - null : mFilesModels.get(mFilesModels.size() - 1).inputUri, + FileHelper.openFile(EncryptFilesFragment.this, mFilesAdapter.getModelCount() == 0 ? + null : mFilesAdapter.getModelItem(mFilesAdapter.getModelCount() - 1).inputUri, "*/*", REQUEST_CODE_INPUT); } } @@ -216,15 +220,20 @@ public class EncryptFilesFragment } private void showOutputFileDialog() { - if (mFilesModels.size() > 1 || mFilesModels.isEmpty()) { + if (mFilesAdapter.getModelCount() != 1) { throw new IllegalStateException(); } - FilesAdapter.ViewModel model = mFilesModels.get(0); + FilesAdapter.ViewModel model = mFilesAdapter.getModelItem(0); String targetName = (mEncryptFilenames ? "1" : FileHelper.getFilename(getActivity(), model.inputUri)) + (mUseArmor ? Constants.FILE_EXTENSION_ASC : Constants.FILE_EXTENSION_PGP_MAIN); + Uri inputUri = model.inputUri; + saveDocumentIntent(targetName, inputUri); + } + + private void saveDocumentIntent(String targetName, Uri inputUri) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { - File file = new File(model.inputUri.getPath()); + File file = new File(inputUri.getPath()); File parentDir = file.exists() ? file.getParentFile() : Constants.Path.APP_DIR; File targetFile = new File(parentDir, targetName); FileHelper.saveFile(this, getString(R.string.title_encrypt_to_file), @@ -267,12 +276,17 @@ public class EncryptFilesFragment public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.encrypt_save: { - mShareAfterEncrypt = false; + mAfterEncryptAction = AfterEncryptAction.SAVE; cryptoOperation(); break; } case R.id.encrypt_share: { - mShareAfterEncrypt = true; + mAfterEncryptAction = AfterEncryptAction.SHARE; + cryptoOperation(); + break; + } + case R.id.encrypt_copy: { + mAfterEncryptAction = AfterEncryptAction.COPY; cryptoOperation(); break; } @@ -375,13 +389,14 @@ public class EncryptFilesFragment protected void onCryptoOperationSuccess(final SignEncryptResult result) { if (mDeleteAfterEncrypt) { + // TODO make behavior coherent here DeleteFileDialogFragment deleteFileDialog = DeleteFileDialogFragment.newInstance(mFilesAdapter.getAsArrayList()); deleteFileDialog.setOnDeletedListener(new DeleteFileDialogFragment.OnDeletedListener() { @Override public void onDeleted() { - if (mShareAfterEncrypt) { + if (mAfterEncryptAction == AfterEncryptAction.SHARE) { // Share encrypted message/file startActivity(sendWithChooserExcludingEncrypt()); } else { @@ -393,12 +408,35 @@ public class EncryptFilesFragment }); deleteFileDialog.show(getActivity().getSupportFragmentManager(), "deleteDialog"); } else { - if (mShareAfterEncrypt) { - // Share encrypted message/file - startActivity(sendWithChooserExcludingEncrypt()); - } else { - // Save encrypted file - result.createNotify(getActivity()).show(); + + switch (mAfterEncryptAction) { + + case SHARE: + // Share encrypted message/file + startActivity(sendWithChooserExcludingEncrypt()); + break; + + case COPY: + Activity activity = getActivity(); + if (activity == null) { + // it's gone, there's nothing we can do here + return; + } + + ClipboardManager clipMan = (ClipboardManager) activity.getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clip = new ClipData(getString(R.string.label_clip_title), + // make available as application/pgp-encrypted + new String[] { "text/plain" }, + new ClipData.Item(mOutputUris.get(0)) + ); + clipMan.setPrimaryClip(clip); + result.createNotify(getActivity()).show(); + break; + + case SAVE: + // Encrypted file was saved already, just show notification + result.createNotify(getActivity()).show(); + break; } } @@ -407,38 +445,44 @@ public class EncryptFilesFragment // prepares mOutputUris, either directly and returns false, or indirectly // which returns true and will call cryptoOperation after mOutputUris has // been set at a later point. - private boolean prepareOutputStreams(boolean share) { + private boolean prepareOutputStreams() { - if (mFilesModels.isEmpty()) { - Notify.create(getActivity(), R.string.no_file_selected, Notify.Style.ERROR) - .show(this); - return true; - } else if (mFilesModels.size() > 1 && !mShareAfterEncrypt) { - Log.e(Constants.TAG, "Aborting: mInputUris.size() > 1 && !mShareAfterEncrypt"); - // This should be impossible... - return true; - } + switch (mAfterEncryptAction) { + default: + case SHARE: + mOutputUris = new ArrayList<>(); + int filenameCounter = 1; + for (FilesAdapter.ViewModel model : mFilesAdapter.mDataset) { + String targetName = (mEncryptFilenames + ? String.valueOf(filenameCounter) : FileHelper.getFilename(getActivity(), model.inputUri)) + + (mUseArmor ? Constants.FILE_EXTENSION_ASC : Constants.FILE_EXTENSION_PGP_MAIN); + mOutputUris.add(TemporaryStorageProvider.createFile(getActivity(), targetName)); + filenameCounter++; + } + return false; - if (share) { - mOutputUris = new ArrayList<>(); - int filenameCounter = 1; - for (FilesAdapter.ViewModel model : mFilesModels) { - String targetName = - (mEncryptFilenames ? String.valueOf(filenameCounter) : FileHelper.getFilename(getActivity(), model.inputUri)) - + (mUseArmor ? Constants.FILE_EXTENSION_ASC : Constants.FILE_EXTENSION_PGP_MAIN); - mOutputUris.add(TemporaryStorageProvider.createFile(getActivity(), targetName)); - filenameCounter++; - } - return false; - } else { - if (mFilesModels.size() > 1) { - Notify.create(getActivity(), R.string.error_multi_not_supported, - Notify.Style.ERROR).show(this); + case SAVE: + if (mFilesAdapter.getModelCount() > 1) { + Notify.create(getActivity(), R.string.error_multi_files, Notify.Style.ERROR).show(this); + return true; + } + showOutputFileDialog(); return true; - } - showOutputFileDialog(); - return true; + + case COPY: + // nothing to do here, but make sure + if (mFilesAdapter.getModelCount() > 1) { + Notify.create(getActivity(), R.string.error_multi_clipboard, Notify.Style.ERROR).show(this); + return true; + } + mOutputUris = new ArrayList<>(); + String targetName = (mEncryptFilenames + ? String.valueOf(1) : FileHelper.getFilename(getActivity(), + mFilesAdapter.getModelItem(0).inputUri)) + Constants.FILE_EXTENSION_ASC; + mOutputUris.add(TemporaryStorageProvider.createFile(getActivity(), targetName, "text/plain")); + return false; } + } protected SignEncryptParcel createOperationInput() { @@ -466,7 +510,7 @@ public class EncryptFilesFragment // if this is still null, prepare output streams again if (mOutputUris == null) { // this may interrupt the flow, and call us again from onActivityResult - if (prepareOutputStreams(mShareAfterEncrypt)) { + if (prepareOutputStreams()) { return null; } } @@ -482,6 +526,11 @@ public class EncryptFilesFragment protected SignEncryptParcel createIncompleteCryptoInput() { + if (mFilesAdapter.getModelCount() == 0) { + Notify.create(getActivity(), R.string.error_no_file_selected, Notify.Style.ERROR).show(this); + return null; + } + // fill values for this action SignEncryptParcel data = new SignEncryptParcel(); @@ -493,7 +542,7 @@ public class EncryptFilesFragment data.setCompressionId(CompressionAlgorithmTags.UNCOMPRESSED); } data.setHiddenRecipients(mHiddenRecipients); - data.setEnableAsciiArmorOutput(mUseArmor); + data.setEnableAsciiArmorOutput(mAfterEncryptAction == AfterEncryptAction.COPY || mUseArmor); data.setSymmetricEncryptionAlgorithm(PgpConstants.OpenKeychainSymmetricKeyAlgorithmTags.USE_PREFERRED); data.setSignatureHashAlgorithm(PgpConstants.OpenKeychainSymmetricKeyAlgorithmTags.USE_PREFERRED); @@ -504,12 +553,14 @@ public class EncryptFilesFragment long[] encryptionKeyIds = modeFragment.getAsymmetricEncryptionKeyIds(); long signingKeyId = modeFragment.getAsymmetricSigningKeyId(); - boolean gotEncryptionKeys = (encryptionKeyIds != null - && encryptionKeyIds.length > 0); + boolean gotEncryptionKeys = (encryptionKeyIds != null && encryptionKeyIds.length > 0); - if (!gotEncryptionKeys && signingKeyId == 0) { - Notify.create(getActivity(), R.string.select_encryption_or_signature_key, Notify.Style.ERROR) - .show(this); + if (!gotEncryptionKeys && signingKeyId != 0) { + Notify.create(getActivity(), R.string.error_detached_signature, Notify.Style.ERROR).show(this); + return null; + } + if (!gotEncryptionKeys) { + Notify.create(getActivity(), R.string.select_encryption_key, Notify.Style.ERROR).show(this); return null; } @@ -542,7 +593,7 @@ public class EncryptFilesFragment // we don't want to encrypt the encrypted, no inception ;) String[] blacklist = new String[]{ - Constants.PACKAGE_NAME + ".ui.EncryptFileActivity", + Constants.PACKAGE_NAME + ".ui.EncryptFilesActivity", "org.thialfihar.android.apg.ui.EncryptActivity" }; @@ -599,7 +650,8 @@ public class EncryptFilesFragment if (resultCode == Activity.RESULT_OK && data != null) { mOutputUris = new ArrayList<>(1); mOutputUris.add(data.getData()); - mShareAfterEncrypt = false; + // make sure this is correct at this point + mAfterEncryptAction = AfterEncryptAction.SAVE; cryptoOperation(); } return; @@ -688,9 +740,9 @@ public class EncryptFilesFragment } // Provide a suitable constructor (depends on the kind of dataset) - public FilesAdapter(Activity activity, List myDataset, View.OnClickListener onFooterClickListener) { + public FilesAdapter(Activity activity, View.OnClickListener onFooterClickListener) { mActivity = activity; - mDataset = myDataset; + mDataset = new ArrayList<>(); mFooterOnClickListener = onFooterClickListener; } @@ -744,7 +796,8 @@ public class EncryptFilesFragment // Return the size of your dataset (invoked by the layout manager) @Override public int getItemCount() { - return mDataset.size() + 1; + // one extra for the footer! + return mDataset.size() +1; } @Override @@ -784,6 +837,14 @@ public class EncryptFilesFragment } } + public int getModelCount() { + return mDataset.size(); + } + + public ViewModel getModelItem(int position) { + return mDataset.get(position); + } + public void remove(ViewModel model) { int position = mDataset.indexOf(model); mDataset.remove(position); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptTextFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptTextFragment.java index d93b52453..e0629eb73 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptTextFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptTextFragment.java @@ -241,7 +241,7 @@ public class EncryptTextFragment && encryptionKeyIds.length > 0); if (!gotEncryptionKeys && signingKeyId == Constants.key.none) { - Notify.create(getActivity(), R.string.select_encryption_or_signature_key, Notify.Style.ERROR) + Notify.create(getActivity(), R.string.error_no_encryption_or_signature_key, Notify.Style.ERROR) .show(this); return null; } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/MainActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/MainActivity.java index a0f6d0e1b..ec6fd1bbe 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/MainActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/MainActivity.java @@ -45,13 +45,15 @@ import org.sufficientlysecure.keychain.util.Preferences; public class MainActivity extends BaseNfcActivity implements FabContainer, OnBackStackChangedListener { - private static final int ID_KEYS = 1; - private static final int ID_ENCRYPT_DECRYPT = 2; - private static final int ID_APPS = 3; - private static final int ID_SETTINGS = 4; - private static final int ID_HELP = 5; + static final int ID_KEYS = 1; + static final int ID_ENCRYPT_DECRYPT = 2; + static final int ID_APPS = 3; + static final int ID_SETTINGS = 4; + static final int ID_HELP = 5; + // both of these are used for instrumentation testing only public static final String EXTRA_SKIP_FIRST_TIME = "skip_first_time"; + public static final String EXTRA_INIT_FRAG = "init_frag"; public Drawer.Result mDrawerResult; private Toolbar mToolbar; @@ -134,8 +136,20 @@ public class MainActivity extends BaseNfcActivity implements FabContainer, OnBac } if (savedInstanceState == null) { - // initialize FragmentLayout with KeyListFragment at first + // always initialize keys fragment to the bottom of the backstack onKeysSelected(); + + if (data != null && data.hasExtra(EXTRA_INIT_FRAG)) { + // initialize FragmentLayout with KeyListFragment at first + switch (data.getIntExtra(EXTRA_INIT_FRAG, -1)) { + case ID_ENCRYPT_DECRYPT: + onEnDecryptSelected(); + break; + case ID_APPS: + onAppsSelected(); + break; + } + } } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/CryptoOperationFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/CryptoOperationFragment.java index 2be162e95..764602735 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/CryptoOperationFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/CryptoOperationFragment.java @@ -123,6 +123,14 @@ public abstract class CryptoOperationFragment 0) { return; } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/FileHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/FileHelper.java index 677acb1b8..4a00f46cb 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/FileHelper.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/FileHelper.java @@ -20,6 +20,7 @@ package org.sufficientlysecure.keychain.util; import android.annotation.TargetApi; import android.app.Activity; import android.content.ActivityNotFoundException; +import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.database.Cursor; @@ -41,7 +42,14 @@ import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.compatibility.DialogFragmentWorkaround; import org.sufficientlysecure.keychain.ui.dialog.FileDialogFragment; +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; import java.text.DecimalFormat; public class FileHelper { @@ -49,7 +57,6 @@ public class FileHelper { /** * Checks if external storage is mounted if file is located on external storage * - * @param file * @return true if storage is mounted */ public static boolean isStorageMounted(String file) { @@ -66,7 +73,6 @@ public class FileHelper { * Opens the preferred installed file manager on Android and shows a toast if no manager is * installed. * - * @param fragment * @param last default selected Uri, not supported by all file managers * @param mimeType can be text/plain for example * @param requestCode requestCode used to identify the result coming back from file manager to @@ -141,7 +147,6 @@ public class FileHelper { /** * Opens the storage browser on Android 4.4 or later for opening a file * - * @param fragment * @param mimeType can be text/plain for example * @param multiple allow file chooser to return multiple files * @param requestCode used to identify the result coming back from storage browser onActivityResult() in your @@ -159,7 +164,6 @@ public class FileHelper { /** * Opens the storage browser on Android 4.4 or later for saving a file * - * @param fragment * @param mimeType can be text/plain for example * @param suggestedName a filename desirable for the file to be saved * @param requestCode used to identify the result coming back from storage browser onActivityResult() in your @@ -234,7 +238,67 @@ public class FileHelper { return new DecimalFormat("#,##0.#").format(size / Math.pow(1024, digitGroups)) + " " + units[digitGroups]; } - public static interface FileDialogCallback { - public void onFileSelected(File file, boolean checked); + public static String readTextFromUri(Context context, Uri outputUri, String charset) + throws IOException { + + byte[] decryptedMessage; + { + InputStream in = context.getContentResolver().openInputStream(outputUri); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + byte[] buf = new byte[256]; + int read; + while ( (read = in.read(buf)) > 0) { + out.write(buf, 0, read); + } + in.close(); + out.close(); + decryptedMessage = out.toByteArray(); + } + + String plaintext; + if (charset != null) { + try { + plaintext = new String(decryptedMessage, charset); + } catch (UnsupportedEncodingException e) { + // if we can't decode properly, just fall back to utf-8 + plaintext = new String(decryptedMessage); + } + } else { + plaintext = new String(decryptedMessage); + } + + return plaintext; + + } + + public static void copyUriData(Context context, Uri fromUri, Uri toUri) throws IOException { + BufferedInputStream bis = null; + BufferedOutputStream bos = null; + + try { + ContentResolver resolver = context.getContentResolver(); + bis = new BufferedInputStream(resolver.openInputStream(fromUri)); + bos = new BufferedOutputStream(resolver.openOutputStream(toUri)); + byte[] buf = new byte[1024]; + int len; + while ( (len = bis.read(buf)) > 0) { + bos.write(buf, 0, len); + } + } finally { + try { + if (bis != null) { + bis.close(); + } + if (bos != null) { + bos.close(); + } + } catch (IOException e) { + // ignore, it's just stream closin' + } + } + } + + public interface FileDialogCallback { + void onFileSelected(File file, boolean checked); } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ParcelableHashMap.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ParcelableHashMap.java new file mode 100644 index 000000000..fa4081acc --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ParcelableHashMap.java @@ -0,0 +1,63 @@ +package org.sufficientlysecure.keychain.util; + + +import java.util.HashMap; + +import android.os.Parcel; +import android.os.Parcelable; +import android.support.annotation.NonNull; + +import org.sufficientlysecure.keychain.KeychainApplication; + + +public class ParcelableHashMap implements Parcelable { + + HashMap mInner; + + public ParcelableHashMap(HashMap inner) { + mInner = inner; + } + + protected ParcelableHashMap(@NonNull Parcel in) { + mInner = new HashMap<>(); + ClassLoader loader = KeychainApplication.class.getClassLoader(); + + int num = in.readInt(); + while (num-- > 0) { + K key = in.readParcelable(loader); + V val = in.readParcelable(loader); + mInner.put(key, val); + } + } + + public HashMap getMap() { + return mInner; + } + + @Override + public void writeToParcel(Parcel parcel, int flags) { + parcel.writeInt(mInner.size()); + for (HashMap.Entry entry : mInner.entrySet()) { + parcel.writeParcelable(entry.getKey(), 0); + parcel.writeParcelable(entry.getValue(), 0); + } + } + + public static final Creator CREATOR = new Creator() { + @Override + public ParcelableHashMap createFromParcel(Parcel in) { + return new ParcelableHashMap(in); + } + + @Override + public ParcelableHashMap[] newArray(int size) { + return new ParcelableHashMap[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Preferences.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Preferences.java index f4c6f7f94..a5b0088c0 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Preferences.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Preferences.java @@ -290,4 +290,9 @@ public class Preferences { .commit(); } } + + public void clear() { + mSharedPreferences.edit().clear().commit(); + } + } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ShareHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ShareHelper.java index 120b84a3b..0297d149c 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ShareHelper.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ShareHelper.java @@ -91,6 +91,7 @@ public class ShareHelper { // Create chooser with only one Intent in it Intent chooserIntent = Intent.createChooser(targetedShareIntents.remove(targetedShareIntents.size() - 1), title); // append all other Intents + // TODO this line looks wrong?! chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, targetedShareIntents.toArray(new Parcelable[]{})); return chooserIntent; } diff --git a/OpenKeychain/src/main/res/drawable-hdpi/ic_menu_moreoverflow_normal_holo_light.png b/OpenKeychain/src/main/res/drawable-hdpi/ic_menu_moreoverflow_normal_holo_light.png new file mode 100644 index 000000000..bb6aef1d0 Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-hdpi/ic_menu_moreoverflow_normal_holo_light.png differ diff --git a/OpenKeychain/src/main/res/drawable-mdpi/ic_menu_moreoverflow_normal_holo_light.png b/OpenKeychain/src/main/res/drawable-mdpi/ic_menu_moreoverflow_normal_holo_light.png new file mode 100644 index 000000000..01d681697 Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-mdpi/ic_menu_moreoverflow_normal_holo_light.png differ diff --git a/OpenKeychain/src/main/res/drawable-xhdpi/ic_menu_moreoverflow_normal_holo_light.png b/OpenKeychain/src/main/res/drawable-xhdpi/ic_menu_moreoverflow_normal_holo_light.png new file mode 100644 index 000000000..930ca8d95 Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-xhdpi/ic_menu_moreoverflow_normal_holo_light.png differ diff --git a/OpenKeychain/src/main/res/layout/decrypt_files_activity.xml b/OpenKeychain/src/main/res/layout/decrypt_files_activity.xml index 6c8a2e859..3d214dbf6 100644 --- a/OpenKeychain/src/main/res/layout/decrypt_files_activity.xml +++ b/OpenKeychain/src/main/res/layout/decrypt_files_activity.xml @@ -5,7 +5,7 @@ + layout="@layout/toolbar_standalone_white" /> - - - - - - -