diff options
Diffstat (limited to 'OpenKeychain')
85 files changed, 3822 insertions, 1625 deletions
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 Binary files differindex cd6aee896..d41bcc446 100644 --- a/OpenKeychain/build/outputs/code-coverage/connected/coverage.ec +++ b/OpenKeychain/build/outputs/code-coverage/connected/coverage.ec diff --git a/OpenKeychain/src/androidTest/assets/ci.png b/OpenKeychain/src/androidTest/assets/ci.png Binary files differnew file mode 100644 index 000000000..3a6117082 --- /dev/null +++ b/OpenKeychain/src/androidTest/assets/ci.png diff --git a/OpenKeychain/src/androidTest/assets/pa.png b/OpenKeychain/src/androidTest/assets/pa.png Binary files differnew file mode 100644 index 000000000..3c6aa7fda --- /dev/null +++ b/OpenKeychain/src/androidTest/assets/pa.png diff --git a/OpenKeychain/src/androidTest/assets/re.png b/OpenKeychain/src/androidTest/assets/re.png Binary files differnew file mode 100644 index 000000000..a441bbc87 --- /dev/null +++ b/OpenKeychain/src/androidTest/assets/re.png 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 <look@my.amazin.horse> - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ - -package org.sufficientlysecure.keychain; - - -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<MainActivity> mActivity - = new ActivityTestRule<MainActivity>(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<View> 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> 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<View> 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<Activity> 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 <look@my.amazin.horse> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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<View> { + + 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<View> withDisplayedChild(final int child) { + return new BoundedMatcher<View, ViewAnimator>(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<View> withSnackbarLineColor(@ColorRes final int colorRes) { return new BoundedMatcher<View, Snackbar>(Snackbar.class) { public void describeTo(Description description) { @@ -80,5 +104,57 @@ public abstract class CustomMatchers { }; } + public static Matcher<View> withRecyclerView(@IdRes int viewId) { + return allOf(isAssignableFrom(RecyclerView.class), withId(viewId)); + } + + public static Matcher<View> isRecyclerItemView(@IdRes int recyclerId, Matcher<View> specificChildMatcher) { + return allOf(withParent(withRecyclerView(recyclerId)), specificChildMatcher); + } + + public static Matcher<View> 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<View> 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<View> 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<View> { } // 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 <look@my.amazin.horse> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package org.sufficientlysecure.keychain.ui; + + +import java.io.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<MainActivity> mActivity + = new IntentsTestRule<MainActivity>(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 index 15de32c8a..11c9f1bee 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 <http://www.gnu.org/licenses/>. */ -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<MainActivity> 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,22 @@ 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(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())); - - 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))); + 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()); + + // "delete file" shouldn't be there + onView(withText(R.string.btn_delete_original)).check(doesNotExist()); + + // 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 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 <http://www.gnu.org/licenses/>. */ -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<MainActivity> mActivity - = new ActivityTestRule<MainActivity>(MainActivity.class) { - @Override - protected Intent getActivityIntent() { - Intent intent = super.getActivityIntent(); - intent.putExtra(MainActivity.EXTRA_SKIP_FIRST_TIME, true); - return intent; - } - }; + public final ActivityTestRule<CreateKeyActivity> 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 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 <http://www.gnu.org/licenses/>. */ -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 <look@my.amazin.horse> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package org.sufficientlysecure.keychain.ui; + + +import java.io.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<MainActivity> mActivityRule + = new IntentsTestRule<MainActivity>(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 <look@my.amazin.horse> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package org.sufficientlysecure.keychain.ui; + + +import android.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<MainActivity> mActivity + = new IntentsTestRule<MainActivity>(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 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 <http://www.gnu.org/licenses/>. */ -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 @@ </intent-filter> </activity> <activity - android:name=".ui.DecryptTextActivity" + android:name=".ui.DisplayTextActivity" android:configChanges="orientation|screenSize|keyboardHidden|keyboard" android:label="@string/title_decrypt" android:parentActivityName=".ui.MainActivity" @@ -203,25 +203,9 @@ <meta-data android:name="android.support.PARENT_ACTIVITY" android:value=".ui.MainActivity" /> - - <!-- DECRYPT_TEXT with text as extra --> - <intent-filter> - <action android:name="org.sufficientlysecure.keychain.action.DECRYPT_TEXT" /> - - <category android:name="android.intent.category.DEFAULT" /> - </intent-filter> - <!-- Android's Send Action --> - <intent-filter android:label="@string/intent_send_decrypt"> - <action android:name="android.intent.action.SEND" /> - - <category android:name="android.intent.category.DEFAULT" /> - - <data android:mimeType="text/*" /> - <data android:mimeType="message/*" /> - </intent-filter> </activity> <activity - android:name=".ui.DecryptFilesActivity" + android:name=".ui.DecryptActivity" android:configChanges="orientation|screenSize|keyboardHidden|keyboard" android:label="@string/title_decrypt" android:parentActivityName=".ui.MainActivity" @@ -266,13 +250,23 @@ <data android:scheme="file" /> <data android:scheme="content" /> </intent-filter> - <!-- Android's Send Action --> + + <!-- DECRYPT_TEXT --> + <intent-filter> + <action android:name="org.sufficientlysecure.keychain.action.DECRYPT_TEXT" /> + + <category android:name="android.intent.category.DEFAULT" /> + </intent-filter> + + <!-- Android's Send and Multi-Send Actions --> <intent-filter android:label="@string/intent_send_decrypt"> <action android:name="android.intent.action.SEND" /> + <action android:name="android.intent.action.SEND_MULTIPLE" /> <category android:name="android.intent.category.DEFAULT" /> - <!-- everything except text/* and message/* --> + <data android:mimeType="text/*" /> + <data android:mimeType="message/*" /> <data android:mimeType="image/*" /> <data android:mimeType="audio/*" /> <data android:mimeType="video/*" /> 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<ByteBuffer, byte[]> mSessionKeyCache; + + private OperatorHelper mOperatorHelper; + + public CachingDataDecryptorFactory(String providerName, + final Map<ByteBuffer,byte[]> sessionKeyCache) + { + mWrappedDecryptor = null; + mSessionKeyCache = sessionKeyCache; + + mOperatorHelper = new OperatorHelper(new NamedJcaJceHelper(providerName)); + } + + public CachingDataDecryptorFactory(PublicKeyDataDecryptorFactory wrapped, + final Map<ByteBuffer,byte[]> 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<ByteBuffer, byte[]> 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<ByteBuffer,byte[]> 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<ByteBuffer,byte[]> 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); - } - } - - /** - * 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); + ClipData clip = ClipData.newPlainText(clipboardLabel, text); + clipboard.setPrimaryClip(clip); - 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); + public static CharSequence getClipboardText(Context context) { + ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); - 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<DecryptVerifyResult> CREATOR = new Creator<DecryptVerifyResult>() { 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<PgpDecryptVerifyInputParcel> 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<PgpDecryptVerifyInputParcel> 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<PgpDecryptVerifyInputParcel> } } - 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<PgpDecryptVerifyInputParcel> 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<PgpDecryptVerifyInputParcel> // 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 { * <p/> * 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,12 +166,33 @@ 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)) { values.put(COLUMN_TIME, System.currentTimeMillis()); @@ -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<ByteBuffer, byte[]> cachedSessionKeys) { + mCryptoData.putAll(cachedSessionKeys); + } + public Map<ByteBuffer, byte[]> 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 <dominik@dominikschuermann.de> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package org.sufficientlysecure.keychain.ui; + + +import java.io.IOException; +import java.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<Uri> 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.<Uri>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<Uri> 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 <dominik@dominikschuermann.de> - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ - -package org.sufficientlysecure.keychain.ui; - -import 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.<Uri>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<PgpDecryptVerifyInputParcel, DecryptVerifyResult> - implements LoaderManager.LoaderCallbacks<Cursor> { +public abstract class DecryptFragment extends Fragment implements LoaderManager.LoaderCallbacks<Cursor> { 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 <dominik@dominikschuermann.de> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package org.sufficientlysecure.keychain.ui; + + +import java.io.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<PgpDecryptVerifyInputParcel,DecryptVerifyResult> + 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<Uri> mInputUris; + private HashMap<Uri, Uri> mOutputUris; + private ArrayList<Uri> mPendingInputUris; + + private Uri mCurrentInputUri; + + private DecryptFilesAdapter mAdapter; + + /** + * Creates new instance of this fragment + */ + public static DecryptListFragment newInstance(ArrayList<Uri> 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<Uri,DecryptVerifyResult> 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<Uri> inputUris = getArguments().getParcelableArrayList(ARG_INPUT_URIS); + ParcelableHashMap<Uri,Uri> outputUris = args.getParcelable(ARG_OUTPUT_URIS); + ParcelableHashMap<Uri,DecryptVerifyResult> results = args.getParcelable(ARG_RESULTS); + + displayInputUris(inputUris, + outputUris != null ? outputUris.getMap() : null, + results != null ? results.getMap() : null + ); + } + + private void displayInputUris(ArrayList<Uri> inputUris, HashMap<Uri,Uri> outputUris, + HashMap<Uri,DecryptVerifyResult> results) { + + mInputUris = inputUris; + mOutputUris = outputUris != null ? outputUris : new HashMap<Uri,Uri>(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<Void, Void, Drawable>() { + @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<ResolveInfo> 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<Void,Void,Intent>() { + + @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<ViewHolder> { + private Context mContext; + private ArrayList<ViewModel> 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 <dominik@dominikschuermann.de> - * Copyright (C) 2010-2014 Thialfihar <thi@thialfihar.org> - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ - -package org.sufficientlysecure.keychain.ui; - -import android.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 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 <dominik@dominikschuermann.de> + * Copyright (C) 2012-2015 Dominik Schürmann <dominik@dominikschuermann.de> + * Copyright (C) 2010-2014 Thialfihar <thi@thialfihar.org> * * 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; - -public class DecryptFilesActivity extends BaseActivity { +import org.sufficientlysecure.keychain.util.FileHelper; - /* Intents */ - public static final String ACTION_DECRYPT_DATA = OpenKeychainIntents.DECRYPT_DATA; +public class DisplayTextActivity extends BaseActivity { - // 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; + if (savedInstanceState != null) { + return; } - // No need to initialize fragments if we are being restored - if (savedInstanceState != null) { + 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; } - // 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); + if (plaintext != null) { + loadFragment(plaintext, result); + } else { + Toast.makeText(this, R.string.error_invalid_data, Toast.LENGTH_LONG).show(); finish(); } + } - boolean showOpenDialog = ACTION_DECRYPT_DATA_OPEN.equals(action); - DecryptFilesFragment frag = DecryptFilesFragment.newInstance(uri, showOpenDialog); + 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 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,28 +31,25 @@ 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); @@ -60,17 +57,6 @@ public class DecryptTextFragment extends DecryptFragment { } /** - * 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 */ private Intent sendWithChooserExcludingDecrypt(String text) { @@ -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,28 +89,37 @@ 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); - - if (savedInstanceState == null) { - cryptoOperation(); - } - + @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; } @Override - public void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); + 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); - outState.putString(ARG_CIPHERTEXT, mCiphertext); - outState.putBoolean(ARG_SHOW_MENU, mShowMenuOptions); - // no need to save the decrypted text, it's in the textview + // display signature result in activity + mText.setText(plaintext); + loadVerifyResult(result); } @Override + protected void onVerifyLoaded(boolean hideErrorOverlay) { + mShowMenuOptions = hideErrorOverlay; + getActivity().supportInvalidateOptionsMenu(); + } + + @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); if (mShowMenuOptions) { @@ -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<Uri> 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<SignEncryptParcel, SignEncryptResult> { @@ -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<Uri> mOutputUris; private RecyclerView mSelectedFiles; - ArrayList<FilesAdapter.ViewModel> 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) { - - 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; - } - - 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); + private boolean prepareOutputStreams() { + + 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; + + 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<ViewModel> 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 <T extends Parcelable, S extends O protected abstract T createOperationInput(); protected void cryptoOperation(CryptoInputParcel cryptoInput) { + cryptoOperation(cryptoInput, true); + } + + protected void cryptoOperation() { + cryptoOperation(new CryptoInputParcel()); + } + + protected void cryptoOperation(CryptoInputParcel cryptoInput, boolean showProgress) { T operationInput = createOperationInput(); if (operationInput == null) { @@ -155,24 +163,30 @@ public abstract class CryptoOperationFragment <T extends Parcelable, S extends O onHandleResult(result); } } + + @Override + protected void onSetProgress(String msg, int progress, int max) { + // allow handling of progress in fragment, or delegate upwards + if ( ! onCryptoSetProgress(msg, progress, max)) { + super.onSetProgress(msg, progress, max); + } + } }; // Create a new Messenger for the communication back Messenger messenger = new Messenger(saveHandler); intent.putExtra(KeychainService.EXTRA_MESSENGER, messenger); - saveHandler.showProgressDialog( - getString(R.string.progress_building_key), - ProgressDialog.STYLE_HORIZONTAL, false); + if (showProgress) { + saveHandler.showProgressDialog( + getString(R.string.progress_building_key), + ProgressDialog.STYLE_HORIZONTAL, false); + } getActivity().startService(intent); } - protected void cryptoOperation() { - cryptoOperation(new CryptoInputParcel()); - } - protected void onCryptoOperationResult(S result) { if (result.success()) { onCryptoOperationSuccess(result); @@ -213,5 +227,8 @@ public abstract class CryptoOperationFragment <T extends Parcelable, S extends O } + protected boolean onCryptoSetProgress(String msg, int progress, int max) { + return false; + } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/KeyFormattingUtils.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/KeyFormattingUtils.java index dd85a6e46..a3cd63d13 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/KeyFormattingUtils.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/KeyFormattingUtils.java @@ -24,9 +24,11 @@ import android.graphics.PorterDuff; import android.text.Spannable; import android.text.SpannableStringBuilder; import android.text.style.ForegroundColorSpan; +import android.view.View; import android.widget.ImageView; import android.widget.TextView; +import org.openintents.openpgp.OpenPgpSignatureResult; import org.spongycastle.asn1.ASN1ObjectIdentifier; import org.spongycastle.asn1.nist.NISTNamedCurves; import org.spongycastle.asn1.teletrust.TeleTrusTNamedCurves; @@ -34,6 +36,8 @@ import org.spongycastle.bcpg.PublicKeyAlgorithmTags; import org.spongycastle.util.encoders.Hex; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.operations.results.DecryptVerifyResult; +import org.sufficientlysecure.keychain.pgp.KeyRing; import org.sufficientlysecure.keychain.service.SaveKeyringParcel.Algorithm; import org.sufficientlysecure.keychain.service.SaveKeyringParcel.Curve; import org.sufficientlysecure.keychain.util.Log; @@ -376,7 +380,6 @@ public class KeyFormattingUtils { /** * Converts the given bytes to a unique RGB color using SHA1 algorithm * - * @param bytes * @return an integer array containing 3 numeric color representations (Red, Green, Black) * @throws java.security.NoSuchAlgorithmException * @throws java.security.DigestException @@ -394,7 +397,7 @@ public class KeyFormattingUtils { public static final int DEFAULT_COLOR = -1; - public static enum State { + public enum State { REVOKED, EXPIRED, VERIFIED, @@ -420,9 +423,165 @@ public class KeyFormattingUtils { setStatusImage(context, statusIcon, statusText, state, color, false); } + public interface StatusHolder { + ImageView getEncryptionStatusIcon(); + TextView getEncryptionStatusText(); + + ImageView getSignatureStatusIcon(); + TextView getSignatureStatusText(); + + View getSignatureLayout(); + TextView getSignatureUserName(); + TextView getSignatureUserEmail(); + TextView getSignatureAction(); + + boolean hasEncrypt(); + + } + + @SuppressWarnings("deprecation") // context.getDrawable is api lvl 21, need to use deprecated + public static void setStatus(Context context, StatusHolder holder, DecryptVerifyResult result) { + + OpenPgpSignatureResult signatureResult = result.getSignatureResult(); + + if (holder.hasEncrypt()) { + int encText, encIcon, encColor; + if (signatureResult != null && signatureResult.isSignatureOnly()) { + encIcon = R.drawable.status_lock_open_24dp; + encText = R.string.decrypt_result_not_encrypted; + encColor = R.color.android_red_light; + } else { + encIcon = R.drawable.status_lock_closed_24dp; + encText = R.string.decrypt_result_encrypted; + encColor = R.color.android_green_light; + } + + int encColorRes = context.getResources().getColor(encColor); + holder.getEncryptionStatusIcon().setColorFilter(encColorRes, PorterDuff.Mode.SRC_IN); + holder.getEncryptionStatusIcon().setImageDrawable(context.getResources().getDrawable(encIcon)); + holder.getEncryptionStatusText().setText(encText); + holder.getEncryptionStatusText().setTextColor(encColorRes); + } + + int sigText, sigIcon, sigColor; + int sigActionText, sigActionIcon; + + if (signatureResult == null) { + + sigText = R.string.decrypt_result_no_signature; + sigIcon = R.drawable.status_signature_invalid_cutout_24dp; + sigColor = R.color.bg_gray; + + // won't be used, but makes compiler happy + sigActionText = 0; + sigActionIcon = 0; + + } else switch (signatureResult.getStatus()) { + + case OpenPgpSignatureResult.SIGNATURE_SUCCESS_CERTIFIED: { + sigText = R.string.decrypt_result_signature_certified; + sigIcon = R.drawable.status_signature_verified_cutout_24dp; + sigColor = R.color.android_green_light; + + sigActionText = R.string.decrypt_result_action_show; + sigActionIcon = R.drawable.ic_vpn_key_grey_24dp; + break; + } + + case OpenPgpSignatureResult.SIGNATURE_SUCCESS_UNCERTIFIED: { + sigText = R.string.decrypt_result_signature_uncertified; + sigIcon = R.drawable.status_signature_unverified_cutout_24dp; + sigColor = R.color.android_orange_light; + + sigActionText = R.string.decrypt_result_action_show; + sigActionIcon = R.drawable.ic_vpn_key_grey_24dp; + break; + } + + case OpenPgpSignatureResult.SIGNATURE_KEY_REVOKED: { + sigText = R.string.decrypt_result_signature_revoked_key; + sigIcon = R.drawable.status_signature_revoked_cutout_24dp; + sigColor = R.color.android_red_light; + + sigActionText = R.string.decrypt_result_action_show; + sigActionIcon = R.drawable.ic_vpn_key_grey_24dp; + break; + } + + case OpenPgpSignatureResult.SIGNATURE_KEY_EXPIRED: { + sigText = R.string.decrypt_result_signature_expired_key; + sigIcon = R.drawable.status_signature_expired_cutout_24dp; + sigColor = R.color.android_red_light; + + sigActionText = R.string.decrypt_result_action_show; + sigActionIcon = R.drawable.ic_vpn_key_grey_24dp; + break; + } + + case OpenPgpSignatureResult.SIGNATURE_KEY_MISSING: { + sigText = R.string.decrypt_result_signature_missing_key; + sigIcon = R.drawable.status_signature_unknown_cutout_24dp; + sigColor = R.color.android_red_light; + + sigActionText = R.string.decrypt_result_action_Lookup; + sigActionIcon = R.drawable.ic_file_download_grey_24dp; + break; + } + + default: + case OpenPgpSignatureResult.SIGNATURE_ERROR: { + sigText = R.string.decrypt_result_invalid_signature; + sigIcon = R.drawable.status_signature_invalid_cutout_24dp; + sigColor = R.color.android_red_light; + + sigActionText = R.string.decrypt_result_action_show; + sigActionIcon = R.drawable.ic_vpn_key_grey_24dp; + break; + } + + } + + int sigColorRes = context.getResources().getColor(sigColor); + holder.getSignatureStatusIcon().setColorFilter(sigColorRes, PorterDuff.Mode.SRC_IN); + holder.getSignatureStatusIcon().setImageDrawable(context.getResources().getDrawable(sigIcon)); + holder.getSignatureStatusText().setText(sigText); + holder.getSignatureStatusText().setTextColor(sigColorRes); + + if (signatureResult != null) { + + holder.getSignatureLayout().setVisibility(View.VISIBLE); + + holder.getSignatureAction().setText(sigActionText); + holder.getSignatureAction().setCompoundDrawablesWithIntrinsicBounds( + 0, 0, sigActionIcon, 0); + + String userId = signatureResult.getPrimaryUserId(); + KeyRing.UserId userIdSplit = KeyRing.splitUserId(userId); + if (userIdSplit.name != null) { + holder.getSignatureUserName().setText(userIdSplit.name); + } else { + holder.getSignatureUserName().setText(R.string.user_id_no_name); + } + if (userIdSplit.email != null) { + holder.getSignatureUserEmail().setVisibility(View.VISIBLE); + holder.getSignatureUserEmail().setText(userIdSplit.email); + } else { + holder.getSignatureUserEmail().setVisibility(View.GONE); + } + + } else { + + holder.getSignatureLayout().setVisibility(View.GONE); + + } + + + } + /** * Sets status image based on constant */ + @SuppressWarnings("deprecation") // context.getDrawable is api lvl 21 public static void setStatusImage(Context context, ImageView statusIcon, TextView statusText, State state, int color, boolean big) { switch (state) { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/ToolableViewAnimator.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/ToolableViewAnimator.java index 44f2b7c3e..18e830139 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/ToolableViewAnimator.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/ToolableViewAnimator.java @@ -27,6 +27,7 @@ package org.sufficientlysecure.keychain.ui.widget; import android.content.Context; import android.content.res.TypedArray; +import android.support.annotation.NonNull; import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; @@ -41,6 +42,20 @@ public class ToolableViewAnimator extends ViewAnimator { private int mInitChild = -1; + public ToolableViewAnimator(Context context) { + super(context); + } + + public ToolableViewAnimator(Context context, AttributeSet attrs) { + super(context, attrs); + + if (isInEditMode()) { + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ToolableViewAnimator); + mInitChild = a.getInt(R.styleable.ToolableViewAnimator_initialView, -1); + a.recycle(); + } + } + public ToolableViewAnimator(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs); @@ -52,7 +67,7 @@ public class ToolableViewAnimator extends ViewAnimator { } @Override - public void addView(View child, int index, ViewGroup.LayoutParams params) { + public void addView(@NonNull View child, int index, ViewGroup.LayoutParams params) { if (isInEditMode() && mInitChild-- > 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 <K extends Parcelable, V extends Parcelable> implements Parcelable { + + HashMap<K,V> mInner; + + public ParcelableHashMap(HashMap<K,V> 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<K,V> getMap() { + return mInner; + } + + @Override + public void writeToParcel(Parcel parcel, int flags) { + parcel.writeInt(mInner.size()); + for (HashMap.Entry<K,V> entry : mInner.entrySet()) { + parcel.writeParcelable(entry.getKey(), 0); + parcel.writeParcelable(entry.getValue(), 0); + } + } + + public static final Creator<ParcelableHashMap> CREATOR = new Creator<ParcelableHashMap>() { + @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 Binary files differnew file mode 100644 index 000000000..bb6aef1d0 --- /dev/null +++ b/OpenKeychain/src/main/res/drawable-hdpi/ic_menu_moreoverflow_normal_holo_light.png 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 Binary files differnew file mode 100644 index 000000000..01d681697 --- /dev/null +++ b/OpenKeychain/src/main/res/drawable-mdpi/ic_menu_moreoverflow_normal_holo_light.png 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 Binary files differnew file mode 100644 index 000000000..930ca8d95 --- /dev/null +++ b/OpenKeychain/src/main/res/drawable-xhdpi/ic_menu_moreoverflow_normal_holo_light.png 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 @@ <include android:id="@+id/toolbar_include" - layout="@layout/toolbar_result_decrypt" /> + layout="@layout/toolbar_standalone_white" /> <!-- fitsSystemWindows and layout_marginTop from diff --git a/OpenKeychain/src/main/res/layout/decrypt_files_fragment.xml b/OpenKeychain/src/main/res/layout/decrypt_files_fragment.xml deleted file mode 100644 index 22ee7e09f..000000000 --- a/OpenKeychain/src/main/res/layout/decrypt_files_fragment.xml +++ /dev/null @@ -1,149 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:orientation="vertical"> - - <LinearLayout - android:layout_width="match_parent" - android:layout_height="match_parent" - android:orientation="vertical"> - - <ScrollView - android:fillViewport="true" - android:paddingTop="8dp" - android:layout_width="match_parent" - android:scrollbars="vertical" - android:layout_height="0dp" - android:layout_weight="1"> - - <LinearLayout - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:orientation="vertical"> - - <View - android:id="@+id/status_divider" - android:layout_height="1dip" - android:layout_width="match_parent" - android:background="?android:attr/listDivider" /> - - <LinearLayout - android:layout_width="match_parent" - android:layout_height="match_parent" - android:paddingTop="4dp" - android:paddingLeft="16dp" - android:paddingRight="16dp" - android:orientation="vertical"> - - <LinearLayout - android:layout_width="match_parent" - android:layout_height="?android:attr/listPreferredItemHeight" - android:orientation="horizontal" - - android:id="@+id/decrypt_files_browse" - android:clickable="true" - android:background="?android:selectableItemBackground"> - - <TextView - android:layout_width="wrap_content" - android:layout_height="match_parent" - android:paddingLeft="8dp" - android:textAppearance="?android:attr/textAppearanceMedium" - android:text="@string/label_file_colon" - android:gravity="center_vertical" /> - - <TextView - android:id="@+id/decrypt_files_filename" - android:paddingLeft="8dp" - android:paddingRight="8dp" - android:textAppearance="?android:attr/textAppearanceMedium" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:hint="@string/filemanager_title_open" - android:drawableRight="@drawable/ic_folder_grey_24dp" - android:drawablePadding="8dp" - android:gravity="center_vertical" /> - </LinearLayout> - - <View - android:layout_width="match_parent" - android:layout_height="1dip" - android:background="?android:attr/listDivider" - android:layout_marginBottom="8dp" /> - - <CheckBox - android:id="@+id/decrypt_files_delete_after_decryption" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:text="@string/label_delete_after_decryption" /> - - <RelativeLayout - android:layout_width="match_parent" - android:layout_height="match_parent"> - - <TextView - android:id="@+id/decrypt_files_action_decrypt" - android:paddingLeft="8dp" - android:paddingRight="8dp" - android:textAppearance="?android:attr/textAppearanceMedium" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:minHeight="?android:attr/listPreferredItemHeight" - android:text="@string/btn_decrypt_verify_file" - android:clickable="true" - android:background="?android:selectableItemBackground" - android:drawableRight="@drawable/ic_save_grey_24dp" - android:drawablePadding="8dp" - android:gravity="center_vertical" - android:layout_alignParentBottom="true" - android:layout_alignParentLeft="true" - android:layout_alignParentStart="true" /> - - <View - android:layout_width="match_parent" - android:layout_height="1dip" - android:background="?android:attr/listDivider" - android:layout_above="@+id/decrypt_files_action_decrypt" /> - - </RelativeLayout> - </LinearLayout> - </LinearLayout> - </ScrollView> - </LinearLayout> - - <!-- TODO: Use this layout later to hide file list --> - <LinearLayout - android:visibility="gone" - android:id="@+id/decrypt_content" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:orientation="vertical"></LinearLayout> - - <LinearLayout - android:visibility="gone" - android:id="@+id/decrypt_error_overlay" - android:layout_width="wrap_content" - android:layout_height="match_parent" - android:orientation="vertical" - android:gravity="center_vertical"> - - <TextView - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:textAppearance="?android:attr/textAppearanceMedium" - android:text="@string/decrypt_invalid_text" - android:padding="16dp" - android:layout_gravity="center" - android:textColor="@color/android_red_light" /> - - <Button - android:id="@+id/decrypt_error_overlay_button" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:background="@drawable/button_edgy" - android:textColor="@color/android_red_light" - android:text="@string/decrypt_invalid_button" - android:layout_gravity="center_horizontal" /> - </LinearLayout> -</LinearLayout>
\ No newline at end of file diff --git a/OpenKeychain/src/main/res/layout/decrypt_files_input_fragment.xml b/OpenKeychain/src/main/res/layout/decrypt_files_input_fragment.xml new file mode 100644 index 000000000..b7e70ce10 --- /dev/null +++ b/OpenKeychain/src/main/res/layout/decrypt_files_input_fragment.xml @@ -0,0 +1,83 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:paddingTop="4dp" + android:paddingLeft="16dp" + android:paddingRight="16dp" + android:orientation="vertical"> + + <View + android:id="@+id/status_divider" + android:layout_height="1dip" + android:layout_width="match_parent" + android:background="?android:attr/listDivider" /> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="?android:attr/listPreferredItemHeight" + android:orientation="horizontal" + android:id="@+id/decrypt_files_browse" + android:clickable="true" + android:background="?android:selectableItemBackground"> + + <TextView + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:paddingLeft="4dp" + android:paddingRight="4dp" + android:textAppearance="?android:attr/textAppearanceMedium" + android:text="@string/label_file_colon" + android:gravity="center_vertical" /> + + <TextView + android:id="@+id/decrypt_files_filename" + android:paddingLeft="8dp" + android:paddingRight="8dp" + android:textAppearance="?android:attr/textAppearanceMedium" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:hint="@string/filemanager_title_open" + android:drawableRight="@drawable/ic_folder_grey_24dp" + android:drawablePadding="8dp" + android:gravity="center_vertical" /> + + </LinearLayout> + + <View + android:layout_width="match_parent" + android:layout_height="1dip" + android:background="?android:attr/listDivider" + android:layout_marginBottom="8dp" /> + + <RelativeLayout + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <TextView + android:id="@+id/decrypt_files_action_decrypt" + android:paddingLeft="8dp" + android:paddingRight="8dp" + android:textAppearance="?android:attr/textAppearanceMedium" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:minHeight="?android:attr/listPreferredItemHeight" + android:text="@string/btn_decrypt_verify_file" + android:clickable="true" + android:background="?android:selectableItemBackground" + android:drawableRight="@drawable/ic_save_grey_24dp" + android:drawablePadding="8dp" + android:gravity="center_vertical" + android:layout_alignParentBottom="true" + android:layout_alignParentLeft="true" + android:layout_alignParentStart="true" /> + + <View + android:layout_width="match_parent" + android:layout_height="1dip" + android:background="?android:attr/listDivider" + android:layout_above="@+id/decrypt_files_action_decrypt" /> + + </RelativeLayout> + +</LinearLayout>
\ No newline at end of file diff --git a/OpenKeychain/src/main/res/layout/decrypt_files_list_fragment.xml b/OpenKeychain/src/main/res/layout/decrypt_files_list_fragment.xml new file mode 100644 index 000000000..d9cde05f8 --- /dev/null +++ b/OpenKeychain/src/main/res/layout/decrypt_files_list_fragment.xml @@ -0,0 +1,49 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:orientation="vertical" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <LinearLayout + android:id="@+id/decrypt_content" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + + <android.support.v7.widget.RecyclerView + android:id="@+id/decrypted_files_list" + android:scrollbars="vertical" + android:layout_width="match_parent" + android:layout_height="match_parent" /> + + </LinearLayout> + + <LinearLayout + android:visibility="gone" + android:id="@+id/decrypt_error_overlay" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:orientation="vertical" + android:gravity="center_vertical"> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textAppearance="?android:attr/textAppearanceMedium" + android:text="@string/decrypt_invalid_text" + android:padding="16dp" + android:layout_gravity="center" + android:textColor="@color/android_red_light" /> + + <Button + android:id="@+id/decrypt_error_overlay_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:background="@drawable/button_edgy" + android:textColor="@color/android_red_light" + android:text="@string/decrypt_invalid_button" + android:layout_gravity="center_horizontal" /> + </LinearLayout> + +</LinearLayout>
\ No newline at end of file diff --git a/OpenKeychain/src/main/res/layout/decrypt_list_entry.xml b/OpenKeychain/src/main/res/layout/decrypt_list_entry.xml new file mode 100644 index 000000000..d58542e62 --- /dev/null +++ b/OpenKeychain/src/main/res/layout/decrypt_list_entry.xml @@ -0,0 +1,291 @@ +<?xml version="1.0" encoding="utf-8"?> +<android.support.v7.widget.CardView + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + xmlns:custom="http://schemas.android.com/apk/res-auto" + android:id="@+id/card_view" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_margin="4dp" + custom:cardBackgroundColor="@android:color/white" + custom:cardElevation="2dp" + custom:cardUseCompatPadding="true" + custom:cardCornerRadius="4dp" + > + + <org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingTop="4dp" + android:paddingBottom="4dp" + android:paddingRight="8dp" + android:paddingLeft="8dp" + android:inAnimation="@anim/fade_in" + android:outAnimation="@anim/fade_out" + android:id="@+id/view_animator" + android:measureAllChildren="false" + custom:initialView="0" + android:minHeight="?listPreferredItemHeightSmall" + android:animateLayoutChanges="true" + > + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical" + android:orientation="horizontal"> + + <ProgressBar + android:id="@+id/progress" + android:layout_width="30dp" + android:layout_height="30dp" + android:padding="4dp" + android:layout_gravity="center_vertical" /> + + <TextView + android:id="@+id/progress_msg" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginLeft="8dp" + android:layout_marginStart="8dp" + android:layout_gravity="center_vertical" + android:text="@string/progress_processing" + android:textAppearance="?android:attr/textAppearanceMedium" + /> + + </LinearLayout> + <!-- --> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical"> + + <LinearLayout + android:orientation="horizontal" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingRight="4dp" + android:paddingLeft="4dp" + tools:ignore="UseCompoundDrawables"> + + <ImageView + android:id="@+id/result_encryption_icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:src="@drawable/status_lock_open_24dp" + android:layout_gravity="center_vertical" /> + + <TextView + android:id="@+id/result_encryption_text" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textAppearance="?android:attr/textAppearanceMedium" + android:layout_marginLeft="8dp" + android:layout_marginTop="8dp" + android:layout_marginBottom="8dp" + android:text="" + tools:text="Encryption status text" /> + </LinearLayout> + + <LinearLayout + android:orientation="horizontal" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingRight="4dp" + android:paddingLeft="4dp" + tools:ignore="UseCompoundDrawables"> + + <ImageView + android:id="@+id/result_signature_icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:src="@drawable/status_signature_unverified_cutout_24dp" + android:layout_gravity="center_vertical" /> + + <TextView + android:id="@+id/result_signature_text" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textAppearance="?android:attr/textAppearanceMedium" + android:layout_marginLeft="8dp" + android:layout_marginTop="8dp" + android:layout_marginBottom="8dp" + android:text="" + tools:text="Signature status text" /> + </LinearLayout> + + <LinearLayout + android:id="@+id/result_signature_layout" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:clickable="true" + android:background="?android:selectableItemBackground" + android:orientation="horizontal"> + + <LinearLayout + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:paddingRight="4dp" + android:paddingLeft="4dp" + android:gravity="center_vertical" + android:orientation="vertical"> + + <TextView + android:id="@+id/result_signature_name" + android:textAppearance="?android:attr/textAppearanceMedium" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="" + tools:text="Alice" /> + + <TextView + android:id="@+id/result_signature_email" + android:textAppearance="?android:attr/textAppearanceSmall" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="center_vertical" + android:text="" + tools:text="alice@example.com" /> + + </LinearLayout> + + <View + android:layout_width="1dip" + android:layout_height="match_parent" + android:gravity="right" + android:layout_marginBottom="8dp" + android:layout_marginTop="8dp" + android:background="?android:attr/listDivider" /> + + <TextView + android:id="@+id/result_signature_action" + android:paddingLeft="8dp" + android:paddingRight="8dp" + android:textAppearance="?android:attr/textAppearanceMedium" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:drawableRight="@drawable/ic_vpn_key_grey_24dp" + android:drawablePadding="8dp" + android:gravity="center_vertical" + android:text="" + tools:text="Show" + /> + + </LinearLayout> + + <View + android:layout_width="match_parent" + android:layout_height="1dip" + tools:layout_height="2dip" + android:background="?android:attr/listDivider" /> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:id="@+id/file" + android:clickable="true" + android:background="?android:selectableItemBackground" + > + + <ImageView + android:id="@+id/thumbnail" + android:layout_gravity="center_vertical" + android:layout_width="36dp" + android:layout_height="36dp" + android:scaleType="center" + android:padding="6dp" + android:src="@drawable/ic_doc_generic_am" /> + + <LinearLayout + android:orientation="vertical" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical" + android:paddingLeft="8dp" + android:paddingRight="8dp" + android:layout_weight="1"> + + <TextView + android:id="@+id/filename" + android:maxLines="1" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textColor="?android:attr/textColorSecondary" + android:textAppearance="?android:attr/textAppearanceMedium" + android:ellipsize="end" + android:text="" + tools:text="filename.jpg" /> + + <TextView + android:id="@+id/filesize" + android:maxLines="1" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textColor="?android:attr/textColorTertiary" + android:textAppearance="?android:attr/textAppearanceSmall" + android:textSize="12sp" + android:ellipsize="end" + android:text="" + tools:text="14kb" /> + + </LinearLayout> + + <ImageView + android:id="@+id/context_menu" + android:scaleType="center" + android:layout_width="36dip" + android:layout_height="48dip" + android:clickable="true" + android:background="?android:selectableItemBackground" + android:src="@drawable/ic_menu_moreoverflow_normal_holo_light" /> + + </LinearLayout> + + </LinearLayout> + + <LinearLayout + android:orientation="horizontal" + android:layout_width="match_parent" + android:layout_height="wrap_content" + tools:ignore="UseCompoundDrawables"> + + <ImageView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:padding="4dp" + android:src="@drawable/status_signature_invalid_cutout_24dp" + android:tint="@color/android_red_light" + android:layout_gravity="center_vertical" /> + + <TextView + android:id="@+id/result_error_msg" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:textAppearance="?android:attr/textAppearanceMedium" + android:layout_marginLeft="8dp" + android:layout_marginStart="8dp" + android:layout_marginTop="8dp" + android:layout_marginBottom="8dp" + android:text="" + android:layout_gravity="center_vertical" + tools:text="Error processing data!" /> + + <ImageView + android:id="@+id/result_error_log" + android:scaleType="center" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:clickable="true" + android:padding="6dp" + android:background="?android:selectableItemBackground" + android:src="@drawable/ic_view_list_grey_24dp" + android:layout_gravity="center_vertical" /> + + </LinearLayout> + + </org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator> + +</android.support.v7.widget.CardView>
\ No newline at end of file diff --git a/OpenKeychain/src/main/res/layout/decrypt_text_fragment.xml b/OpenKeychain/src/main/res/layout/decrypt_text_fragment.xml index cb20254ff..9362b35bb 100644 --- a/OpenKeychain/src/main/res/layout/decrypt_text_fragment.xml +++ b/OpenKeychain/src/main/res/layout/decrypt_text_fragment.xml @@ -1,9 +1,10 @@ <?xml version="1.0" encoding="utf-8"?> -<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" +<org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator + xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" + xmlns:custom="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" - android:layout_height="match_parent" - android:orientation="vertical"> + android:layout_height="match_parent"> <LinearLayout android:visibility="gone" @@ -63,4 +64,5 @@ android:text="@string/decrypt_invalid_button" android:layout_gravity="center_horizontal" /> </LinearLayout> -</LinearLayout>
\ No newline at end of file + +</org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator>
\ No newline at end of file diff --git a/OpenKeychain/src/main/res/layout/encrypt_decrypt_overview_fragment.xml b/OpenKeychain/src/main/res/layout/encrypt_decrypt_overview_fragment.xml index bd640d9af..394f14a34 100644 --- a/OpenKeychain/src/main/res/layout/encrypt_decrypt_overview_fragment.xml +++ b/OpenKeychain/src/main/res/layout/encrypt_decrypt_overview_fragment.xml @@ -1,5 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" android:paddingTop="4dp" android:paddingLeft="16dp" android:paddingRight="16dp" @@ -89,31 +90,16 @@ style="?android:attr/borderlessButtonStyle" android:orientation="horizontal" android:paddingLeft="8dp" - android:paddingRight="0dp"> + android:paddingRight="0dp" + tools:ignore="UseCompoundDrawables"> - <LinearLayout + <TextView + android:textAppearance="?android:attr/textAppearanceMedium" android:layout_width="0dp" - android:layout_height="match_parent" + android:layout_height="wrap_content" android:layout_weight="1" - android:paddingRight="4dp" - android:gravity="center_vertical" - android:orientation="vertical"> - - <TextView - android:textAppearance="?android:attr/textAppearanceMedium" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:text="@string/btn_decrypt_clipboard" /> - - <TextView - android:textAppearance="?android:attr/textAppearanceSmall" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:textColor="@color/tertiary_text_light" - android:text="@string/btn_decrypt_and_verify" - android:gravity="center_vertical" /> - - </LinearLayout> + android:layout_gravity="center_vertical" + android:text="@string/btn_decrypt_clipboard" /> <ImageView android:id="@+id/clipboard_icon" diff --git a/OpenKeychain/src/main/res/menu/decrypt_item_context_menu.xml b/OpenKeychain/src/main/res/menu/decrypt_item_context_menu.xml new file mode 100644 index 000000000..b825fa498 --- /dev/null +++ b/OpenKeychain/src/main/res/menu/decrypt_item_context_menu.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + + <item + android:id="@+id/view_log" + android:title="@string/btn_view_log" + android:icon="@drawable/ic_view_list_grey_24dp" + /> + + <item + android:id="@+id/decrypt_save" + android:title="@string/btn_save" + android:icon="@drawable/ic_action_encrypt_file_24dp" + /> + + <item + android:id="@+id/decrypt_delete" + android:title="@string/btn_delete_original" + android:icon="@drawable/ic_delete_grey_24dp" + /> + +</menu>
\ No newline at end of file diff --git a/OpenKeychain/src/main/res/menu/encrypt_activity.xml b/OpenKeychain/src/main/res/menu/encrypt_activity.xml index fdd19927e..c71e426c5 100644 --- a/OpenKeychain/src/main/res/menu/encrypt_activity.xml +++ b/OpenKeychain/src/main/res/menu/encrypt_activity.xml @@ -4,6 +4,7 @@ <item android:id="@+id/check_use_symmetric" android:title="@string/label_symmetric" - android:checkable="true" /> + android:checkable="true" + android:orderInCategory="1" /> </menu>
\ No newline at end of file diff --git a/OpenKeychain/src/main/res/menu/encrypt_file_fragment.xml b/OpenKeychain/src/main/res/menu/encrypt_file_fragment.xml index 54c6c2768..e719487a7 100644 --- a/OpenKeychain/src/main/res/menu/encrypt_file_fragment.xml +++ b/OpenKeychain/src/main/res/menu/encrypt_file_fragment.xml @@ -6,32 +6,45 @@ android:id="@+id/encrypt_save" android:title="@string/btn_encrypt_save_file" android:icon="@drawable/ic_action_encrypt_save_24dp" - app:showAsAction="always" /> + android:orderInCategory="0" + app:showAsAction="ifRoom" /> <item android:id="@+id/encrypt_share" android:title="@string/btn_encrypt_share_file" android:icon="@drawable/ic_action_encrypt_share_24dp" - app:showAsAction="always" /> + android:orderInCategory="0" + app:showAsAction="ifRoom" /> + + <item + android:id="@+id/encrypt_copy" + android:title="@string/btn_copy_encrypted_signed" + android:icon="@drawable/ic_action_encrypt_copy_24dp" + android:orderInCategory="0" + app:showAsAction="ifRoom" /> <item android:id="@+id/check_delete_after_encrypt" android:title="@string/label_delete_after_encryption" + android:orderInCategory="1" android:checkable="true" /> <item android:id="@+id/check_enable_compression" android:title="@string/label_enable_compression" + android:orderInCategory="1" android:checkable="true" /> <item android:id="@+id/check_encrypt_filenames" android:title="@string/label_encrypt_filenames" + android:orderInCategory="1" android:checkable="true" /> <item android:id="@+id/check_use_armor" android:title="@string/label_file_ascii_armor" + android:orderInCategory="1" android:checkable="true" /> <!--<item--> diff --git a/OpenKeychain/src/main/res/menu/encrypt_text_fragment.xml b/OpenKeychain/src/main/res/menu/encrypt_text_fragment.xml index e49898093..80b78457d 100644 --- a/OpenKeychain/src/main/res/menu/encrypt_text_fragment.xml +++ b/OpenKeychain/src/main/res/menu/encrypt_text_fragment.xml @@ -6,17 +6,20 @@ android:id="@+id/encrypt_copy" android:title="@string/btn_copy_encrypted_signed" android:icon="@drawable/ic_action_encrypt_copy_24dp" - app:showAsAction="always" /> + android:orderInCategory="1" + app:showAsAction="ifRoom" /> <item android:id="@+id/encrypt_share" android:title="@string/btn_share_encrypted_signed" android:icon="@drawable/ic_action_encrypt_share_24dp" - app:showAsAction="always" /> + android:orderInCategory="1" + app:showAsAction="ifRoom" /> <item android:id="@+id/check_enable_compression" android:title="@string/label_enable_compression" + android:orderInCategory="1" android:checked="true" android:checkable="true" /> diff --git a/OpenKeychain/src/main/res/values-cs/strings.xml b/OpenKeychain/src/main/res/values-cs/strings.xml index 56ba11f29..4c18003a8 100644 --- a/OpenKeychain/src/main/res/values-cs/strings.xml +++ b/OpenKeychain/src/main/res/values-cs/strings.xml @@ -185,7 +185,7 @@ <string name="encrypt_sign_successful">Úspěšně podepsáno a/nebo zašifrováno.</string> <string name="encrypt_sign_clipboard_successful">Úspěšně podepsání a/nebo zašifrováno do schránky.</string> <string name="select_encryption_key">Vyberte alespoň jeden šifrovací klíč.</string> - <string name="select_encryption_or_signature_key">Vyberte alespoň jeden šifrovací nebo podpisový klíč.</string> + <string name="error_no_encryption_or_signature_key">Vyberte alespoň jeden šifrovací nebo podpisový klíč.</string> <string name="also_export_secret_keys">Zárověň exportovat tajný klíč</string> <string name="reinstall_openkeychain">Narazili jste na známou chybu v Androidu. Přeinstalujte prosím OpenKeychain pokud chcete své propojit kontakty s klíči.</string> <string name="key_exported">Úspěšně exportován 1 klíč.</string> diff --git a/OpenKeychain/src/main/res/values-de/strings.xml b/OpenKeychain/src/main/res/values-de/strings.xml index ea04f56b9..d2f643e9f 100644 --- a/OpenKeychain/src/main/res/values-de/strings.xml +++ b/OpenKeychain/src/main/res/values-de/strings.xml @@ -201,7 +201,7 @@ <string name="encrypt_sign_successful">Erfolgreich signiert und/oder verschlüsselt.</string> <string name="encrypt_sign_clipboard_successful">Erfolgreich in die Zwischenablage signiert und/oder verschlüsselt.</string> <string name="select_encryption_key">Mindestens einen Schlüssel zum Verschlüsseln auswählen.</string> - <string name="select_encryption_or_signature_key">Mindestens einen Schlüssel zum Verschlüsseln oder einen zum Signieren auswählen.</string> + <string name="error_no_encryption_or_signature_key">Mindestens einen Schlüssel zum Verschlüsseln oder einen zum Signieren auswählen.</string> <string name="specify_file_to_encrypt_to">Bitte angeben in welche Datei verschlüsselt werden soll.\nWARNUNG: Datei wird überschrieben, wenn sie bereits existiert.</string> <string name="specify_file_to_decrypt_to">Bitte angeben in welche Datei entschlüsselt werden soll.\nWARNUNG: Datei wird überschrieben, wenn sie bereits existiert.</string> <string name="specify_file_to_export_to">Bitte angeben in welche Datei exportiert werden soll.\nWARNUNG: Datei wird überschrieben, wenn sie bereits existiert.</string> @@ -363,7 +363,7 @@ <string name="import_qr_code_button">QR-Code scannen</string> <string name="import_qr_code_text">Halte deine Kamera über den QR-Code!</string> <!--Generic result toast--> - <string name="view_log">Details</string> + <string name="snackbar_details">Details</string> <string name="with_warnings">, mit Warnungen</string> <string name="with_cancelled">. bis abgebrochen wurde</string> <!--Import result toast--> @@ -1132,7 +1132,7 @@ <string name="contact_show_key">Schlüssel anzeigen (%s)</string> <string name="swipe_to_update">Nach unten wischen um von Schlüsselserver zu aktualisieren</string> <string name="error_no_file_selected">Mindestens eine Datei zum Verschlüsseln auswählen!</string> - <string name="error_multi_not_supported">Das speichern von mehreren Dateien wird nicht unterstützt. Dies ist eine Einschränkung der aktuellen Android Version.</string> + <string name="error_multi_files">Das speichern von mehreren Dateien wird nicht unterstützt. Dies ist eine Einschränkung der aktuellen Android Version.</string> <string name="key_colon">Schlüssel:</string> <string name="exchange_description">Um einen Schlüsselaustausch zu starten wähle auf der rechten Seite die Teilnehmer aus, drücke dann den \"Austausch starten\"-Knopf.\n\nDu wirst zusätzlich zwei Fragen gestellt bekommen um sicherzustellen, dass nur die richtigen Teilnehmer am Austausch beteiligt sind und deren Fingerabdrücke korrekt sind.</string> <string name="btn_start_exchange">Austausch starten</string> diff --git a/OpenKeychain/src/main/res/values-es/strings.xml b/OpenKeychain/src/main/res/values-es/strings.xml index 6503fe95b..8b20aeb04 100644 --- a/OpenKeychain/src/main/res/values-es/strings.xml +++ b/OpenKeychain/src/main/res/values-es/strings.xml @@ -201,7 +201,7 @@ <string name="encrypt_sign_successful">Firmado y/o cifrado con éxito.</string> <string name="encrypt_sign_clipboard_successful">Firmado y/o cifrado del portapapeles con éxito.</string> <string name="select_encryption_key">Selecciona al menos una clave de cifrado.</string> - <string name="select_encryption_or_signature_key">Selecciona al menos una clave de cifrado o de firma.</string> + <string name="error_no_encryption_or_signature_key">Selecciona al menos una clave de cifrado o de firma.</string> <string name="specify_file_to_encrypt_to">Por favor especifique hacia qué fichero cifrar.\nADVERTENCIA: ¡El fichero se sobreescribirá si existe!</string> <string name="specify_file_to_decrypt_to">Por favor especifique hacia qué fichero descifrar.\nADVERTENCIA: ¡El fichero se sobreescribirá si existe!</string> <string name="specify_file_to_export_to">Por favor especifique hacia qué fichero exportar.\nADVERTENCIA: ¡El fichero se sobreescribirá si existe!</string> @@ -363,7 +363,7 @@ <string name="import_qr_code_button">Escanear código QR</string> <string name="import_qr_code_text">¡Sitúe su cámara sobre el código QR!</string> <!--Generic result toast--> - <string name="view_log">Detalles</string> + <string name="snackbar_details">Detalles</string> <string name="with_warnings">, con advertencias</string> <string name="with_cancelled">, hasta que sea cancelado</string> <!--Import result toast--> @@ -1131,7 +1131,7 @@ <string name="contact_show_key">Mostrar clave (%s)</string> <string name="swipe_to_update">Gesto de barrido hacia abajo para actualizar desde el servidor de claves</string> <string name="error_no_file_selected">¡Seleccione al menos un fichero a cifrar!</string> - <string name="error_multi_not_supported">El guardado de múltiples ficheros no está soportado. Esto es una limitación de su Android actual.</string> + <string name="error_multi_files">El guardado de múltiples ficheros no está soportado. Esto es una limitación de su Android actual.</string> <string name="key_colon">Clave:</string> <string name="exchange_description">Para iniciar un intercambio de claves, en el lado derecho elija el número de participantes, y luego pulse el botón \"Iniciar intercambio\".\n\nSe le formularán dos preguntas más para asegurar que sólo están en el intercambio los participantes debidos, y que sus huellas de validación son correctas.</string> <string name="btn_start_exchange">Comenzar intercambio</string> diff --git a/OpenKeychain/src/main/res/values-eu/strings.xml b/OpenKeychain/src/main/res/values-eu/strings.xml index 784217b95..a0b206f93 100644 --- a/OpenKeychain/src/main/res/values-eu/strings.xml +++ b/OpenKeychain/src/main/res/values-eu/strings.xml @@ -172,7 +172,7 @@ <string name="encrypt_sign_successful">Ongi sinatu eta/edo enkriptatu da.</string> <string name="encrypt_sign_clipboard_successful">Ongi sinatu eta/edo enkriptatu da gakora.</string> <string name="select_encryption_key">Hautatu enkriptaketa giltza bat gutxienez.</string> - <string name="select_encryption_or_signature_key">Hautatu enkriptaketa giltza bat edo sinadura giltza bat gutxienez.</string> + <string name="error_no_encryption_or_signature_key">Hautatu enkriptaketa giltza bat edo sinadura giltza bat gutxienez.</string> <string name="key_deletion_confirmation_multi">Egitan nahi duzu hautaturiko giltzak ezabatzea?</string> <string name="public_key_deletetion_confirmation">Ezabatu \'%s\' giltza?</string> <string name="also_export_secret_keys">Esportatu giltza sekretuak ere</string> @@ -304,7 +304,7 @@ <string name="import_qr_code_button">Eskaneatu QR Kodea</string> <string name="import_qr_code_text">Jarri zure kamera QR Kodearen gainean!</string> <!--Generic result toast--> - <string name="view_log">Xehetasunak</string> + <string name="snackbar_details">Xehetasunak</string> <string name="with_warnings">, kontuz oharrekin</string> <!--Import result toast--> <string name="import_error_nothing">Ez dago ezer inportatzeko.</string> diff --git a/OpenKeychain/src/main/res/values-fr/strings.xml b/OpenKeychain/src/main/res/values-fr/strings.xml index 17e3fcde0..7f408f603 100644 --- a/OpenKeychain/src/main/res/values-fr/strings.xml +++ b/OpenKeychain/src/main/res/values-fr/strings.xml @@ -201,7 +201,7 @@ <string name="encrypt_sign_successful">Signé et/ou chiffré avec succès.</string> <string name="encrypt_sign_clipboard_successful">Signé et/ou chiffré vers le presse-papiers avec succès.</string> <string name="select_encryption_key">Choisir au moins une clef de chiffrement.</string> - <string name="select_encryption_or_signature_key">Choisir au moins une clef de chiffrement ou de signature.</string> + <string name="error_no_encryption_or_signature_key">Choisir au moins une clef de chiffrement ou de signature.</string> <string name="specify_file_to_encrypt_to">Veuillez spécifier vers quel fichier chiffrer.\nAVERTISSEMENT : le fichier sera écrasé s\'il existe !</string> <string name="specify_file_to_decrypt_to">Veuillez spécifier vers quel fichier déchiffrer.\nAVERTISSEMENT : le fichier sera écrasé s\'il existe !</string> <string name="specify_file_to_export_to">Veuillez spécifier vers quel fichier exporter.\nAVERTISSEMENT : le fichier sera écrasé s\'il existe !</string> @@ -363,7 +363,7 @@ <string name="import_qr_code_button">Balayer le code QR</string> <string name="import_qr_code_text">Placez votre appareil photo au-dessus du code QR !</string> <!--Generic result toast--> - <string name="view_log">Détails</string> + <string name="snackbar_details">Détails</string> <string name="with_warnings">, avec des avertissements</string> <string name="with_cancelled">, jusqu\'à l\'annulation</string> <!--Import result toast--> @@ -1131,7 +1131,7 @@ <string name="contact_show_key">Montrer la clef (%s)</string> <string name="swipe_to_update">Glisser vers le bas pour mettre à jour à partir du serveur de clefs</string> <string name="error_no_file_selected">Choisir au moins un fichier à chiffrer !</string> - <string name="error_multi_not_supported">L\'enregistrement des fichiers multiples n\'est pas pris en charge. Ceci est actuellement une limitation d\'Android.</string> + <string name="error_multi_files">L\'enregistrement des fichiers multiples n\'est pas pris en charge. Ceci est actuellement une limitation d\'Android.</string> <string name="key_colon">Clef :</string> <string name="exchange_description">Pour démarrer un échange de clef, choisir le nombre de participants du côté droit, puis cliquer sur le bouton « Démarrer l\'échange ».\n\Deux questions de plus seront posées pour s\'assurer que seuls les bons participants sont dans l\'échange et que les empreintes sont correctes.</string> <string name="btn_start_exchange">Démarrer l\'échange</string> diff --git a/OpenKeychain/src/main/res/values-it/strings.xml b/OpenKeychain/src/main/res/values-it/strings.xml index 2e598a0c2..6ee4fad47 100644 --- a/OpenKeychain/src/main/res/values-it/strings.xml +++ b/OpenKeychain/src/main/res/values-it/strings.xml @@ -149,7 +149,7 @@ <string name="encrypt_sign_successful">Firmato e/o codificato con successo.</string> <string name="encrypt_sign_clipboard_successful">Firmato e/o codificato con successo negli appunti.</string> <string name="select_encryption_key">Seleziona almeno una chiave di codifica.</string> - <string name="select_encryption_or_signature_key">Seleziona almeno una chiave di codifica o di firma.</string> + <string name="error_no_encryption_or_signature_key">Seleziona almeno una chiave di codifica o di firma.</string> <string name="also_export_secret_keys">Esporta anche chiave segreta</string> <string name="reinstall_openkeychain">Hai riscontrato un bug noto con Android. Si prega di reinstallare OpenKeychain se vuoi collegare i tuoi contatti con le chiavi.</string> <string name="key_exported">1 chiave esportata correttamente.</string> @@ -686,7 +686,7 @@ Permetti accesso?\n\nATTENZIONE: Se non sai perche\' questo schermata e\' appars <string name="info_no_manual_account_creation">Non creare account OpenKeychain manualmente.\nPer ulteriori informazioni, vedere la Guida.</string> <string name="contact_show_key">Mostra chiave (%s)</string> <string name="error_no_file_selected">Seleziona almeno un file da codificare!</string> - <string name="error_multi_not_supported">Il salvataggio di più file non è supportato. Questa è una limitazione corrente di Android.</string> + <string name="error_multi_files">Il salvataggio di più file non è supportato. Questa è una limitazione corrente di Android.</string> <string name="key_colon">Chiave:</string> <!--Passphrase wizard--> <!--TODO: rename all the things!--> diff --git a/OpenKeychain/src/main/res/values-ja/strings.xml b/OpenKeychain/src/main/res/values-ja/strings.xml index 82ca70d67..790913684 100644 --- a/OpenKeychain/src/main/res/values-ja/strings.xml +++ b/OpenKeychain/src/main/res/values-ja/strings.xml @@ -199,7 +199,7 @@ <string name="encrypt_sign_successful">署名/暗号化に成功しました。</string> <string name="encrypt_sign_clipboard_successful">クリップボードの中身の署名/暗号化に成功しました。</string> <string name="select_encryption_key">少なくとも1つの暗号化鍵を選択して下さい。</string> - <string name="select_encryption_or_signature_key">少なくとも1つの暗号化鍵か署名鍵を選択して下さい。</string> + <string name="error_no_encryption_or_signature_key">少なくとも1つの暗号化鍵か署名鍵を選択して下さい。</string> <string name="specify_file_to_encrypt_to">どれのファイルを暗号化するのを入力してください。\n注意:ファイルが存在しているなら上書きされる!</string> <string name="specify_file_to_decrypt_to">どれのファイルを暗号するのを入力してください。\n注意:ファイルが存在しているなら上書きされる!</string> <string name="specify_file_to_export_to">どれのファイルを復号化するのを入力してください。\n注意:ファイルが存在しているなら上書きされる!</string> @@ -364,7 +364,7 @@ <string name="import_qr_code_button">QCコードのスキャン</string> <string name="import_qr_code_text">カメラをQRコードにかざしてください!</string> <!--Generic result toast--> - <string name="view_log">概要</string> + <string name="snackbar_details">概要</string> <string name="with_warnings">、とワーニング</string> <string name="with_cancelled">、キャンセルされるまで</string> <!--Import result toast--> @@ -1102,7 +1102,7 @@ <string name="contact_show_key">鍵 (%s) を表示</string> <string name="swipe_to_update">下スワイプでキーサーバから更新します</string> <string name="error_no_file_selected">暗号化するファイルを少なくとも1つ選択して下さい。</string> - <string name="error_multi_not_supported">複数ファイルの保存はサポートされていません。これは現在のAndroidでの制限です。</string> + <string name="error_multi_files">複数ファイルの保存はサポートされていません。これは現在のAndroidでの制限です。</string> <string name="key_colon">鍵:</string> <string name="exchange_description">鍵交換の開始は、右側の参加者の番号を選択し、その後、\"交換開始\"ボタンを推します。\n\n2つ以上の質問で交換にいる右の参加者とその指紋が正しいかを確認してください。</string> <string name="btn_start_exchange">交換開始</string> diff --git a/OpenKeychain/src/main/res/values-nl/strings.xml b/OpenKeychain/src/main/res/values-nl/strings.xml index f5d079dab..e69208dbb 100644 --- a/OpenKeychain/src/main/res/values-nl/strings.xml +++ b/OpenKeychain/src/main/res/values-nl/strings.xml @@ -201,7 +201,7 @@ <string name="encrypt_sign_successful">Succesvol gesigneerd en/of gecodeerd.</string> <string name="encrypt_sign_clipboard_successful">Succesvol gesigneerd en/of gecodeerd naar klembord.</string> <string name="select_encryption_key">Selecteer ten minste één versleutelingssleutel.</string> - <string name="select_encryption_or_signature_key">Selecter ten minste één versleutelings-/ondertekeningssleutel.</string> + <string name="error_no_encryption_or_signature_key">Selecter ten minste één versleutelings-/ondertekeningssleutel.</string> <string name="specify_file_to_encrypt_to">Gelieve aan te geven naar welk bestand versleuteld moet worden.\nWAARSCHUWING: Als het bestand al bestaat, zal het overschreven worden!</string> <string name="specify_file_to_decrypt_to">Gelieve aan te geven naar welk bestand ontcijferd moet worden.\nWAARSCHUWING: Als het bestand al bestaat, zal het overschreven worden!</string> <string name="specify_file_to_export_to">Gelieve aan te geven naar welk bestand geëxporteerd moet worden.\nWAARSCHUWING: Als het bestand al bestaat, zal het overschreven worden!</string> @@ -363,7 +363,7 @@ <string name="import_qr_code_button">QR code scannen</string> <string name="import_qr_code_text">Plaats je camera voor de QR-code!</string> <!--Generic result toast--> - <string name="view_log">Details</string> + <string name="snackbar_details">Details</string> <string name="with_warnings">, met waarschuwingen</string> <string name="with_cancelled">, tot annulatie</string> <!--Import result toast--> @@ -1131,7 +1131,7 @@ <string name="contact_show_key">Toon sleutel (%s)</string> <string name="swipe_to_update">Veeg naar beneden om van sleutelserver te updaten</string> <string name="error_no_file_selected">Selecteer minstens een bestand om te versleutelen!</string> - <string name="error_multi_not_supported">Opslaan van meerdere bestanden wordt niet ondersteund. Dit is een beperking van Android.</string> + <string name="error_multi_files">Opslaan van meerdere bestanden wordt niet ondersteund. Dit is een beperking van Android.</string> <string name="key_colon">Sleutel:</string> <string name="exchange_description">Selecteer om een sleuteluitwisseling te starten het aantal deelnemers aan de rechterkant, en klik vervolgens op de knop \'Start uitwisseling\'.\n\nJe zal twee vragne gesteld worden om zeker te zijn dat enkel de juiste deelnemers zich in de uitwisseling bevinden en dat hun vingerafdrukken correct zijn.</string> <string name="btn_start_exchange">Uitwisseling starten</string> diff --git a/OpenKeychain/src/main/res/values-pl/strings.xml b/OpenKeychain/src/main/res/values-pl/strings.xml index c7b461edf..caa9092e5 100644 --- a/OpenKeychain/src/main/res/values-pl/strings.xml +++ b/OpenKeychain/src/main/res/values-pl/strings.xml @@ -163,7 +163,7 @@ <string name="encrypt_sign_successful">Pomyślnie podpisano i/lub zaszyfrowano.</string> <string name="encrypt_sign_clipboard_successful">Pomyslnie podpisano i/lub zaszyfrowano do schowka.</string> <string name="select_encryption_key">Wybierz co najmniej jeden klucz szyfrujący.</string> - <string name="select_encryption_or_signature_key">Wybierz co najmniej jeden klucz szyfrujący lub klucz podpisujący.</string> + <string name="error_no_encryption_or_signature_key">Wybierz co najmniej jeden klucz szyfrujący lub klucz podpisujący.</string> <string name="also_export_secret_keys">Także eksportuj tajne klucze</string> <string name="reinstall_openkeychain">Napotkałeś się na znany błąd w Androidzie. Proszę ponownie zainstalować OpenKeychain jeśli chcesz połączyć kontakty z kluczami.</string> <string name="key_exported">Pomyślnie wyeksportowano 1 klucz.</string> @@ -308,7 +308,7 @@ <string name="import_qr_code_button">Skanuj kod QR</string> <string name="import_qr_code_text">Umieść kod QR przed kamerą!</string> <!--Generic result toast--> - <string name="view_log">Szczegóły</string> + <string name="snackbar_details">Szczegóły</string> <string name="with_warnings">, z ostrzeżeniami</string> <string name="with_cancelled">, aż anulowano</string> <!--Import result toast--> diff --git a/OpenKeychain/src/main/res/values-ru/strings.xml b/OpenKeychain/src/main/res/values-ru/strings.xml index f4b60fcc1..e5e164c49 100644 --- a/OpenKeychain/src/main/res/values-ru/strings.xml +++ b/OpenKeychain/src/main/res/values-ru/strings.xml @@ -184,7 +184,7 @@ <string name="encrypt_sign_successful">Успешно подписано и/или зашифровано.</string> <string name="encrypt_sign_clipboard_successful">Успешно подписано и/или зашифровано в буфер обмена.</string> <string name="select_encryption_key">Укажите хотя бы один ключ.</string> - <string name="select_encryption_or_signature_key">Выберите хотя бы один ключ для шифрования или подписи.</string> + <string name="error_no_encryption_or_signature_key">Выберите хотя бы один ключ для шифрования или подписи.</string> <string name="also_export_secret_keys">Экспортировать секретные ключи</string> <string name="reinstall_openkeychain">Вы столкнулись с багом Андроид. Пожалуйста, переустановите OpenKeychain чтобы связать ваши контакты и ключи. </string> <string name="key_exported">Успешный экспорт 1 ключа.</string> @@ -332,7 +332,7 @@ <string name="import_qr_code_button">Сканировать QR код...</string> <string name="import_qr_code_text">Расположите вашу камеру над QR кодом!</string> <!--Generic result toast--> - <string name="view_log">Сведения</string> + <string name="snackbar_details">Сведения</string> <string name="with_warnings">, с предупреждениями</string> <string name="with_cancelled">, до отмены</string> <!--Import result toast--> diff --git a/OpenKeychain/src/main/res/values-sl/strings.xml b/OpenKeychain/src/main/res/values-sl/strings.xml index 02b268d6b..21c528820 100644 --- a/OpenKeychain/src/main/res/values-sl/strings.xml +++ b/OpenKeychain/src/main/res/values-sl/strings.xml @@ -205,7 +205,7 @@ <string name="encrypt_sign_successful">Uspešno podpisano in/ali šifrirano.</string> <string name="encrypt_sign_clipboard_successful">Uspešno podpisano in/ali šifrirano ter poslano v odložišče.</string> <string name="select_encryption_key">Izberite vsaj en šifrirni ključ.</string> - <string name="select_encryption_or_signature_key">Vnesite vsaj en šifrirni ključ ali ključ za podpisovanje.</string> + <string name="error_no_encryption_or_signature_key">Vnesite vsaj en šifrirni ključ ali ključ za podpisovanje.</string> <string name="specify_file_to_encrypt_to">Določite datoteko, v katero želite šifrirati vsebino.\nPOZOR: če datoteka že obstaja, bo prepisana.</string> <string name="specify_file_to_decrypt_to">Določite datoteko, v katero želite dešifrirati vsebino.\nPOZOR: če datoteka že obstaja, bo prepisana.</string> <string name="specify_file_to_export_to">Določite datoteko, v katero želite izvoziti vsebino.\nPOZOR: če datoteka že obstaja, bo prepisana.</string> @@ -371,7 +371,7 @@ <string name="import_qr_code_button">Skeniraj kodo QR</string> <string name="import_qr_code_text">Zajamite kodo QR s kamero!</string> <!--Generic result toast--> - <string name="view_log">Podrobnosti</string> + <string name="snackbar_details">Podrobnosti</string> <string name="with_warnings">, z opozorili</string> <string name="with_cancelled">, do preklica</string> <!--Import result toast--> diff --git a/OpenKeychain/src/main/res/values-sr/strings.xml b/OpenKeychain/src/main/res/values-sr/strings.xml index 75bbdf480..ebacebb5f 100644 --- a/OpenKeychain/src/main/res/values-sr/strings.xml +++ b/OpenKeychain/src/main/res/values-sr/strings.xml @@ -209,7 +209,7 @@ <string name="encrypt_sign_clipboard_successful">Потписивање и/или шифровање на клипборд је успело.</string> <string name="enter_passphrase_twice">Унесите лозинку два пута.</string> <string name="select_encryption_key">Изаберите бар један кључ за шифровање.</string> - <string name="select_encryption_or_signature_key">Изаберите бар један кључ за шифровање или потписивање.</string> + <string name="error_no_encryption_or_signature_key">Изаберите бар један кључ за шифровање или потписивање.</string> <string name="specify_file_to_encrypt_to">Одредите у који фајл да шифрујем.\nУПОЗОРЕЊЕ: Фајл ће бити пребрисан ако постоји.</string> <string name="specify_file_to_decrypt_to">Одредите у који фајл да дешифрујем.\nУПОЗОРЕЊЕ: Фајл ће бити пребрисан ако постоји.</string> <string name="specify_file_to_export_to">Одредите у који фајл да извезем.\nУПОЗОРЕЊЕ: Фајл ће бити пребрисан ако постоји.</string> @@ -370,7 +370,7 @@ <string name="import_qr_code_button">Очитај бар-код</string> <string name="import_qr_code_text">Усмерите камеру на бар-код!</string> <!--Generic result toast--> - <string name="view_log">Детаљи</string> + <string name="snackbar_details">Детаљи</string> <string name="with_warnings">, са упозорењима</string> <string name="with_cancelled">, док није отказано</string> <!--Import result toast--> @@ -1112,7 +1112,7 @@ <string name="contact_show_key">Прикажи кључ (%s)</string> <string name="swipe_to_update">Превуците прстом доле да ажурирате са сервера кључева</string> <string name="error_no_file_selected">Изаберите бар један фајл за шифровање!</string> - <string name="error_multi_not_supported">Упис више фајлова није подржан. Ово је ограничење у текућем издању Андроида.</string> + <string name="error_multi_files">Упис више фајлова није подржан. Ово је ограничење у текућем издању Андроида.</string> <string name="key_colon">Кључ:</string> <string name="exchange_description">Да бисте почели размену кључева, са десне стране изаберите број учесника и додирните дугме „Почни размену“.\n\nБиће вам постављено још два питања да би се осигурало да су само исправни учесници у размени и да су њихови отисци тачни.</string> <string name="btn_start_exchange">Почни размену</string> diff --git a/OpenKeychain/src/main/res/values-sv/strings.xml b/OpenKeychain/src/main/res/values-sv/strings.xml index f73be4861..94f4fbe31 100644 --- a/OpenKeychain/src/main/res/values-sv/strings.xml +++ b/OpenKeychain/src/main/res/values-sv/strings.xml @@ -192,7 +192,7 @@ <string name="encrypt_sign_successful">Signerades och/eller krypterades.</string> <string name="encrypt_sign_clipboard_successful">Signerades och/eller krypterades till urklipp.</string> <string name="select_encryption_key">Välj åtminstone en krypteringsnyckel.</string> - <string name="select_encryption_or_signature_key">Välj åtminstone en krypterings- eller signeringsnyckel.</string> + <string name="error_no_encryption_or_signature_key">Välj åtminstone en krypterings- eller signeringsnyckel.</string> <string name="key_deletion_confirmation_multi">Vill du verkligen radera alla markerade nycklar?</string> <string name="secret_key_deletion_confirmation">Efter radering kommer du inte kunna läsa meddelande krypterade med den här nyckeln samt förlora alla nyckelbekräftningar som gjorts med den!</string> <string name="public_key_deletetion_confirmation">Radera nyckel \'%s\'?</string> @@ -350,7 +350,7 @@ <string name="import_qr_code_button">Skanna QR-kod</string> <string name="import_qr_code_text">Håll din kamera över QR-koden!</string> <!--Generic result toast--> - <string name="view_log">Detaljer</string> + <string name="snackbar_details">Detaljer</string> <string name="with_warnings">, med varningar</string> <string name="with_cancelled">, tills det avbryts</string> <!--Import result toast--> @@ -873,7 +873,7 @@ <string name="contact_show_key">Visa nyckel (%s)</string> <string name="swipe_to_update">Dra nedåt för att uppdatera från nyckelserver</string> <string name="error_no_file_selected">Välj åtminstone en fil att kryptera!</string> - <string name="error_multi_not_supported">Att spara flera filer stöds ej. Detta är en begränsning i nuvarande Android.</string> + <string name="error_multi_files">Att spara flera filer stöds ej. Detta är en begränsning i nuvarande Android.</string> <string name="key_colon">Nyckel:</string> <string name="user_id_none"><![CDATA[<none>]]></string> <!--Passphrase wizard--> diff --git a/OpenKeychain/src/main/res/values-tr/strings.xml b/OpenKeychain/src/main/res/values-tr/strings.xml index 12115e93b..09d39dcc9 100644 --- a/OpenKeychain/src/main/res/values-tr/strings.xml +++ b/OpenKeychain/src/main/res/values-tr/strings.xml @@ -152,7 +152,7 @@ <string name="encrypt_sign_successful">Başarıyla imzalandı ve/veya şifrelendi.</string> <string name="encrypt_sign_clipboard_successful">Kopyalama önbelleğine başarıyla imzalandı ve/veya şifrelendi.</string> <string name="select_encryption_key">En az bir şifreleme anahtarı seçiniz.</string> - <string name="select_encryption_or_signature_key">En az bir şifreleme anahtarı veya imza anahtarı seçiniz.</string> + <string name="error_no_encryption_or_signature_key">En az bir şifreleme anahtarı veya imza anahtarı seçiniz.</string> <string name="also_export_secret_keys">Özel anahtarları da dışa aktar</string> <string name="reinstall_openkeychain">Android için bilinen bir hataya denk geldiniz. Eğer kişilerinizi anahtarlarla eşlemek istiyorsanız, lütfen OpenKeychain uygulamasını yeniden yükleyin.</string> <string name="key_exported">1 anahtar başarıyla dışa aktarıldı.</string> @@ -480,7 +480,7 @@ <string name="contact_show_key">Anahtarı göster (%s)</string> <string name="swipe_to_update">Anahtar sunucudan güncelleme almak için parmağınızı aşağıya doğru kaydırın</string> <string name="error_no_file_selected">Şifrelemek için en az bir dosya seçin!</string> - <string name="error_multi_not_supported">Birden çok dosyanın kaydedilmesi desteklenmiyor. Bu şu anki Android\'in bir kısıtlamasıdır.</string> + <string name="error_multi_files">Birden çok dosyanın kaydedilmesi desteklenmiyor. Bu şu anki Android\'in bir kısıtlamasıdır.</string> <string name="key_colon">Anahtar:</string> <string name="exchange_description">Anahtar değiş tokuşu başlatmak için sağ taraftan katılımcıların sayısını seçin ve \"Değiş tokuşu başlat\" tuşuna tıklayın.\n\nSadece istenilen katılımcıların değişim işleminde olduğundan ve parmak izlerinin doğruluğundan emin olmak için size iki soru daha sorulacak.</string> <!--Passphrase wizard--> diff --git a/OpenKeychain/src/main/res/values-uk/strings.xml b/OpenKeychain/src/main/res/values-uk/strings.xml index 58272c597..626ea6c80 100644 --- a/OpenKeychain/src/main/res/values-uk/strings.xml +++ b/OpenKeychain/src/main/res/values-uk/strings.xml @@ -153,7 +153,7 @@ <string name="encrypt_sign_successful">Успішно підписано та/або перевірено.</string> <string name="encrypt_sign_clipboard_successful">Успішно підписано та/або зашифровано до буфера обміну.</string> <string name="select_encryption_key">Виберіть принаймні один ключ шифрування.</string> - <string name="select_encryption_or_signature_key">Виберіть принаймні один ключ шифрування або ключ підпису.</string> + <string name="error_no_encryption_or_signature_key">Виберіть принаймні один ключ шифрування або ключ підпису.</string> <string name="also_export_secret_keys">Також експортувати секретні ключі</string> <string name="key_exported">Успішно експортовано 1 ключ.</string> <string name="keys_exported">Успішно експортовано %d ключів.</string> diff --git a/OpenKeychain/src/main/res/values-zh-rTW/strings.xml b/OpenKeychain/src/main/res/values-zh-rTW/strings.xml index 1d57ef430..53cc26aeb 100644 --- a/OpenKeychain/src/main/res/values-zh-rTW/strings.xml +++ b/OpenKeychain/src/main/res/values-zh-rTW/strings.xml @@ -190,7 +190,7 @@ <string name="encrypt_sign_successful">成功簽名並/或加密。</string> <string name="encrypt_sign_clipboard_successful">成功簽名並/或加密到剪貼簿。</string> <string name="select_encryption_key">選擇至少一把加密金鑰。</string> - <string name="select_encryption_or_signature_key">選擇至少一把加密或簽名金鑰。</string> + <string name="error_no_encryption_or_signature_key">選擇至少一把加密或簽名金鑰。</string> <string name="specify_file_to_encrypt_to">請指定欲加密的檔案。\n警告:已經存在的檔案將被覆蓋。</string> <string name="specify_file_to_decrypt_to">請指定欲解密的檔案。\n警告:已經存在的檔案將被覆蓋。</string> <string name="specify_file_to_export_to">請指定欲輸出的檔案。\n警告:已經存在的檔案將被覆蓋。</string> @@ -348,7 +348,7 @@ <string name="import_qr_code_button">掃描二維條碼</string> <string name="import_qr_code_text">將您的相機對準 QR Code !</string> <!--Generic result toast--> - <string name="view_log">詳細</string> + <string name="snackbar_details">詳細</string> <string name="with_warnings">,包含警告</string> <string name="with_cancelled">,直到被取消</string> <!--Import result toast--> diff --git a/OpenKeychain/src/main/res/values-zh/strings.xml b/OpenKeychain/src/main/res/values-zh/strings.xml index f43cb3ebd..4488a6020 100644 --- a/OpenKeychain/src/main/res/values-zh/strings.xml +++ b/OpenKeychain/src/main/res/values-zh/strings.xml @@ -115,7 +115,7 @@ <string name="encrypt_sign_successful">加密并签名成功</string> <string name="encrypt_sign_clipboard_successful">加密签名并复制到剪贴板成功</string> <string name="select_encryption_key">选择至少一个加密密钥</string> - <string name="select_encryption_or_signature_key">选择至少一个加密密钥或者签名密钥</string> + <string name="error_no_encryption_or_signature_key">选择至少一个加密密钥或者签名密钥</string> <string name="also_export_secret_keys">同时导出密钥</string> <string name="key_exported">成功地导出了1个密钥</string> <string name="keys_exported">成功导出多个密钥</string> diff --git a/OpenKeychain/src/main/res/values/strings.xml b/OpenKeychain/src/main/res/values/strings.xml index 65ef1e687..e05186c61 100644 --- a/OpenKeychain/src/main/res/values/strings.xml +++ b/OpenKeychain/src/main/res/values/strings.xml @@ -55,7 +55,7 @@ <string name="section_key_server">"Keyserver"</string> <string name="section_fingerprint">"Fingerprint"</string> <string name="section_encrypt">"Encrypt"</string> - <string name="section_decrypt">"Decrypt"</string> + <string name="section_decrypt">"Decrypt / Verify"</string> <string name="section_current_expiry">"Current expiry"</string> <string name="section_new_expiry">"New expiry"</string> @@ -63,7 +63,8 @@ <string name="btn_decrypt_verify_file">"Decrypt, verify, and save file"</string> <string name="btn_encrypt_share_file">"Encrypt and share file"</string> <string name="btn_encrypt_save_file">"Encrypt and save file"</string> - <string name="btn_save">"Save"</string> + <string name="btn_save">"Save file"</string> + <string name="btn_view_log">"View log"</string> <string name="btn_do_not_save">"Cancel"</string> <string name="btn_delete">"Delete"</string> <string name="btn_no_date">"No Expiry"</string> @@ -80,9 +81,8 @@ <string name="btn_add_files">"Add file(s)"</string> <string name="btn_share_decrypted_text">"Share decrypted text"</string> <string name="btn_copy_decrypted_text">"Copy decrypted text"</string> - <string name="btn_decrypt_clipboard">"Decrypt text from clipboard"</string> - <string name="btn_decrypt_and_verify">"and verify signatures"</string> - <string name="btn_decrypt_files">"Decrypt files"</string> + <string name="btn_decrypt_clipboard">"Read from clipboard"</string> + <string name="btn_decrypt_files">"Select input file"</string> <string name="btn_encrypt_files">"Encrypt files"</string> <string name="btn_encrypt_text">"Encrypt text"</string> <string name="btn_add_email">"Add additional email address"</string> @@ -222,11 +222,11 @@ <string name="file_delete_confirmation_title">"Delete original files?"</string> <string name="file_delete_confirmation">"The following files will be deleted:%s"</string> <string name="file_delete_successful">"%1$d out of %2$d files have been deleted.%3$s"</string> - <string name="no_file_selected">"Select a file first."</string> + <string name="no_file_selected">"No file selected."</string> <string name="encrypt_sign_successful">"Successfully signed and/or encrypted."</string> <string name="encrypt_sign_clipboard_successful">"Successfully signed and/or encrypted to clipboard."</string> <string name="select_encryption_key">"Select at least one encryption key."</string> - <string name="select_encryption_or_signature_key">"Select at least one encryption key or a signature key."</string> + <string name="error_no_encryption_or_signature_key">"Select at least one encryption key or a signature key."</string> <string name="specify_file_to_encrypt_to">"Please specify which file to encrypt to.\nWARNING: File will be overwritten if it exists!"</string> <string name="specify_file_to_decrypt_to">"Please specify which file to decrypt to.\nWARNING: File will be overwritten if it exists!"</string> <string name="specify_file_to_export_to">"Please specify which file to export to.\nWARNING: File will be overwritten if it exists!"</string> @@ -421,7 +421,7 @@ <string name="import_url_warn_no_search_parameter">"No search query defined. You can still manually search on this keyserver."</string> <!-- Generic result toast --> - <string name="view_log">"Details"</string> + <string name="snackbar_details">"Details"</string> <string name="with_warnings">", with warnings"</string> <string name="with_cancelled">", until cancelled"</string> @@ -1267,7 +1267,9 @@ <string name="contact_show_key">"Show key (%s)"</string> <string name="swipe_to_update">"Swipe down to update from keyserver"</string> <string name="error_no_file_selected">"Select at least one file to encrypt!"</string> - <string name="error_multi_not_supported">"Saving of multiple files not supported. This is a limitation on current Android."</string> + <string name="error_multi_files">"Saving of multiple files not supported. This is a limitation on current Android."</string> + <string name="error_multi_clipboard">"Encryption of multiple files to clipboard not supported."</string> + <string name="error_detached_signature">"Sign-only operation of binary files is not supported, select at least one encryption key."</string> <string name="error_empty_text">"Type some text to encrypt!"</string> <string name="key_colon">"Key:"</string> <string name="exchange_description">"To start a key exchange, choose the number of participants on the right side, then hit the “Start exchange” button.\n\nYou will be asked two more questions to make sure only the right participants are in the exchange and their fingerprints are correct."</string> @@ -1326,13 +1328,28 @@ <string name="error_nfc_header">"NFC: Card reported invalid %s byte"</string> <string name="error_pin_nodefault">Default PIN was rejected!</string> <string name="error_bluetooth_file">Error creating temporary file. Bluetooth sharing will fail.</string> + <string name="btn_delete_original">Delete original file</string> + <string name="snack_encrypt_filenames_on">"Filenames <b>are</b> encrypted."</string> <string name="snack_encrypt_filenames_off">"Filenames <b>are not</b> encrypted."</string> <string name="snack_armor_on">"Output encoded as Text."</string> <string name="snack_armor_off">"Output encoded as Binary."</string> <string name="snack_compression_on">"Compression <b>enabled</b>."</string> <string name="snack_compression_off">"Compression <b>disabled</b>."</string> - <string name="error_loading_keys">Error loading keys!</string> - <string name="error_empty_log">(error, empty log)</string> + <string name="error_loading_keys">"Error loading keys!"</string> + <string name="error_empty_log">"(error, empty log)"</string> + <string name="error_reading_text">"Could not read input to decrypt!"</string> + <string name="filename_unknown"><![CDATA[<no filename>]]></string> + <string name="filename_unknown_text"><![CDATA[<plain text data>]]></string> + <string name="intent_show">Show Signed/Encrypted Content</string> + <string name="view_internal">"View in OpenKeychain"</string> + <string name="error_preparing_data">"Error preparing data!"</string> + <string name="label_clip_title">"Encrypted Data"</string> + <string name="progress_processing">"Processing…"</string> + <string name="error_saving_file">"Error saving file!"</string> + <string name="file_saved">"File saved!"</string> + <string name="file_delete_ok">"Original file deleted."</string> + <string name="file_delete_none">"No file deleted! (Already deleted?)"</string> + <string name="file_delete_exception">"Original file could not be deleted!"</string> </resources> diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/pgp/PgpEncryptDecryptTest.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/pgp/PgpEncryptDecryptTest.java index ce7414a3d..9c3636d07 100644 --- a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/pgp/PgpEncryptDecryptTest.java +++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/pgp/PgpEncryptDecryptTest.java @@ -171,6 +171,10 @@ public class PgpEncryptDecryptTest { out.toByteArray(), plaintext.getBytes()); Assert.assertNull("signature should be an error", result.getSignatureResult()); + CryptoInputParcel cryptoInput = result.getCachedCryptoInputParcel(); + Assert.assertEquals("cached session keys must be empty", + 0, cryptoInput.getCryptoData().size()); + OpenPgpMetadata metadata = result.getDecryptMetadata(); Assert.assertEquals("filesize must be correct", out.toByteArray().length, metadata.getOriginalSize()); @@ -272,6 +276,10 @@ public class PgpEncryptDecryptTest { out.toByteArray(), plaintext.getBytes()); Assert.assertNull("signature be empty", result.getSignatureResult()); + CryptoInputParcel cryptoInput = result.getCachedCryptoInputParcel(); + Assert.assertEquals("must have one cached session key", + 1, cryptoInput.getCryptoData().size()); + OpenPgpMetadata metadata = result.getDecryptMetadata(); Assert.assertEquals("filesize must be correct", out.toByteArray().length, metadata.getOriginalSize()); @@ -289,6 +297,10 @@ public class PgpEncryptDecryptTest { PgpDecryptVerifyInputParcel input = new PgpDecryptVerifyInputParcel(); DecryptVerifyResult result = op.execute(input, new CryptoInputParcel(), data, out); + CryptoInputParcel cryptoInput = result.getCachedCryptoInputParcel(); + Assert.assertEquals("must have one cached session key", + 1, cryptoInput.getCryptoData().size()); + Assert.assertTrue("decryption with cached passphrase must succeed", result.success()); Assert.assertArrayEquals("decrypted ciphertext with cached passphrase should equal plaintext", out.toByteArray(), plaintext.getBytes()); |