Merge pull request #1352 from open-keychain/v/instrument

instrumentation branch
This commit is contained in:
Dominik Schürmann 2015-06-17 19:56:13 +02:00
commit 300fd8e0f2
44 changed files with 2568 additions and 401 deletions

View File

@ -1,18 +1,36 @@
language: java language: android
jdk: openjdk7 jdk: openjdk7
before_install: # container based build, we don't need root anyways
# Install base Android SDK sudo: false
- sudo apt-get update -qq # env:
- if [ `uname -m` = x86_64 ]; then sudo apt-get install -qq --force-yes libgd2-xpm lib32z1 lib32stdc++6; fi # global:
- wget http://dl.google.com/android/android-sdk_r24.1.2-linux.tgz # - ANDROID_API_LEVEL=21
- tar xzf android-sdk_r24.1.2-linux.tgz # - ANDROID_ABI=armeabi-v7a
- export ANDROID_HOME=$PWD/android-sdk-linux # - ADB_INSTALL_TIMEOUT=8 # minutes (2 minutes by default)
- export PATH=${PATH}:${ANDROID_HOME}/tools:${ANDROID_HOME}/platform-tools android:
components:
- build-tools-22.0.1
- build-tools-21.1.2
- build-tools-21.1.1
- build-tools-19.1.0
- android-22
- android-21
- android-19
- platform-tools
- extra-android-support
- extra-android-m2repository
licenses:
- 'android-sdk-preview-license-52d11cd2'
- 'android-sdk-license-.+'
- 'google-gdk-license-.+'
# doesn't work, travis is just too slow
# before_script:
# - echo no | android create avd --force -n test -t android-$ANDROID_API_LEVEL --abi $ANDROID_ABI
# - emulator -avd test -no-skin -no-audio -no-window &
# - ./tools/android-wait-for-emulator
# - adb shell input keyevent 82 &
# Install required Android components.
#- echo "y" | android update sdk -a --filter build-tools-19.1.0,android-19,platform-tools,extra-android-support,extra-android-m2repository --no-ui --force
- ( sleep 5 && while [ 1 ]; do sleep 1; echo y; done ) | android update sdk --no-ui --all --force --filter build-tools-22.0.1,build-tools-21.1.2,build-tools-21.1.1,build-tools-19.1.0,android-22,android-21,android-19,platform-tools,extra-android-support,extra-android-m2repository
install: echo "Installation done"
script: script:
- ./gradlew testDebug jacocoTestReport coveralls --info # - ./gradlew connectedAndroidTest
- ./gradlew testDebug jacocoTestReport coveralls

View File

@ -21,10 +21,14 @@ dependencies {
testCompile 'org.robolectric:robolectric:3.0-rc3' testCompile 'org.robolectric:robolectric:3.0-rc3'
// UI testing with Espresso // UI testing with Espresso
androidTestCompile 'com.android.support.test:runner:0.2' androidTestCompile 'com.android.support.test:runner:0.3'
androidTestCompile 'com.android.support.test:rules:0.2' androidTestCompile 'com.android.support.test:rules:0.3'
androidTestCompile 'com.android.support.test.espresso:espresso-core:2.1' androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2'
androidTestCompile 'com.android.support.test.espresso:espresso-contrib:2.1' 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'
exclude module: 'recyclerview-v7'
}
// Temporary workaround for bug: https://code.google.com/p/android-test-kit/issues/detail?id=136 // Temporary workaround for bug: https://code.google.com/p/android-test-kit/issues/detail?id=136
// from https://github.com/googlesamples/android-testing/blob/master/build.gradle#L21 // from https://github.com/googlesamples/android-testing/blob/master/build.gradle#L21
@ -110,7 +114,13 @@ android {
versionCode 32300 versionCode 32300
versionName "3.2.3" versionName "3.2.3"
applicationId "org.sufficientlysecure.keychain" applicationId "org.sufficientlysecure.keychain"
// the androidjunitrunner is broken regarding coverage, see here:
// https://code.google.com/p/android/issues/detail?id=170607
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
// this workaround runner fixes the coverage problem, BUT doesn't work
// with android studio single test execution. use it to generate coverage
// data, but keep the other one otherwis
// testInstrumentationRunner "org.sufficientlysecure.keychain.JacocoWorkaroundJUnitRunner"
} }
compileOptions { compileOptions {
@ -139,9 +149,8 @@ android {
// Reference them in .xml files. // Reference them in .xml files.
resValue "string", "account_type", "org.sufficientlysecure.keychain.debug.account" resValue "string", "account_type", "org.sufficientlysecure.keychain.debug.account"
// Disabled: only works for androidTest not test!
// Enable code coverage (Jacoco) // Enable code coverage (Jacoco)
//testCoverageEnabled true testCoverageEnabled true
} }
} }
@ -194,6 +203,8 @@ android {
} }
} }
// apply plugin: 'spoon'
task jacocoTestReport(type:JacocoReport) { task jacocoTestReport(type:JacocoReport) {
group = "Reporting" group = "Reporting"
description = "Generate Jacoco coverage reports" description = "Generate Jacoco coverage reports"
@ -212,7 +223,10 @@ task jacocoTestReport(type:JacocoReport) {
"${buildDir}/generated/source/buildConfig/debug", "${buildDir}/generated/source/buildConfig/debug",
"${buildDir}/generated/source/r/debug" "${buildDir}/generated/source/r/debug"
]) ])
executionData = files("${buildDir}/jacoco/testDebug.exec") executionData = files([
"${buildDir}/jacoco/testDebug.exec",
"${buildDir}/outputs/code-coverage/connected/coverage.ec"
])
reports { reports {
xml.enabled = true xml.enabled = true

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,156 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQINBFTxJOIBEACpXpXjz5ivyR/uWJexJXZrLHrvBRcivQSxzvy5Owdun8MGOxzo
YTPAJzskZH4Gg9MIQj9puUke8A59o9KtNMAd3Szp6aQs3lv/eGOw0DUe6g8a6kJ+
oQsHMYYLnyuYrOvQrBewKhh4FynkryvUZCL25dgrMHeTeMAeslCscdT6JYd2gxgi
o0+Bn8R6BonS7hTZStfAOYB0SO7X+7R0CpPsMiHtNZASzLRkk53dB9AO6m81CurP
qh1oPjc7j1QIiwCszSD8IxuR3fH6nyUlMtqx/mRmKNlbIYnr3sN5VAMp7THq/Qjt
9XDs1XHBE+0H+yg7K4fPamSJsxy9zPq/rA9vy4pQIMzxugxeWEn4OPWtguRSYtI1
WA+Ki93QJ+DVRlpMvz/MA71ZkvWUHpFnIssU52NdHZrIZ21KCo7PiPRY9tQkVhz5
S39a8npSeCIwZ9bH3UGKrRyPkhal59rjPqhZky1X+O2iTfbbFrPE+I4b46pWX9XU
UgXDnUh8Qeymhh0f1DTcbquW2nwU/R1quKjw/TCZQxKnkAzCXsVFk2QoIG1qDVrU
YlKXTXoYaRfzhMSYSvh7JHqdldB+MyKAbXpLtZsYIlLkuqe/KI0IPfO69ajDxLOr
+wTtuA9T1SICGmQkjYkQiqVy4y7vDW4Zw143sPXreZhiBbLBoWAIJ/JcbQARAQAB
tAVYIDx4PokCNwQTAQoAIQULCQgHAwYVCgkLCAMEFgIDAQIZAQWCVPEk4wKeAQKb
AQAKCRCdYE0vMQcWo/TGD/9SEfE7LxWw4pPNTpVTECDztpLGrj7kCr0anO6XnCqL
3OYoZz5vZvKBmunoPT7VqerQIZCnhB0b0ZOI+l51J03Xwvfy1+MIQCnZz++JY94B
h5dv3QAj8WUJJ6KfKrAxoXGU67mNhW5oe3mXQd3/a0JjmjV2yzFFOS4edyWPQal9
lY6MpOKfEIiD6uRqh8A1A9jAstJ9c5XHVtuBv265DEK5tMyAU3cmtm0pEzgmvPoG
kxo1vDAhP9GKb+DcBrB6XuZ7Kahl+kDpEhmuI6drxJBhez169aE43dK7G3X1W78f
OyO0C4jWAy1kVj4aYT4qgJj8TwRoAwlzX5RqmK4RW17sX3nOlff/FQclepawOrU+
LO5ZgbQ4qG2yDJSJ+tcS2fIO1MoI5vPa7DIVEpMM7DbrPYVy0Ix/xv80MwKQhnWs
P5tLRGuJ7JbKPzqvBtG7xWow5isOMkqeBkU0yUr59tAtHwM6c3Vi2at9YBiraqBY
3mgEukIuNpSuhFSBhniVUovVGgj8LCLDL+mpuc9+HUzYWJKGks+eGY0Od+dJjgh+
wXQk0rTTkY80kKpIjREDOVuPRhsw5OYb63fbiaYwormPx4pXv2mitOYNAXy3YNpR
Xl5MvObYLQugpqtyjpijyyANbsHKWwClkL/vxnbcfRXF307NQGSwhs3gKpSuovVE
edEkI2QBI0VncGdwaWQ6QGRuczp0ZXN0Lm11Z2VuZ3VpbGQuY29tiQIcBBMBCgAG
BYJU9KuzAAoJEJ1gTS8xBxajnPEP+gL3MasL9GcXt0c93QkQsRay8IMCspM+Qt/X
rfoUSJb41V4nqeyumX/kRmY7/eMZJnxIJgn8aWmRjiPhasUMBfeXmm4RAmwHEkFP
6nw288dHIlKgRaAMwfs44thKxPKTkscLiSqbmrRhLULW840pIZkgtOLmhH2tiaa7
w/hgDCMgHfwZaSlQkEDfvAef7i61itNdBlL0e0CelzNL0Uc5P6b/Nvn5uWlrLIHZ
evOx8dswhmwz25D7KZQPh91JpAqoLk5hj3/DAbVxeUFK/Zt9o/bu+ij57qSfRif3
HlLydH98FFcetqLbGDt1/Eiea3tpqjK5Xgf+HOppQo2MpOCP64xNejGWnKx6M7yF
A/yiI4Ahe4uP9+SXar0FPLGXAfyc/pSrRN94dY+iXpWPCSlNRNAOyr1mxPCvw9MM
C5wYnfLLSWUMPYv9rQyr1pvYyxe2r0Fla6JAkRy5nMnR1RdorwafeMcyQi3nllBS
S0wzmagus1W2qqRbbOR8J5CXkkjbw9vV0vXFnCHq93ms9Ebnhr5rNzVndKnxVCmK
hftW7gFz8Qz3tG66eSWA/6VrRvfUNfR5cUBpVOARqxqYz7OPrGg0Lkfw+JPQX8v/
WMIfM6Jsj/koJmOb2y/h0rJ+iTBofPCoGONKIzlOK5c8bhSmU/27KX1xBacTgN9Q
hlulIbZs0SsqZAAS1odwZ3BpZCtjb29raWU6QGRuczp0ZXN0Lm11Z2VuZ3VpbGQu
Y29tiQIcBBMBCgAGBYJU901MAAoJEJ1gTS8xBxajHKAP/3WDeUfBkuq50v3E8SBx
qDHXomPncNOccRPS1/DYwxVp5JYE8NsFW2rlmeJiy4Rhm4MnptUFUTPMBfYOcHvT
oWr/3Gz0y5ik5oiPC5hAXWCPrNkdtgBugBByL+0+w2PWKQqwBIc9ef+7aIh28dKn
6zr71Au+DXJKqYz/jOqY3IJGzBkydxOPSy83WNPm9OR/zfhCOoCTDAy+0TGxYgjW
Idi4k4yDRWhAeF+GqJRuDf1RKZxPL1MjwhX0Lv4ndR+V3sECRVh6HRIBGYYti8kU
0Q9jdbYjU+guestyyvsWQBOaGRP+gGQY+fYdHb2YUK6jMcndPxbRonjaRY5Bcc0J
NAYAqFv/WyR7hd4SWXI8Zg3u+GV7x8ZbnUOwfvXBVvZohbycpoT7WVOPRhQ2CQ5P
uS2E5QaoA6fUfH2qUAaxlvnfMRPL9E1svIEFXMtf1GqO6hzzJWV6Qw59HODTy568
StloM6IVnlolJfxf7+aEQx+KrmxJqVVWlbZ5u5gzQa+Mak0N5F6m8w4VftyHT0W6
RinNh3IGbk64+uXYzAUhJ6badfXrrxTP0wkjPPPjNC/BKfPCzYhZnyGLCuFwlOqE
o1Nq6qMzSHOlxR08ycX0MLNEfWsqu2CV8YGLYThejOr8JSjyXM3ANHNB+AZ5/eeY
AGUc5XgxTbq/MpnytiBzaZfw0Tw7ZAAS1odwZ3BpZCtjb29raWU6Z2VuZXJpY0Bo
dHRwczovL211Z2VuZ3VpbGQuY29tL3BncGtleS50eHSJAhwEEwEIAAYFglT4KOIA
CgkQnWBNLzEHFqOZJQ//UL+/ZfikmVA8DDIbbd8beIgJ0uRZgQ4aVuxRNiWhxQbl
5KPQaiztrwr3ESgyf3HjjUyGhg5/UP+ObvloCqcwCphSrBNxHiEp31jLin7QfWLp
vzPOjtMJNEeq3yeXq+YpHw2VZ+nod+XD785YtLBqq1SipNSZEifRYnsLhqQOj6cB
t0F2RLBm5afLbd4KjelT7W7229lYlGJkoQEwrmVYASiMfeBDBnHJ1Xgp1P0dm1gS
ERVSzV2qW6w0/psOHq4cnq4XWSn/IRW3UYfHIDDZJG125keYoaJAbBnzC+OO4eV2
KheY5dkmaeT0RmgzpEyJq5PRDNpnoXvSYuC9KGiNTbCiQ3liJnUJslbzDwvTyTfa
wxUba4dXhpcSE8mjXa/9/vMJER5yW0zzkSa+fjIIZcL0PQqlTeDBzFMSA9Ut9Y+m
6vdGwelIeyb38dWvsDC1NUUR2pydTsRtFM1o7jUtGoovQlLh5/9x/YDh8vqpzip3
L5SSvN4RdXN4P+EZor4xfi0PVbHoDZusxyPfKEFlCnX6k9qEaWplacLbl8M8mtpv
++3vYVW6hA3rK/6XdftFDJpbmj+zTXvrATE/lvPcJHiswiuMLx9BPoEiv1MGXTyE
6TxB5H7L1eCQWfbZYnkEWiuXlfDyofvDfufiGTsBAQzN3ePuhYUNznUB+1syD4E=
=Sxs8
-----END PGP PUBLIC KEY BLOCK-----
-----BEGIN PGP PRIVATE KEY BLOCK-----
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
=AYKY
-----END PGP PRIVATE KEY BLOCK-----

View File

@ -0,0 +1,262 @@
/*
* 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.app.Activity;
import android.content.Intent;
import android.support.test.rule.ActivityTestRule;
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.service.PassphraseCacheService;
import org.sufficientlysecure.keychain.ui.MainActivity;
import org.sufficientlysecure.keychain.ui.util.Notify.Style;
import static android.support.test.espresso.Espresso.onData;
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.matches;
import static android.support.test.espresso.contrib.DrawerActions.openDrawer;
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.withKeyItemId;
import static org.sufficientlysecure.keychain.matcher.DrawableMatcher.withDrawable;
@RunWith(AndroidJUnit4.class)
@LargeTest
public class AsymmetricOperationTests {
@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;
}
};
@Before
public void setUp() throws Exception {
Activity activity = mActivity.getActivity();
// 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 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);
{ // encrypt
// the EncryptKeyCompletionView is tested individually
onView(withId(R.id.recipient_list)).perform(tokenEncryptViewAddToken(0x9D604D2F310716A3L));
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();
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)));
}
}
@Test
public void testTextEncryptDecryptFromKeyView() throws Exception {
String cleartext = randomString(10, 30);
{ // encrypt
// 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());
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)));
}
}
@Test
public void testSignVerify() throws Exception {
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
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.sign)).perform(click());
onData(withKeyItemId(0x9D604D2F310716A3L))
.inAdapterView(isAssignableFrom(AdapterView.class))
.perform(click());
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(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)));
}
}
}

View File

@ -17,12 +17,14 @@
package org.sufficientlysecure.keychain; package org.sufficientlysecure.keychain;
import android.support.test.rule.ActivityTestRule; import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4; import android.support.test.runner.AndroidJUnit4;
import android.test.ActivityInstrumentationTestCase2;
import android.test.suitebuilder.annotation.LargeTest;
import android.text.method.HideReturnsTransformationMethod; import android.text.method.HideReturnsTransformationMethod;
import android.text.method.PasswordTransformationMethod; import android.text.method.PasswordTransformationMethod;
import org.junit.Rule; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.sufficientlysecure.keychain.ui.CreateKeyActivity; import org.sufficientlysecure.keychain.ui.CreateKeyActivity;
@ -44,15 +46,24 @@ import static org.sufficientlysecure.keychain.matcher.EditTextMatchers.withError
import static org.sufficientlysecure.keychain.matcher.EditTextMatchers.withTransformationMethod; import static org.sufficientlysecure.keychain.matcher.EditTextMatchers.withTransformationMethod;
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
public class CreateKeyActivityTest { @LargeTest
public class CreateKeyActivityTest extends ActivityInstrumentationTestCase2<CreateKeyActivity> {
public static final String SAMPLE_NAME = "Sample Name"; public static final String SAMPLE_NAME = "Sample Name";
public static final String SAMPLE_EMAIL = "sample_email@gmail.com"; public static final String SAMPLE_EMAIL = "sample_email@gmail.com";
public static final String SAMPLE_ADDITIONAL_EMAIL = "sample_additional_email@gmail.com"; public static final String SAMPLE_ADDITIONAL_EMAIL = "sample_additional_email@gmail.com";
public static final String SAMPLE_PASSWORD = "sample_password"; public static final String SAMPLE_PASSWORD = "sample_password";
@Rule public CreateKeyActivityTest() {
public ActivityTestRule<CreateKeyActivity> mActivityRule = new ActivityTestRule<>(CreateKeyActivity.class); super(CreateKeyActivity.class);
}
@Before
public void setUp() throws Exception {
super.setUp();
injectInstrumentation(InstrumentationRegistry.getInstrumentation());
getActivity();
}
@Test @Test
public void testCreateMyKey() { public void testCreateMyKey() {

View File

@ -0,0 +1,89 @@
/*
* 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.app.Activity;
import android.content.Intent;
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.LargeTest;
import android.widget.AdapterView;
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.provider.KeychainDatabase;
import org.sufficientlysecure.keychain.ui.MainActivity;
import org.sufficientlysecure.keychain.ui.util.Notify.Style;
import static android.support.test.espresso.Espresso.onData;
import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.action.ViewActions.click;
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.withId;
import static android.support.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.CoreMatchers.allOf;
import static org.sufficientlysecure.keychain.TestHelpers.checkSnackbar;
import static org.sufficientlysecure.keychain.TestHelpers.importKeysFromResource;
import static org.sufficientlysecure.keychain.matcher.CustomMatchers.withKeyItemId;
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
@RunWith(AndroidJUnit4.class)
@LargeTest
public class EditKeyTest {
@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 test01Edit() throws Exception {
Activity activity = mActivity.getActivity();
new KeychainDatabase(activity).clearDatabase();
// import key for testing, get a stable initial state
importKeysFromResource(activity, "x.sec.asc");
// 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.menu_key_view_edit)).perform(click());
// no-op should yield snackbar
onView(withText(R.string.btn_save)).perform(click());
checkSnackbar(Style.ERROR, R.string.msg_mf_error_noop);
}
}

View File

@ -0,0 +1,132 @@
/*
* 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)));
}
}
}

View File

@ -0,0 +1,89 @@
/*
* 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.app.Activity;
import android.content.Intent;
import android.support.test.espresso.action.ViewActions;
import android.support.test.espresso.matcher.RootMatchers;
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.LargeTest;
import android.view.KeyEvent;
import android.widget.AdapterView;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.sufficientlysecure.keychain.ui.EncryptTextActivity;
import static android.support.test.espresso.Espresso.onData;
import static android.support.test.espresso.Espresso.onView;
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.matcher.ViewMatchers.hasDescendant;
import static android.support.test.espresso.matcher.ViewMatchers.isAssignableFrom;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static org.hamcrest.CoreMatchers.allOf;
import static org.sufficientlysecure.keychain.TestHelpers.importKeysFromResource;
import static org.sufficientlysecure.keychain.actions.CustomActions.tokenEncryptViewAddToken;
import static org.sufficientlysecure.keychain.matcher.CustomMatchers.withKeyItemId;
import static org.sufficientlysecure.keychain.matcher.CustomMatchers.withKeyToken;
@RunWith(AndroidJUnit4.class)
@LargeTest
public class EncryptKeyCompletionViewTest {
@Rule
public final ActivityTestRule<EncryptTextActivity> mActivity
= new ActivityTestRule<>(EncryptTextActivity.class);
@Test
public void testTextEncryptDecryptFromToken() throws Exception {
Intent intent = new Intent();
intent.putExtra(EncryptTextActivity.EXTRA_ENCRYPTION_KEY_IDS, new long[] { 0x9D604D2F310716A3L });
Activity activity = mActivity.launchActivity(intent);
// import these two, make sure they're there
importKeysFromResource(activity, "x.sec.asc");
// check if the element passed in from intent
onView(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
onView(withId(R.id.recipient_list)).perform(typeText("x"));
onData(withKeyItemId(0x9D604D2F310716A3L)).inRoot(RootMatchers.isPlatformPopup())
.inAdapterView(allOf(isAssignableFrom(AdapterView.class),
hasDescendant(withId(R.id.key_list_item_name)))).perform(click());
onView(withId(R.id.recipient_list)).check(matches(withKeyToken(0x9D604D2F310716A3L)));
onView(withId(R.id.recipient_list)).perform(ViewActions.pressKey(KeyEvent.KEYCODE_DEL));
onView(withId(R.id.recipient_list)).perform(ViewActions.pressKey(KeyEvent.KEYCODE_DEL));
// add directly, check if it's there
onView(withId(R.id.recipient_list)).perform(tokenEncryptViewAddToken(0x9D604D2F310716A3L));
onView(withId(R.id.recipient_list)).check(matches(withKeyToken(0x9D604D2F310716A3L)));
onView(withId(R.id.recipient_list)).perform(ViewActions.pressKey(KeyEvent.KEYCODE_DEL));
}
}

View File

@ -0,0 +1,29 @@
package org.sufficientlysecure.keychain;
import java.lang.reflect.Method;
import android.os.Bundle;
import android.support.test.runner.AndroidJUnitRunner;
public class JacocoWorkaroundJUnitRunner extends AndroidJUnitRunner {
static {
System.setProperty("jacoco-agent.destfile", "/data/data/"
+ BuildConfig.APPLICATION_ID + "/coverage.ec");
}
@Override
public void finish(int resultCode, Bundle results) {
try {
Class rt = Class.forName("org.jacoco.agent.rt.RT");
Method getAgent = rt.getMethod("getAgent");
Method dump = getAgent.getReturnType().getMethod("dump", boolean.class);
Object agent = getAgent.invoke(null);
dump.invoke(agent, false);
} catch (Exception e) {
e.printStackTrace();
}
super.finish(resultCode, results);
}
}

View File

@ -0,0 +1,86 @@
/*
* 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 java.util.Random;
import android.content.Context;
import android.support.annotation.StringRes;
import org.hamcrest.CoreMatchers;
import org.sufficientlysecure.keychain.pgp.UncachedKeyRing;
import org.sufficientlysecure.keychain.pgp.UncachedKeyRing.IteratorWithIOThrow;
import org.sufficientlysecure.keychain.provider.ProviderHelper;
import org.sufficientlysecure.keychain.ui.util.Notify.Style;
import org.sufficientlysecure.keychain.util.ProgressScaler;
import static android.support.test.InstrumentationRegistry.getInstrumentation;
import static android.support.test.espresso.Espresso.onView;
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.sufficientlysecure.keychain.matcher.CustomMatchers.withSnackbarLineColor;
public class TestHelpers {
public static void checkSnackbar(Style style, @StringRes Integer text) {
onView(withClassName(CoreMatchers.endsWith("Snackbar")))
.check(matches(withSnackbarLineColor(style.mLineColor)));
if (text != null) {
onView(withClassName(CoreMatchers.endsWith("Snackbar")))
.check(matches(hasDescendant(withText(text))));
}
}
static void importKeysFromResource(Context context, String name) throws Exception {
IteratorWithIOThrow<UncachedKeyRing> stream = UncachedKeyRing.fromStream(
getInstrumentation().getContext().getAssets().open(name));
ProviderHelper helper = new ProviderHelper(context);
while(stream.hasNext()) {
UncachedKeyRing ring = stream.next();
if (ring.isSecret()) {
helper.saveSecretKeyRing(ring, new ProgressScaler());
} else {
helper.savePublicKeyRing(ring, new ProgressScaler());
}
}
}
public static String randomString(int min, int max) {
String chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456789!@#$%^&*()-_=";
Random r = new Random();
StringBuilder passbuilder = new StringBuilder();
// 5% chance for an empty string
for(int i = 0, j = r.nextInt(max)+min; i < j; i++) {
passbuilder.append(chars.charAt(r.nextInt(chars.length())));
}
return passbuilder.toString();
}
}

View File

@ -0,0 +1,82 @@
/*
* 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.actions;
import android.support.test.espresso.UiController;
import android.support.test.espresso.ViewAction;
import android.support.test.espresso.matcher.ViewMatchers;
import android.support.v4.view.GravityCompat;
import android.support.v4.widget.DrawerLayout;
import android.view.View;
import com.tokenautocomplete.TokenCompleteTextView;
import org.hamcrest.Matcher;
import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKeyRing;
import org.sufficientlysecure.keychain.provider.ProviderHelper;
import org.sufficientlysecure.keychain.ui.adapter.KeyAdapter;
import static android.support.test.InstrumentationRegistry.getTargetContext;
public abstract class CustomActions {
public static ViewAction tokenEncryptViewAddToken(long keyId) throws Exception {
CanonicalizedPublicKeyRing ring =
new ProviderHelper(getTargetContext()).getCanonicalizedPublicKeyRing(keyId);
final Object item = new KeyAdapter.KeyItem(ring);
return new ViewAction() {
@Override
public Matcher<View> getConstraints() {
return ViewMatchers.isAssignableFrom(TokenCompleteTextView.class);
}
@Override
public String getDescription() {
return "add completion token";
}
@Override
public void perform(UiController uiController, View view) {
((TokenCompleteTextView) view).addObject(item);
}
};
}
public static ViewAction tokenViewAddToken(final Object item) {
return new ViewAction() {
@Override
public Matcher<View> getConstraints() {
return ViewMatchers.isAssignableFrom(TokenCompleteTextView.class);
}
@Override
public String getDescription() {
return "add completion token";
}
@Override
public void perform(UiController uiController, View view) {
((TokenCompleteTextView) view).addObject(item);
}
};
}
}

View File

@ -0,0 +1,84 @@
/*
* 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.matcher;
import android.support.annotation.ColorRes;
import android.support.test.espresso.matcher.BoundedMatcher;
import android.view.View;
import com.nispok.snackbar.Snackbar;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.sufficientlysecure.keychain.EncryptKeyCompletionViewTest;
import org.sufficientlysecure.keychain.ui.adapter.KeyAdapter.KeyItem;
import org.sufficientlysecure.keychain.ui.widget.EncryptKeyCompletionView;
import static android.support.test.internal.util.Checks.checkNotNull;
public abstract class CustomMatchers {
public static Matcher<View> withSnackbarLineColor(@ColorRes final int colorRes) {
return new BoundedMatcher<View, Snackbar>(Snackbar.class) {
public void describeTo(Description description) {
description.appendText("with color resource id: " + colorRes);
}
@Override
public boolean matchesSafely(Snackbar snackbar) {
return snackbar.getResources().getColor(colorRes) == snackbar.getLineColor();
}
};
}
public static Matcher<Object> withKeyItemId(final long keyId) {
return new BoundedMatcher<Object, KeyItem>(KeyItem.class) {
@Override
public boolean matchesSafely(KeyItem item) {
return item.mKeyId == keyId;
}
@Override
public void describeTo(Description description) {
description.appendText("with key id: " + keyId);
}
};
}
public static Matcher<View> withKeyToken(@ColorRes final long keyId) {
return new BoundedMatcher<View, EncryptKeyCompletionView>(EncryptKeyCompletionView.class) {
public void describeTo(Description description) {
description.appendText("with key id token: " + keyId);
}
@Override
public boolean matchesSafely(EncryptKeyCompletionView tokenView) {
for (Object object : tokenView.getObjects()) {
if (object instanceof KeyItem && ((KeyItem) object).mKeyId == keyId) {
return true;
}
}
return false;
}
};
}
}

View File

@ -0,0 +1,128 @@
/*
* Copyright (C) 2015 Xavi Rigau <xrigau@gmail.com>
* 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.content.res.Resources;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import org.hamcrest.Description;
import org.hamcrest.TypeSafeMatcher;
public class DrawableMatcher extends TypeSafeMatcher<View> {
private final int mResourceId;
private final boolean mIgnoreFilters;
public DrawableMatcher(int resourceId, boolean ignoreFilters) {
super(View.class);
mResourceId = resourceId;
mIgnoreFilters = ignoreFilters;
}
private String resourceName = null;
private Drawable expectedDrawable = null;
@Override
public boolean matchesSafely(View target) {
if (expectedDrawable == null) {
loadDrawableFromResources(target.getResources());
}
if (invalidExpectedDrawable()) {
return false;
}
if (target instanceof ImageView) {
return hasImage((ImageView) target) || hasBackground(target);
}
if (target instanceof TextView) {
return hasCompoundDrawable((TextView) target) || hasBackground(target);
}
return hasBackground(target);
}
private void loadDrawableFromResources(Resources resources) {
try {
expectedDrawable = resources.getDrawable(mResourceId);
resourceName = resources.getResourceEntryName(mResourceId);
} catch (Resources.NotFoundException ignored) {
// view could be from a context unaware of the resource id.
}
}
private boolean invalidExpectedDrawable() {
return expectedDrawable == null;
}
private boolean hasImage(ImageView target) {
return isSameDrawable(target.getDrawable());
}
private boolean hasCompoundDrawable(TextView target) {
for (Drawable drawable : target.getCompoundDrawables()) {
if (isSameDrawable(drawable)) {
return true;
}
}
return false;
}
private boolean hasBackground(View target) {
return isSameDrawable(target.getBackground());
}
private boolean isSameDrawable(Drawable drawable) {
if (drawable == null) {
return false;
}
// 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 expectedDrawable.getConstantState().equals(drawable.getConstantState());
}
@Override
public void describeTo(Description description) {
description.appendText("with drawable from resource id: ");
description.appendValue(mResourceId);
if (resourceName != null) {
description.appendText("[");
description.appendText(resourceName);
description.appendText("]");
}
}
public static DrawableMatcher withDrawable(int resourceId, boolean ignoreFilters) {
return new DrawableMatcher(resourceId, ignoreFilters);
}
public static DrawableMatcher withDrawable(int resourceId) {
return new DrawableMatcher(resourceId, true);
}
}

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2015 Dominik Schürmann <dominik@dominikschuermann.de> * Copyright (C) 2015 Vincent Breitmoser <look@my.amazin.horse>
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by

View File

@ -74,6 +74,10 @@ public class ClipboardReflection {
Method methodGetPrimaryClip = clipboard.getClass().getMethod("getPrimaryClip"); Method methodGetPrimaryClip = clipboard.getClass().getMethod("getPrimaryClip");
Object clipData = methodGetPrimaryClip.invoke(clipboard); Object clipData = methodGetPrimaryClip.invoke(clipboard);
if (clipData == null) {
return null;
}
// ClipData.Item clipDataItem = clipData.getItemAt(0); // ClipData.Item clipDataItem = clipData.getItemAt(0);
Method methodGetItemAt = clipData.getClass().getMethod("getItemAt", int.class); Method methodGetItemAt = clipData.getClass().getMethod("getItemAt", int.class);
Object clipDataItem = methodGetItemAt.invoke(clipData, 0); Object clipDataItem = methodGetItemAt.invoke(clipData, 0);

View File

@ -82,7 +82,7 @@ public class EditKeyOperation extends BaseOperation<SaveKeyringParcel> {
CanonicalizedSecretKeyRing secRing = CanonicalizedSecretKeyRing secRing =
mProviderHelper.getCanonicalizedSecretKeyRing(saveParcel.mMasterKeyId); mProviderHelper.getCanonicalizedSecretKeyRing(saveParcel.mMasterKeyId);
modifyResult = keyOperations.modifySecretKeyRing(secRing, cryptoInput, saveParcel, log, 2); modifyResult = keyOperations.modifySecretKeyRing(secRing, cryptoInput, saveParcel);
if (modifyResult.isPending()) { if (modifyResult.isPending()) {
return modifyResult; return modifyResult;
} }

View File

@ -854,7 +854,11 @@ public abstract class OperationResult implements Parcelable {
if (mParcels.isEmpty()) { if (mParcels.isEmpty()) {
return null; return null;
} }
return mParcels.get(mParcels.size() -1); LogEntryParcel last = mParcels.get(mParcels.size() -1);
if (last instanceof SubLogEntryParcel) {
return ((SubLogEntryParcel) last).getSubResult().getLog().getLast();
}
return last;
} }
@Override @Override

View File

@ -149,7 +149,7 @@ public class PgpDecryptVerify extends BaseOperation<PgpDecryptVerifyInputParcel>
return verifySignedLiteralData(input, aIn, outputStream, 0); return verifySignedLiteralData(input, aIn, outputStream, 0);
} else if (aIn.isClearText()) { } else if (aIn.isClearText()) {
// a cleartext signature, verify it with the other method // a cleartext signature, verify it with the other method
return verifyCleartextSignature(aIn, 0); return verifyCleartextSignature(aIn, outputStream, 0);
} else { } else {
// else: ascii armored encryption! go on... // else: ascii armored encryption! go on...
return decryptVerify(input, cryptoInput, in, outputStream, 0); return decryptVerify(input, cryptoInput, in, outputStream, 0);
@ -757,6 +757,12 @@ public class PgpDecryptVerify extends BaseOperation<PgpDecryptVerifyInputParcel>
// TODO: slow annealing to fake a progress? // TODO: slow annealing to fake a progress?
} }
metadata = new OpenPgpMetadata(
originalFilename,
mimeType,
literalData.getModificationTime().getTime(),
alreadyWritten);
if (signature != null) { if (signature != null) {
updateProgress(R.string.progress_verifying_signature, 90, 100); updateProgress(R.string.progress_verifying_signature, 90, 100);
log.add(LogType.MSG_DC_CLEAR_SIGNATURE_CHECK, indent); log.add(LogType.MSG_DC_CLEAR_SIGNATURE_CHECK, indent);
@ -833,7 +839,7 @@ public class PgpDecryptVerify extends BaseOperation<PgpDecryptVerifyInputParcel>
* pg/src/main/java/org/spongycastle/openpgp/examples/ClearSignedFileProcessor.java * pg/src/main/java/org/spongycastle/openpgp/examples/ClearSignedFileProcessor.java
*/ */
private DecryptVerifyResult verifyCleartextSignature( private DecryptVerifyResult verifyCleartextSignature(
ArmoredInputStream aIn, int indent) throws IOException, PGPException { ArmoredInputStream aIn, OutputStream outputStream, int indent) throws IOException, PGPException {
OperationLog log = new OperationLog(); OperationLog log = new OperationLog();
@ -863,8 +869,9 @@ public class PgpDecryptVerify extends BaseOperation<PgpDecryptVerifyInputParcel>
out.close(); out.close();
byte[] clearText = out.toByteArray(); byte[] clearText = out.toByteArray();
if (out != null) { if (outputStream != null) {
out.write(clearText); outputStream.write(clearText);
outputStream.close();
} }
updateProgress(R.string.progress_processing_signature, 60, 100); updateProgress(R.string.progress_processing_signature, 60, 100);

View File

@ -365,14 +365,9 @@ public class PgpKeyOperation {
public PgpEditKeyResult modifySecretKeyRing(CanonicalizedSecretKeyRing wsKR, public PgpEditKeyResult modifySecretKeyRing(CanonicalizedSecretKeyRing wsKR,
CryptoInputParcel cryptoInput, CryptoInputParcel cryptoInput,
SaveKeyringParcel saveParcel) { SaveKeyringParcel saveParcel) {
return modifySecretKeyRing(wsKR, cryptoInput, saveParcel, new OperationLog(), 0);
}
public PgpEditKeyResult modifySecretKeyRing(CanonicalizedSecretKeyRing wsKR, OperationLog log = new OperationLog();
CryptoInputParcel cryptoInput, int indent = 0;
SaveKeyringParcel saveParcel,
OperationLog log,
int indent) {
/* /*
* 1. Unlock private key * 1. Unlock private key

View File

@ -179,7 +179,7 @@ public class KeychainDatabase extends SQLiteOpenHelper {
+ Tables.API_APPS + "(" + ApiAppsAllowedKeysColumns.PACKAGE_NAME + ") ON DELETE CASCADE" + Tables.API_APPS + "(" + ApiAppsAllowedKeysColumns.PACKAGE_NAME + ") ON DELETE CASCADE"
+ ")"; + ")";
KeychainDatabase(Context context) { public KeychainDatabase(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION); super(context, DATABASE_NAME, null, DATABASE_VERSION);
mContext = context; mContext = context;

View File

@ -149,6 +149,14 @@ public class PassphraseCacheService extends Service {
context.startService(intent); context.startService(intent);
} }
public static void clearCachedPassphrases(Context context) {
Log.d(Constants.TAG, "PassphraseCacheService.clearCachedPassphrase()");
Intent intent = new Intent(context, PassphraseCacheService.class);
intent.setAction(ACTION_PASSPHRASE_CACHE_CLEAR);
context.startService(intent);
}
/** /**
* Gets a cached passphrase from memory by sending an intent to the service. This method is * Gets a cached passphrase from memory by sending an intent to the service. This method is

View File

@ -153,7 +153,9 @@ public class DecryptTextFragment extends DecryptFragment {
@Override @Override
protected PgpDecryptVerifyInputParcel createOperationInput() { protected PgpDecryptVerifyInputParcel createOperationInput() {
return new PgpDecryptVerifyInputParcel(mCiphertext.getBytes()); PgpDecryptVerifyInputParcel input = new PgpDecryptVerifyInputParcel(mCiphertext.getBytes());
input.setAllowSymmetricDecryption(true);
return input;
} }
@Override @Override

View File

@ -75,7 +75,6 @@ public class EncryptModeAsymmetricFragment extends EncryptModeFragment {
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.encrypt_asymmetric_fragment, container, false); View view = inflater.inflate(R.layout.encrypt_asymmetric_fragment, container, false);
mSignKeySpinner = (KeySpinner) view.findViewById(R.id.sign); mSignKeySpinner = (KeySpinner) view.findViewById(R.id.sign);
mEncryptKeyView = (EncryptKeyCompletionView) view.findViewById(R.id.recipient_list); mEncryptKeyView = (EncryptKeyCompletionView) view.findViewById(R.id.recipient_list);
mEncryptKeyView.setThreshold(1); // Start working from first character mEncryptKeyView.setThreshold(1); // Start working from first character
@ -168,7 +167,7 @@ public class EncryptModeAsymmetricFragment extends EncryptModeFragment {
@Override @Override
public long getAsymmetricSigningKeyId() { public long getAsymmetricSigningKeyId() {
return mSignKeySpinner.getSelectedItemId(); return mSignKeySpinner.getSelectedKeyId();
} }
@Override @Override

View File

@ -240,7 +240,7 @@ public class EncryptTextFragment
boolean gotEncryptionKeys = (encryptionKeyIds != null boolean gotEncryptionKeys = (encryptionKeyIds != null
&& encryptionKeyIds.length > 0); && encryptionKeyIds.length > 0);
if (!gotEncryptionKeys && signingKeyId == 0L) { if (!gotEncryptionKeys && signingKeyId == Constants.key.none) {
Notify.create(getActivity(), R.string.select_encryption_or_signature_key, Notify.Style.ERROR) Notify.create(getActivity(), R.string.select_encryption_or_signature_key, Notify.Style.ERROR)
.show(this); .show(this);
return null; return null;

View File

@ -51,6 +51,8 @@ public class MainActivity extends BaseNfcActivity implements FabContainer, OnBac
private static final int ID_SETTINGS = 4; private static final int ID_SETTINGS = 4;
private static final int ID_HELP = 5; private static final int ID_HELP = 5;
public static final String EXTRA_SKIP_FIRST_TIME = "skip_first_time";
public Drawer.Result mDrawerResult; public Drawer.Result mDrawerResult;
private Toolbar mToolbar; private Toolbar mToolbar;
@ -114,7 +116,7 @@ public class MainActivity extends BaseNfcActivity implements FabContainer, OnBac
// if this is the first time show first time activity // if this is the first time show first time activity
Preferences prefs = Preferences.getPreferences(this); Preferences prefs = Preferences.getPreferences(this);
if (prefs.isFirstTime()) { if (!getIntent().getBooleanExtra(EXTRA_SKIP_FIRST_TIME, false) && prefs.isFirstTime()) {
Intent intent = new Intent(this, CreateKeyActivity.class); Intent intent = new Intent(this, CreateKeyActivity.class);
intent.putExtra(CreateKeyActivity.EXTRA_FIRST_TIME, true); intent.putExtra(CreateKeyActivity.EXTRA_FIRST_TIME, true);
startActivity(intent); startActivity(intent);

View File

@ -17,6 +17,7 @@
package org.sufficientlysecure.keychain.ui; package org.sufficientlysecure.keychain.ui;
import android.app.Activity; import android.app.Activity;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.app.Dialog; import android.app.Dialog;
@ -43,14 +44,12 @@ import android.widget.Toast;
import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.compatibility.DialogFragmentWorkaround;
import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey; import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey;
import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey.SecretKeyType; import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey.SecretKeyType;
import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKeyRing; import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKeyRing;
import org.sufficientlysecure.keychain.pgp.KeyRing; import org.sufficientlysecure.keychain.pgp.KeyRing;
import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException;
import org.sufficientlysecure.keychain.provider.CachedPublicKeyRing;
import org.sufficientlysecure.keychain.provider.KeychainContract; import org.sufficientlysecure.keychain.provider.KeychainContract;
import org.sufficientlysecure.keychain.provider.ProviderHelper; import org.sufficientlysecure.keychain.provider.ProviderHelper;
import org.sufficientlysecure.keychain.provider.ProviderHelper.NotFoundException; import org.sufficientlysecure.keychain.provider.ProviderHelper.NotFoundException;
@ -75,8 +74,7 @@ public class PassphraseDialogActivity extends FragmentActivity {
// special extra for OpenPgpService // special extra for OpenPgpService
public static final String EXTRA_SERVICE_INTENT = "data"; public static final String EXTRA_SERVICE_INTENT = "data";
private long mSubKeyId;
private static final int REQUEST_CODE_ENTER_PATTERN = 2;
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
@ -93,14 +91,13 @@ public class PassphraseDialogActivity extends FragmentActivity {
// this activity itself has no content view (see manifest) // this activity itself has no content view (see manifest)
long keyId;
if (getIntent().hasExtra(EXTRA_SUBKEY_ID)) { if (getIntent().hasExtra(EXTRA_SUBKEY_ID)) {
keyId = getIntent().getLongExtra(EXTRA_SUBKEY_ID, 0); mSubKeyId = getIntent().getLongExtra(EXTRA_SUBKEY_ID, 0);
} else { } else {
RequiredInputParcel requiredInput = getIntent().getParcelableExtra(EXTRA_REQUIRED_INPUT); RequiredInputParcel requiredInput = getIntent().getParcelableExtra(EXTRA_REQUIRED_INPUT);
switch (requiredInput.mType) { switch (requiredInput.mType) {
case PASSPHRASE_SYMMETRIC: { case PASSPHRASE_SYMMETRIC: {
keyId = Constants.key.symmetric; mSubKeyId = Constants.key.symmetric;
break; break;
} }
case PASSPHRASE: { case PASSPHRASE: {
@ -127,7 +124,7 @@ public class PassphraseDialogActivity extends FragmentActivity {
return; return;
} }
keyId = requiredInput.getSubKeyId(); mSubKeyId = requiredInput.getSubKeyId();
break; break;
} }
default: { default: {
@ -136,62 +133,35 @@ public class PassphraseDialogActivity extends FragmentActivity {
} }
} }
Intent serviceIntent = getIntent().getParcelableExtra(EXTRA_SERVICE_INTENT);
show(this, keyId, serviceIntent);
} }
@Override @Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) { protected void onResume() {
switch (requestCode) { super.onResume();
case REQUEST_CODE_ENTER_PATTERN: {
/*
* NOTE that there are 4 possible result codes!!!
*/
switch (resultCode) {
case RESULT_OK:
// The user passed
break;
case RESULT_CANCELED:
// The user cancelled the task
break;
// case LockPatternActivity.RESULT_FAILED:
// // The user failed to enter the pattern
// break;
// case LockPatternActivity.RESULT_FORGOT_PATTERN:
// // The user forgot the pattern and invoked your recovery Activity.
// break;
}
/* /* Show passphrase dialog to cache a new passphrase the user enters for using it later for
* In any case, there's always a key EXTRA_RETRY_COUNT, which holds
* the number of tries that the user did.
*/
// int retryCount = data.getIntExtra(
// LockPatternActivity.EXTRA_RETRY_COUNT, 0);
break;
}
}
}
/**
* Shows passphrase dialog to cache a new passphrase the user enters for using it later for
* encryption. Based on mSecretKeyId it asks for a passphrase to open a private key or it asks * encryption. Based on mSecretKeyId it asks for a passphrase to open a private key or it asks
* for a symmetric passphrase * for a symmetric passphrase
*/ */
public static void show(final FragmentActivity context, final long keyId, final Intent serviceIntent) {
DialogFragmentWorkaround.INTERFACE.runnableRunDelayed(new Runnable() { Intent serviceIntent = getIntent().getParcelableExtra(EXTRA_SERVICE_INTENT);
public void run() {
// do NOT check if the key even needs a passphrase. that's not our job here.
PassphraseDialogFragment frag = new PassphraseDialogFragment(); PassphraseDialogFragment frag = new PassphraseDialogFragment();
Bundle args = new Bundle(); Bundle args = new Bundle();
args.putLong(EXTRA_SUBKEY_ID, keyId); args.putLong(EXTRA_SUBKEY_ID, mSubKeyId);
args.putParcelable(EXTRA_SERVICE_INTENT, serviceIntent); args.putParcelable(EXTRA_SERVICE_INTENT, serviceIntent);
frag.setArguments(args); frag.setArguments(args);
frag.show(context.getSupportFragmentManager(), "passphraseDialog"); frag.show(getSupportFragmentManager(), "passphraseDialog");
}
@Override
protected void onPause() {
super.onPause();
DialogFragment dialog = (DialogFragment) getSupportFragmentManager().findFragmentByTag("passphraseDialog");
if (dialog != null) {
dialog.dismiss();
} }
});
} }
public static class PassphraseDialogFragment extends DialogFragment implements TextView.OnEditorActionListener { public static class PassphraseDialogFragment extends DialogFragment implements TextView.OnEditorActionListener {
@ -205,9 +175,6 @@ public class PassphraseDialogActivity extends FragmentActivity {
private Intent mServiceIntent; private Intent mServiceIntent;
/**
* Creates dialog
*/
@Override @Override
public Dialog onCreateDialog(Bundle savedInstanceState) { public Dialog onCreateDialog(Bundle savedInstanceState) {
final Activity activity = getActivity(); final Activity activity = getActivity();
@ -268,12 +235,7 @@ public class PassphraseDialogActivity extends FragmentActivity {
userId = null; userId = null;
} }
/* Get key type for message */ keyType = mSecretRing.getSecretKey(mSubKeyId).getSecretKeyType();
// find a master key id for our key
long masterKeyId = new ProviderHelper(activity).getMasterKeyId(mSubKeyId);
CachedPublicKeyRing keyRing = new ProviderHelper(activity).getCachedPublicKeyRing(masterKeyId);
// get the type of key (from the database)
keyType = keyRing.getSecretKeyType(mSubKeyId);
switch (keyType) { switch (keyType) {
case PASSPHRASE: case PASSPHRASE:
message = getString(R.string.passphrase_for, userId); message = getString(R.string.passphrase_for, userId);
@ -468,20 +430,16 @@ public class PassphraseDialogActivity extends FragmentActivity {
// note we need no synchronization here, this variable is only accessed in the ui thread // note we need no synchronization here, this variable is only accessed in the ui thread
mIsCancelled = true; mIsCancelled = true;
getActivity().setResult(RESULT_CANCELED);
getActivity().finish();
} }
@Override @Override
public void onDismiss(DialogInterface dialog) { public void onDismiss(DialogInterface dialog) {
super.onDismiss(dialog); super.onDismiss(dialog);
if (getActivity() == null) {
return;
}
hideKeyboard(); hideKeyboard();
getActivity().setResult(RESULT_CANCELED);
getActivity().finish();
} }
private void hideKeyboard() { private void hideKeyboard() {
@ -495,11 +453,9 @@ public class PassphraseDialogActivity extends FragmentActivity {
inputManager.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0); inputManager.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0);
} }
/**
* Associate the "done" button on the soft keyboard with the okay button in the view
*/
@Override @Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
// Associate the "done" button on the soft keyboard with the okay button in the view
if (EditorInfo.IME_ACTION_DONE == actionId) { if (EditorInfo.IME_ACTION_DONE == actionId) {
AlertDialog dialog = ((AlertDialog) getDialog()); AlertDialog dialog = ((AlertDialog) getDialog());
Button bt = dialog.getButton(AlertDialog.BUTTON_POSITIVE); Button bt = dialog.getButton(AlertDialog.BUTTON_POSITIVE);

View File

@ -300,7 +300,7 @@ public class ViewKeyFragment extends LoaderFragment implements
* because the notification triggers faster than the activity closes. * because the notification triggers faster than the activity closes.
*/ */
// Avoid NullPointerExceptions... // Avoid NullPointerExceptions...
if (data.getCount() == 0) { if (data == null || data.getCount() == 0) {
return; return;
} }
// Swap the new cursor in. (The framework will take care of closing the // Swap the new cursor in. (The framework will take care of closing the

View File

@ -19,15 +19,16 @@ package org.sufficientlysecure.keychain.ui.adapter;
import java.io.Serializable; import java.io.Serializable;
import java.util.Calendar; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date; import java.util.Date;
import java.util.TimeZone; import java.util.List;
import android.content.Context; import android.content.Context;
import android.database.Cursor; import android.database.Cursor;
import android.graphics.PorterDuff; import android.graphics.PorterDuff;
import android.support.v4.widget.CursorAdapter; import android.support.v4.widget.CursorAdapter;
import android.text.format.DateFormat;
import android.text.format.DateUtils; import android.text.format.DateUtils;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
@ -60,7 +61,6 @@ public class KeyAdapter extends CursorAdapter {
KeyRings.VERIFIED, KeyRings.VERIFIED,
KeyRings.HAS_ANY_SECRET, KeyRings.HAS_ANY_SECRET,
KeyRings.HAS_DUPLICATE_USER_ID, KeyRings.HAS_DUPLICATE_USER_ID,
KeyRings.HAS_ENCRYPT,
KeyRings.FINGERPRINT, KeyRings.FINGERPRINT,
KeyRings.CREATION, KeyRings.CREATION,
}; };
@ -72,9 +72,8 @@ public class KeyAdapter extends CursorAdapter {
public static final int INDEX_VERIFIED = 5; public static final int INDEX_VERIFIED = 5;
public static final int INDEX_HAS_ANY_SECRET = 6; public static final int INDEX_HAS_ANY_SECRET = 6;
public static final int INDEX_HAS_DUPLICATE_USER_ID = 7; public static final int INDEX_HAS_DUPLICATE_USER_ID = 7;
public static final int INDEX_HAS_ENCRYPT = 8; public static final int INDEX_FINGERPRINT = 8;
public static final int INDEX_FINGERPRINT = 9; public static final int INDEX_CREATION = 9;
public static final int INDEX_CREATION = 10;
public KeyAdapter(Context context, Cursor c, int flags) { public KeyAdapter(Context context, Cursor c, int flags) {
super(context, c, flags); super(context, c, flags);
@ -87,6 +86,7 @@ public class KeyAdapter extends CursorAdapter {
} }
public static class KeyItemViewHolder { public static class KeyItemViewHolder {
public View mView;
public Long mMasterKeyId; public Long mMasterKeyId;
public TextView mMainUserId; public TextView mMainUserId;
public TextView mMainUserIdRest; public TextView mMainUserIdRest;
@ -96,6 +96,7 @@ public class KeyAdapter extends CursorAdapter {
public ImageButton mSlingerButton; public ImageButton mSlingerButton;
public KeyItemViewHolder(View view) { public KeyItemViewHolder(View view) {
mView = view;
mMainUserId = (TextView) view.findViewById(R.id.key_list_item_name); mMainUserId = (TextView) view.findViewById(R.id.key_list_item_name);
mMainUserIdRest = (TextView) view.findViewById(R.id.key_list_item_email); mMainUserIdRest = (TextView) view.findViewById(R.id.key_list_item_email);
mStatus = (ImageView) view.findViewById(R.id.key_list_item_status_icon); mStatus = (ImageView) view.findViewById(R.id.key_list_item_status_icon);
@ -104,11 +105,10 @@ public class KeyAdapter extends CursorAdapter {
mCreationDate = (TextView) view.findViewById(R.id.key_list_item_creation); mCreationDate = (TextView) view.findViewById(R.id.key_list_item_creation);
} }
public void setData(Context context, Cursor cursor, Highlighter highlighter) { public void setData(Context context, KeyItem item, Highlighter highlighter) {
{ // set name and stuff, common to both key types { // set name and stuff, common to both key types
String userId = cursor.getString(INDEX_USER_ID); KeyRing.UserId userIdSplit = item.mUserId;
KeyRing.UserId userIdSplit = KeyRing.splitUserId(userId);
if (userIdSplit.name != null) { if (userIdSplit.name != null) {
mMainUserId.setText(highlighter.highlight(userIdSplit.name)); mMainUserId.setText(highlighter.highlight(userIdSplit.name));
} else { } else {
@ -124,30 +124,23 @@ public class KeyAdapter extends CursorAdapter {
{ // set edit button and status, specific by key type { // set edit button and status, specific by key type
long masterKeyId = cursor.getLong(INDEX_MASTER_KEY_ID); mMasterKeyId = item.mKeyId;
boolean isSecret = cursor.getInt(INDEX_HAS_ANY_SECRET) != 0;
boolean isRevoked = cursor.getInt(INDEX_IS_REVOKED) > 0;
boolean isExpired = cursor.getInt(INDEX_IS_EXPIRED) != 0;
boolean isVerified = cursor.getInt(INDEX_VERIFIED) > 0;
boolean hasDuplicate = cursor.getInt(INDEX_HAS_DUPLICATE_USER_ID) != 0;
mMasterKeyId = masterKeyId;
// Note: order is important! // Note: order is important!
if (isRevoked) { if (item.mIsRevoked) {
KeyFormattingUtils KeyFormattingUtils
.setStatusImage(context, mStatus, null, State.REVOKED, R.color.bg_gray); .setStatusImage(context, mStatus, null, State.REVOKED, R.color.bg_gray);
mStatus.setVisibility(View.VISIBLE); mStatus.setVisibility(View.VISIBLE);
mSlinger.setVisibility(View.GONE); mSlinger.setVisibility(View.GONE);
mMainUserId.setTextColor(context.getResources().getColor(R.color.bg_gray)); mMainUserId.setTextColor(context.getResources().getColor(R.color.bg_gray));
mMainUserIdRest.setTextColor(context.getResources().getColor(R.color.bg_gray)); mMainUserIdRest.setTextColor(context.getResources().getColor(R.color.bg_gray));
} else if (isExpired) { } else if (item.mIsExpired) {
KeyFormattingUtils.setStatusImage(context, mStatus, null, State.EXPIRED, R.color.bg_gray); KeyFormattingUtils.setStatusImage(context, mStatus, null, State.EXPIRED, R.color.bg_gray);
mStatus.setVisibility(View.VISIBLE); mStatus.setVisibility(View.VISIBLE);
mSlinger.setVisibility(View.GONE); mSlinger.setVisibility(View.GONE);
mMainUserId.setTextColor(context.getResources().getColor(R.color.bg_gray)); mMainUserId.setTextColor(context.getResources().getColor(R.color.bg_gray));
mMainUserIdRest.setTextColor(context.getResources().getColor(R.color.bg_gray)); mMainUserIdRest.setTextColor(context.getResources().getColor(R.color.bg_gray));
} else if (isSecret) { } else if (item.mIsSecret) {
mStatus.setVisibility(View.GONE); mStatus.setVisibility(View.GONE);
if (mSlingerButton.hasOnClickListeners()) { if (mSlingerButton.hasOnClickListeners()) {
mSlinger.setVisibility(View.VISIBLE); mSlinger.setVisibility(View.VISIBLE);
@ -158,7 +151,7 @@ public class KeyAdapter extends CursorAdapter {
mMainUserIdRest.setTextColor(context.getResources().getColor(R.color.black)); mMainUserIdRest.setTextColor(context.getResources().getColor(R.color.black));
} else { } else {
// this is a public key - show if it's verified // this is a public key - show if it's verified
if (isVerified) { if (item.mIsVerified) {
KeyFormattingUtils.setStatusImage(context, mStatus, State.VERIFIED); KeyFormattingUtils.setStatusImage(context, mStatus, State.VERIFIED);
mStatus.setVisibility(View.VISIBLE); mStatus.setVisibility(View.VISIBLE);
} else { } else {
@ -170,9 +163,9 @@ public class KeyAdapter extends CursorAdapter {
mMainUserIdRest.setTextColor(context.getResources().getColor(R.color.black)); mMainUserIdRest.setTextColor(context.getResources().getColor(R.color.black));
} }
if (hasDuplicate) { if (item.mHasDuplicate) {
String dateTime = DateUtils.formatDateTime(context, String dateTime = DateUtils.formatDateTime(context,
cursor.getLong(INDEX_CREATION) * 1000, item.mCreation.getTime(),
DateUtils.FORMAT_SHOW_DATE DateUtils.FORMAT_SHOW_DATE
| DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_SHOW_YEAR
| DateUtils.FORMAT_ABBREV_MONTH); | DateUtils.FORMAT_ABBREV_MONTH);
@ -190,6 +183,10 @@ public class KeyAdapter extends CursorAdapter {
} }
public boolean isEnabled(Cursor cursor) {
return true;
}
@Override @Override
public View newView(Context context, Cursor cursor, ViewGroup parent) { public View newView(Context context, Cursor cursor, ViewGroup parent) {
View view = mInflater.inflate(R.layout.key_list_item, parent, false); View view = mInflater.inflate(R.layout.key_list_item, parent, false);
@ -204,7 +201,8 @@ public class KeyAdapter extends CursorAdapter {
public void bindView(View view, Context context, Cursor cursor) { public void bindView(View view, Context context, Cursor cursor) {
Highlighter highlighter = new Highlighter(context, mQuery); Highlighter highlighter = new Highlighter(context, mQuery);
KeyItemViewHolder h = (KeyItemViewHolder) view.getTag(); KeyItemViewHolder h = (KeyItemViewHolder) view.getTag();
h.setData(context, cursor, highlighter); KeyItem item = new KeyItem(cursor);
h.setData(context, item, highlighter);
} }
public boolean isSecretAvailable(int id) { public boolean isSecretAvailable(int id) {
@ -234,8 +232,9 @@ public class KeyAdapter extends CursorAdapter {
@Override @Override
public long getItemId(int position) { public long getItemId(int position) {
Cursor cursor = getCursor();
// prevent a crash on rapid cursor changes // prevent a crash on rapid cursor changes
if (getCursor().isClosed()) { if (cursor != null && getCursor().isClosed()) {
return 0L; return 0L;
} }
return super.getItemId(position); return super.getItemId(position);
@ -250,6 +249,7 @@ public class KeyAdapter extends CursorAdapter {
public final boolean mHasDuplicate; public final boolean mHasDuplicate;
public final Date mCreation; public final Date mCreation;
public final String mFingerprint; public final String mFingerprint;
public final boolean mIsSecret, mIsRevoked, mIsExpired, mIsVerified;
private KeyItem(Cursor cursor) { private KeyItem(Cursor cursor) {
String userId = cursor.getString(INDEX_USER_ID); String userId = cursor.getString(INDEX_USER_ID);
@ -260,6 +260,10 @@ public class KeyAdapter extends CursorAdapter {
mCreation = new Date(cursor.getLong(INDEX_CREATION) * 1000); mCreation = new Date(cursor.getLong(INDEX_CREATION) * 1000);
mFingerprint = KeyFormattingUtils.convertFingerprintToHex( mFingerprint = KeyFormattingUtils.convertFingerprintToHex(
cursor.getBlob(INDEX_FINGERPRINT)); cursor.getBlob(INDEX_FINGERPRINT));
mIsSecret = cursor.getInt(INDEX_HAS_ANY_SECRET) != 0;
mIsRevoked = cursor.getInt(INDEX_IS_REVOKED) > 0;
mIsExpired = cursor.getInt(INDEX_IS_EXPIRED) > 0;
mIsVerified = cursor.getInt(INDEX_VERIFIED) > 0;
} }
public KeyItem(CanonicalizedPublicKeyRing ring) { public KeyItem(CanonicalizedPublicKeyRing ring) {
@ -272,6 +276,12 @@ public class KeyAdapter extends CursorAdapter {
mCreation = key.getCreationTime(); mCreation = key.getCreationTime();
mFingerprint = KeyFormattingUtils.convertFingerprintToHex( mFingerprint = KeyFormattingUtils.convertFingerprintToHex(
ring.getFingerprint()); ring.getFingerprint());
mIsRevoked = key.isRevoked();
mIsExpired = key.isExpired();
// these two are actually "don't know"s
mIsSecret = false;
mIsVerified = false;
} }
public String getReadableName() { public String getReadableName() {
@ -284,4 +294,11 @@ public class KeyAdapter extends CursorAdapter {
} }
public static String[] getProjectionWith(String[] projection) {
List<String> list = new ArrayList<>();
list.addAll(Arrays.asList(PROJECTION));
list.addAll(Arrays.asList(projection));
return list.toArray(new String[list.size()]);
}
} }

View File

@ -38,25 +38,16 @@ import org.sufficientlysecure.keychain.util.FabContainer;
public class Notify { public class Notify {
public static enum Style { public static enum Style {
OK, WARN, ERROR; OK (R.color.android_green_light), WARN(R.color.android_orange_light), ERROR(R.color.android_red_light);
public void applyToBar(Snackbar bar) { public final int mLineColor;
switch (this) { Style(int color) {
case OK: mLineColor = color;
// bar.actionColorResource(R.color.android_green_light);
bar.lineColorResource(R.color.android_green_light);
break;
case WARN:
// bar.textColorResource(R.color.android_orange_light);
bar.lineColorResource(R.color.android_orange_light);
break;
case ERROR:
// bar.textColorResource(R.color.android_red_light);
bar.lineColorResource(R.color.android_red_light);
break;
} }
public void applyToBar(Snackbar bar) {
bar.lineColorResource(mLineColor);
} }
} }

View File

@ -17,6 +17,10 @@
package org.sufficientlysecure.keychain.ui.widget; package org.sufficientlysecure.keychain.ui.widget;
import java.util.Arrays;
import java.util.List;
import android.content.Context; import android.content.Context;
import android.database.Cursor; import android.database.Cursor;
import android.net.Uri; import android.net.Uri;
@ -24,14 +28,11 @@ import android.os.Bundle;
import android.support.v4.content.CursorLoader; import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader; import android.support.v4.content.Loader;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.widget.ImageView;
import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.provider.KeychainContract; import org.sufficientlysecure.keychain.provider.KeychainContract;
import org.sufficientlysecure.keychain.provider.KeychainDatabase; import org.sufficientlysecure.keychain.provider.KeychainDatabase;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; import org.sufficientlysecure.keychain.ui.adapter.KeyAdapter;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils.State;
public class CertifyKeySpinner extends KeySpinner { public class CertifyKeySpinner extends KeySpinner {
private long mHiddenMasterKeyId = Constants.key.none; private long mHiddenMasterKeyId = Constants.key.none;
@ -59,19 +60,9 @@ public class CertifyKeySpinner extends KeySpinner {
// sample only has one Loader, so we don't care about the ID. // sample only has one Loader, so we don't care about the ID.
Uri baseUri = KeychainContract.KeyRings.buildUnifiedKeyRingsUri(); Uri baseUri = KeychainContract.KeyRings.buildUnifiedKeyRingsUri();
// These are the rows that we will retrieve. String[] projection = KeyAdapter.getProjectionWith(new String[] {
String[] projection = new String[]{
KeychainContract.KeyRings._ID,
KeychainContract.KeyRings.MASTER_KEY_ID,
KeychainContract.KeyRings.KEY_ID,
KeychainContract.KeyRings.USER_ID,
KeychainContract.KeyRings.IS_REVOKED,
KeychainContract.KeyRings.IS_EXPIRED,
KeychainContract.KeyRings.HAS_CERTIFY, KeychainContract.KeyRings.HAS_CERTIFY,
KeychainContract.KeyRings.HAS_ANY_SECRET, });
KeychainContract.KeyRings.HAS_DUPLICATE_USER_ID,
KeychainContract.KeyRings.CREATION
};
String where = KeychainContract.KeyRings.HAS_ANY_SECRET + " = 1 AND " String where = KeychainContract.KeyRings.HAS_ANY_SECRET + " = 1 AND "
+ KeychainDatabase.Tables.KEYS + "." + KeychainContract.KeyRings.MASTER_KEY_ID + KeychainDatabase.Tables.KEYS + "." + KeychainContract.KeyRings.MASTER_KEY_ID
@ -82,7 +73,7 @@ public class CertifyKeySpinner extends KeySpinner {
return new CursorLoader(getContext(), baseUri, projection, where, null, null); return new CursorLoader(getContext(), baseUri, projection, where, null, null);
} }
private int mIndexHasCertify, mIndexIsRevoked, mIndexIsExpired; private int mIndexHasCertify;
@Override @Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) { public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
@ -90,8 +81,6 @@ public class CertifyKeySpinner extends KeySpinner {
if (loader.getId() == LOADER_ID) { if (loader.getId() == LOADER_ID) {
mIndexHasCertify = data.getColumnIndex(KeychainContract.KeyRings.HAS_CERTIFY); mIndexHasCertify = data.getColumnIndex(KeychainContract.KeyRings.HAS_CERTIFY);
mIndexIsRevoked = data.getColumnIndex(KeychainContract.KeyRings.IS_REVOKED);
mIndexIsExpired = data.getColumnIndex(KeychainContract.KeyRings.IS_EXPIRED);
// If: // If:
// - no key has been pre-selected (e.g. by SageSlinger) // - no key has been pre-selected (e.g. by SageSlinger)
@ -119,18 +108,15 @@ public class CertifyKeySpinner extends KeySpinner {
@Override @Override
boolean setStatus(Context context, Cursor cursor, ImageView statusView) { boolean isItemEnabled(Cursor cursor) {
if (cursor.getInt(mIndexIsRevoked) != 0) { if (cursor.getInt(KeyAdapter.INDEX_IS_REVOKED) != 0) {
KeyFormattingUtils.setStatusImage(getContext(), statusView, null, State.REVOKED, R.color.bg_gray);
return false; return false;
} }
if (cursor.getInt(mIndexIsExpired) != 0) { if (cursor.getInt(KeyAdapter.INDEX_IS_EXPIRED) != 0) {
KeyFormattingUtils.setStatusImage(getContext(), statusView, null, State.EXPIRED, R.color.bg_gray);
return false; return false;
} }
// don't invalidate the "None" entry, which is also null! // don't invalidate the "None" entry, which is also null!
if (cursor.getPosition() != 0 && cursor.isNull(mIndexHasCertify)) { if (cursor.getPosition() != 0 && cursor.isNull(mIndexHasCertify)) {
KeyFormattingUtils.setStatusImage(getContext(), statusView, null, State.UNAVAILABLE, R.color.bg_gray);
return false; return false;
} }

View File

@ -38,6 +38,7 @@ import com.tokenautocomplete.TokenCompleteTextView;
import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.provider.KeychainContract;
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables; import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables;
import org.sufficientlysecure.keychain.ui.adapter.KeyAdapter; import org.sufficientlysecure.keychain.ui.adapter.KeyAdapter;
@ -126,7 +127,13 @@ public class EncryptKeyCompletionView extends TokenCompleteTextView
public Loader<Cursor> onCreateLoader(int id, Bundle args) { public Loader<Cursor> onCreateLoader(int id, Bundle args) {
// These are the rows that we will retrieve. // These are the rows that we will retrieve.
Uri baseUri = KeyRings.buildUnifiedKeyRingsUri(); Uri baseUri = KeyRings.buildUnifiedKeyRingsUri();
String where = KeyRings.HAS_ENCRYPT + " NOT NULL AND " + KeyRings.IS_EXPIRED + " = 0 AND "
String[] projection = KeyAdapter.getProjectionWith(new String[] {
KeychainContract.KeyRings.HAS_ENCRYPT,
});
String where = KeyRings.HAS_ENCRYPT + " NOT NULL AND "
+ KeyRings.IS_EXPIRED + " = 0 AND "
+ Tables.KEYS + "." + KeyRings.IS_REVOKED + " = 0"; + Tables.KEYS + "." + KeyRings.IS_REVOKED + " = 0";
if (args != null && args.containsKey(ARG_QUERY)) { if (args != null && args.containsKey(ARG_QUERY)) {
@ -135,12 +142,12 @@ public class EncryptKeyCompletionView extends TokenCompleteTextView
where += " AND " + KeyRings.USER_ID + " LIKE ?"; where += " AND " + KeyRings.USER_ID + " LIKE ?";
return new CursorLoader(getContext(), baseUri, KeyAdapter.PROJECTION, where, return new CursorLoader(getContext(), baseUri, projection, where,
new String[]{"%" + query + "%"}, null); new String[]{"%" + query + "%"}, null);
} }
mAdapter.setSearchQuery(null); mAdapter.setSearchQuery(null);
return new CursorLoader(getContext(), baseUri, KeyAdapter.PROJECTION, where, null, null); return new CursorLoader(getContext(), baseUri, projection, where, null, null);
} }

View File

@ -17,33 +17,30 @@
package org.sufficientlysecure.keychain.ui.widget; package org.sufficientlysecure.keychain.ui.widget;
import android.annotation.SuppressLint;
import android.content.Context; import android.content.Context;
import android.database.Cursor; import android.database.Cursor;
import android.graphics.Color;
import android.os.Bundle; import android.os.Bundle;
import android.os.Parcelable; import android.os.Parcelable;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.v4.app.FragmentActivity; import android.support.v4.app.FragmentActivity;
import android.support.v4.app.LoaderManager; import android.support.v4.app.LoaderManager;
import android.support.v4.content.Loader; import android.support.v4.content.Loader;
import android.support.v4.widget.CursorAdapter;
import android.support.v7.widget.AppCompatSpinner; import android.support.v7.widget.AppCompatSpinner;
import android.text.format.DateUtils;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.AdapterView; import android.widget.AdapterView;
import android.widget.BaseAdapter; import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.SpinnerAdapter; import android.widget.SpinnerAdapter;
import android.widget.TextView;
import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.pgp.KeyRing;
import org.sufficientlysecure.keychain.provider.KeychainContract; import org.sufficientlysecure.keychain.provider.KeychainContract;
import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.ui.adapter.KeyAdapter;
import org.sufficientlysecure.keychain.ui.adapter.KeyAdapter.KeyItem;
/** /**
* Use AppCompatSpinner from AppCompat lib instead of Spinner. Fixes white dropdown icon. * Use AppCompatSpinner from AppCompat lib instead of Spinner. Fixes white dropdown icon.
@ -119,7 +116,8 @@ public abstract class KeySpinner extends AppCompatSpinner implements
if (getContext() instanceof FragmentActivity) { if (getContext() instanceof FragmentActivity) {
((FragmentActivity) getContext()).getSupportLoaderManager().restartLoader(LOADER_ID, null, this); ((FragmentActivity) getContext()).getSupportLoaderManager().restartLoader(LOADER_ID, null, this);
} else { } else {
Log.e(Constants.TAG, "KeySpinner must be attached to FragmentActivity, this is " + getContext().getClass()); throw new AssertionError("KeySpinner must be attached to FragmentActivity, this is "
+ getContext().getClass());
} }
} }
@ -138,7 +136,11 @@ public abstract class KeySpinner extends AppCompatSpinner implements
} }
public long getSelectedKeyId() { public long getSelectedKeyId() {
return getSelectedItemId(); Object item = getSelectedItem();
if (item instanceof KeyItem) {
return ((KeyItem) item).mKeyId;
}
return Constants.key.none;
} }
public void setPreSelectedKeyId(long selectedKeyId) { public void setPreSelectedKeyId(long selectedKeyId) {
@ -146,90 +148,26 @@ public abstract class KeySpinner extends AppCompatSpinner implements
} }
protected class SelectKeyAdapter extends BaseAdapter implements SpinnerAdapter { protected class SelectKeyAdapter extends BaseAdapter implements SpinnerAdapter {
private CursorAdapter inner; private KeyAdapter inner;
private int mIndexUserId;
private int mIndexDuplicate;
private int mIndexMasterKeyId; private int mIndexMasterKeyId;
private int mIndexCreationDate;
public SelectKeyAdapter() { public SelectKeyAdapter() {
inner = new CursorAdapter(getContext(), null, 0) { inner = new KeyAdapter(getContext(), null, 0) {
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
return View.inflate(getContext(), R.layout.keyspinner_item, null);
}
@Override @Override
public void bindView(View view, Context context, Cursor cursor) { public boolean isEnabled(Cursor cursor) {
TextView vKeyName = (TextView) view.findViewById(R.id.keyspinner_key_name); return KeySpinner.this.isItemEnabled(cursor);
ImageView vKeyStatus = (ImageView) view.findViewById(R.id.keyspinner_key_status);
TextView vKeyEmail = (TextView) view.findViewById(R.id.keyspinner_key_email);
TextView vDuplicate = (TextView) view.findViewById(R.id.keyspinner_duplicate);
KeyRing.UserId userId = KeyRing.splitUserId(cursor.getString(mIndexUserId));
vKeyName.setText(userId.name);
vKeyEmail.setText(userId.email);
boolean duplicate = cursor.getLong(mIndexDuplicate) > 0;
if (duplicate) {
String dateTime = DateUtils.formatDateTime(context,
cursor.getLong(mIndexCreationDate) * 1000,
DateUtils.FORMAT_SHOW_DATE
| DateUtils.FORMAT_SHOW_YEAR
| DateUtils.FORMAT_ABBREV_MONTH);
vDuplicate.setText(context.getString(R.string.label_key_created, dateTime));
vDuplicate.setVisibility(View.VISIBLE);
} else {
vDuplicate.setVisibility(View.GONE);
} }
boolean valid = setStatus(getContext(), cursor, vKeyStatus);
setItemEnabled(view, valid);
}
@Override
public long getItemId(int position) {
try {
return ((Cursor) getItem(position)).getLong(mIndexMasterKeyId);
} catch (Exception e) {
// This can happen on concurrent modification :(
return Constants.key.none;
}
}
}; };
} }
private void setItemEnabled(View view, boolean enabled) {
TextView vKeyName = (TextView) view.findViewById(R.id.keyspinner_key_name);
ImageView vKeyStatus = (ImageView) view.findViewById(R.id.keyspinner_key_status);
TextView vKeyEmail = (TextView) view.findViewById(R.id.keyspinner_key_email);
TextView vKeyDuplicate = (TextView) view.findViewById(R.id.keyspinner_duplicate);
if (enabled) {
vKeyName.setTextColor(Color.BLACK);
vKeyEmail.setTextColor(Color.BLACK);
vKeyDuplicate.setTextColor(Color.BLACK);
vKeyStatus.setVisibility(View.GONE);
view.setClickable(false);
} else {
vKeyName.setTextColor(Color.GRAY);
vKeyEmail.setTextColor(Color.GRAY);
vKeyDuplicate.setTextColor(Color.GRAY);
vKeyStatus.setVisibility(View.VISIBLE);
// this is a HACK. the trick is, if the element itself is clickable, the
// click is not passed on to the view list
view.setClickable(true);
}
}
public Cursor swapCursor(Cursor newCursor) { public Cursor swapCursor(Cursor newCursor) {
if (newCursor == null) return inner.swapCursor(null); if (newCursor == null) return inner.swapCursor(null);
mIndexDuplicate = newCursor.getColumnIndex(KeychainContract.KeyRings.HAS_DUPLICATE_USER_ID);
mIndexUserId = newCursor.getColumnIndex(KeychainContract.KeyRings.USER_ID);
mIndexMasterKeyId = newCursor.getColumnIndex(KeychainContract.KeyRings.MASTER_KEY_ID); mIndexMasterKeyId = newCursor.getColumnIndex(KeychainContract.KeyRings.MASTER_KEY_ID);
mIndexCreationDate = newCursor.getColumnIndex(KeychainContract.KeyRings.CREATION);
Cursor oldCursor = inner.swapCursor(newCursor);
// pre-select key if mPreSelectedKeyId is given // pre-select key if mPreSelectedKeyId is given
if (mPreSelectedKeyId != Constants.key.none && newCursor.moveToFirst()) { if (mPreSelectedKeyId != Constants.key.none && newCursor.moveToFirst()) {
@ -239,7 +177,7 @@ public abstract class KeySpinner extends AppCompatSpinner implements
} }
} while (newCursor.moveToNext()); } while (newCursor.moveToNext());
} }
return inner.swapCursor(newCursor); return oldCursor;
} }
@Override @Override
@ -249,58 +187,48 @@ public abstract class KeySpinner extends AppCompatSpinner implements
@Override @Override
public Object getItem(int position) { public Object getItem(int position) {
if (position == 0) return null; if (position == 0) {
return null;
}
return inner.getItem(position -1); return inner.getItem(position -1);
} }
@Override @Override
public long getItemId(int position) { public long getItemId(int position) {
if (position == 0) return Constants.key.none; if (position == 0) {
return Constants.key.none;
}
return inner.getItemId(position -1); return inner.getItemId(position -1);
} }
@SuppressLint("ViewHolder") // inflate call is for the preview only
@Override @Override
public View getView(int position, View convertView, ViewGroup parent) { public View getView(int position, View convertView, ViewGroup parent) {
try {
View v = getDropDownView(position, convertView, parent); // Unfortunately, SpinnerAdapter does not support multiple view
v.findViewById(R.id.keyspinner_key_email).setVisibility(View.GONE); // types. For this reason, we throw away convertViews of a bad
return v; // type. This is sort of a hack, but since the number of elements
} catch (NullPointerException e) { // we deal with in KeySpinners is usually very small (number of
// This is for the preview... // secret keys), this is the easiest solution. (I'm sorry.)
return View.inflate(getContext(), android.R.layout.simple_list_item_1, null); if (convertView != null) {
// This assumes that the inner view has non-null tags on its views!
boolean isWrongType = (convertView.getTag() == null) != (position == 0);
if (isWrongType) {
convertView = null;
} }
} }
@Override if (position > 0) {
public View getDropDownView(int position, View convertView, ViewGroup parent) { return inner.getView(position -1, convertView, parent);
View view;
if (position == 0) {
if (convertView == null) {
view = inner.newView(null, null, parent);
} else {
view = convertView;
}
TextView vKeyName = (TextView) view.findViewById(R.id.keyspinner_key_name);
ImageView vKeyStatus = (ImageView) view.findViewById(R.id.keyspinner_key_status);
TextView vKeyEmail = (TextView) view.findViewById(R.id.keyspinner_key_email);
TextView vKeyDuplicate = (TextView) view.findViewById(R.id.keyspinner_duplicate);
vKeyName.setText(R.string.choice_none);
vKeyEmail.setVisibility(View.GONE);
vKeyDuplicate.setVisibility(View.GONE);
vKeyStatus.setVisibility(View.GONE);
setItemEnabled(view, true);
} else {
view = inner.getView(position - 1, convertView, parent);
TextView vKeyEmail = (TextView) view.findViewById(R.id.keyspinner_key_email);
vKeyEmail.setVisibility(View.VISIBLE);
}
return view;
}
} }
boolean setStatus(Context context, Cursor cursor, ImageView statusView) { return convertView != null ? convertView :
LayoutInflater.from(getContext()).inflate(
R.layout.keyspinner_item_none, parent, false);
}
}
boolean isItemEnabled(Cursor cursor) {
return true; return true;
} }

View File

@ -17,6 +17,7 @@
package org.sufficientlysecure.keychain.ui.widget; package org.sufficientlysecure.keychain.ui.widget;
import android.content.Context; import android.content.Context;
import android.database.Cursor; import android.database.Cursor;
import android.net.Uri; import android.net.Uri;
@ -24,12 +25,9 @@ import android.os.Bundle;
import android.support.v4.content.CursorLoader; import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader; import android.support.v4.content.Loader;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.widget.ImageView;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.provider.KeychainContract; import org.sufficientlysecure.keychain.provider.KeychainContract;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; import org.sufficientlysecure.keychain.ui.adapter.KeyAdapter;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils.State;
public class SignKeySpinner extends KeySpinner { public class SignKeySpinner extends KeySpinner {
public SignKeySpinner(Context context) { public SignKeySpinner(Context context) {
@ -50,19 +48,9 @@ public class SignKeySpinner extends KeySpinner {
// sample only has one Loader, so we don't care about the ID. // sample only has one Loader, so we don't care about the ID.
Uri baseUri = KeychainContract.KeyRings.buildUnifiedKeyRingsUri(); Uri baseUri = KeychainContract.KeyRings.buildUnifiedKeyRingsUri();
// These are the rows that we will retrieve. String[] projection = KeyAdapter.getProjectionWith(new String[] {
String[] projection = new String[]{
KeychainContract.KeyRings._ID,
KeychainContract.KeyRings.MASTER_KEY_ID,
KeychainContract.KeyRings.KEY_ID,
KeychainContract.KeyRings.USER_ID,
KeychainContract.KeyRings.IS_REVOKED,
KeychainContract.KeyRings.IS_EXPIRED,
KeychainContract.KeyRings.HAS_SIGN, KeychainContract.KeyRings.HAS_SIGN,
KeychainContract.KeyRings.HAS_ANY_SECRET, });
KeychainContract.KeyRings.HAS_DUPLICATE_USER_ID,
KeychainContract.KeyRings.CREATION
};
String where = KeychainContract.KeyRings.HAS_ANY_SECRET + " = 1"; String where = KeychainContract.KeyRings.HAS_ANY_SECRET + " = 1";
@ -71,7 +59,7 @@ public class SignKeySpinner extends KeySpinner {
return new CursorLoader(getContext(), baseUri, projection, where, null, null); return new CursorLoader(getContext(), baseUri, projection, where, null, null);
} }
private int mIndexHasSign, mIndexIsRevoked, mIndexIsExpired; private int mIndexHasSign;
@Override @Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) { public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
@ -79,23 +67,18 @@ public class SignKeySpinner extends KeySpinner {
if (loader.getId() == LOADER_ID) { if (loader.getId() == LOADER_ID) {
mIndexHasSign = data.getColumnIndex(KeychainContract.KeyRings.HAS_SIGN); mIndexHasSign = data.getColumnIndex(KeychainContract.KeyRings.HAS_SIGN);
mIndexIsRevoked = data.getColumnIndex(KeychainContract.KeyRings.IS_REVOKED);
mIndexIsExpired = data.getColumnIndex(KeychainContract.KeyRings.IS_EXPIRED);
} }
} }
@Override @Override
boolean setStatus(Context context, Cursor cursor, ImageView statusView) { boolean isItemEnabled(Cursor cursor) {
if (cursor.getInt(mIndexIsRevoked) != 0) { if (cursor.getInt(KeyAdapter.INDEX_IS_REVOKED) != 0) {
KeyFormattingUtils.setStatusImage(getContext(), statusView, null, State.REVOKED, R.color.bg_gray);
return false; return false;
} }
if (cursor.getInt(mIndexIsExpired) != 0) { if (cursor.getInt(KeyAdapter.INDEX_IS_EXPIRED) != 0) {
KeyFormattingUtils.setStatusImage(getContext(), statusView, null, State.EXPIRED, R.color.bg_gray);
return false; return false;
} }
if (cursor.getInt(mIndexHasSign) == 0) { if (cursor.getInt(mIndexHasSign) == 0) {
KeyFormattingUtils.setStatusImage(getContext(), statusView, null, State.UNAVAILABLE, R.color.bg_gray);
return false; return false;
} }

View File

@ -0,0 +1,61 @@
/*
* Copyright (C) 2015 Vincent Breitmoser <look@my.amazin.horse>
*
* The MIT License (MIT)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.sufficientlysecure.keychain.ui.widget;
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ViewAnimator;
import org.sufficientlysecure.keychain.R;
/** This view is essentially identical to ViewAnimator, but allows specifying the initial view
* for preview as an xml attribute. */
public class ToolableViewAnimator extends ViewAnimator {
private int mInitChild = -1;
public ToolableViewAnimator(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs);
if (isInEditMode()) {
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ToolableViewAnimator, defStyleAttr, 0);
mInitChild = a.getInt(R.styleable.ToolableViewAnimator_initialView, -1);
a.recycle();
}
}
@Override
public void addView(View child, int index, ViewGroup.LayoutParams params) {
if (isInEditMode() && mInitChild-- > 0) {
return;
}
super.addView(child, index, params);
}
}

View File

@ -34,7 +34,8 @@
android:inputType="textPassword" android:inputType="textPassword"
android:hint="@string/label_passphrase" android:hint="@string/label_passphrase"
android:ems="10" android:ems="10"
android:layout_gravity="center_horizontal" /> android:layout_gravity="center_horizontal"
/>
<EditText <EditText
android:id="@+id/create_key_passphrase_again" android:id="@+id/create_key_passphrase_again"

View File

@ -1,11 +1,13 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical"> android:orientation="vertical">
<LinearLayout <LinearLayout
android:visibility="gone" android:visibility="gone"
tools:visibility="visible"
android:id="@+id/decrypt_content" android:id="@+id/decrypt_content"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
@ -28,6 +30,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="top" android:gravity="top"
android:hint="" android:hint=""
tools:text="This is the plaintext"
android:textIsSelectable="true" /> android:textIsSelectable="true" />
</ScrollView> </ScrollView>

View File

@ -1,57 +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="wrap_content"
android:gravity="center_vertical"
android:singleLine="true"
android:orientation="horizontal"
android:descendantFocusability="blocksDescendants"
android:focusable="false"
android:minHeight="44dip">
<LinearLayout
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="1"
android:focusable="true"
android:orientation="vertical"
android:paddingLeft="8dp"
android:paddingRight="4dp">
<TextView
android:id="@+id/keyspinner_key_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/label_main_user_id"
android:textAppearance="?android:attr/textAppearanceMedium" />
<TextView
android:id="@+id/keyspinner_key_email"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="true"
android:ellipsize="end"
android:text="user@example.com"
android:textAppearance="?android:attr/textAppearanceSmall" />
<TextView
android:id="@+id/keyspinner_duplicate"
android:text="creation: bla"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="true"
android:ellipsize="end"
android:textAppearance="?android:attr/textAppearanceSmall" />
</LinearLayout>
<ImageView
android:id="@+id/keyspinner_key_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:src="@drawable/status_signature_revoked_cutout_24dp"
android:paddingLeft="16dp"
android:paddingRight="16dp" />
</LinearLayout>

View File

@ -0,0 +1,19 @@
<?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="wrap_content"
android:gravity="center_vertical"
android:singleLine="true"
android:orientation="horizontal"
android:minHeight="44dip"
android:paddingLeft="8dp"
android:paddingRight="4dp">
<TextView
android:id="@+id/keyspinner_key_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/choice_none"
android:textAppearance="?android:attr/textAppearanceMedium" />
</LinearLayout>

View File

@ -60,6 +60,7 @@
android:layout_marginRight="48dp" android:layout_marginRight="48dp"
android:layout_marginEnd="48dp" android:layout_marginEnd="48dp"
android:text="" android:text=""
tools:text="Alice Skywalker"
android:textColor="@color/icons" android:textColor="@color/icons"
android:textAppearance="?android:attr/textAppearanceLarge" android:textAppearance="?android:attr/textAppearanceLarge"
android:layout_above="@+id/view_key_status" /> android:layout_above="@+id/view_key_status" />
@ -73,6 +74,7 @@
android:layout_marginRight="48dp" android:layout_marginRight="48dp"
android:layout_marginEnd="48dp" android:layout_marginEnd="48dp"
android:text="" android:text=""
tools:text="My Key"
android:textColor="@color/tab_text" android:textColor="@color/tab_text"
android:textAppearance="?android:attr/textAppearanceSmall" android:textAppearance="?android:attr/textAppearanceSmall"
android:layout_above="@+id/toolbar2" /> android:layout_above="@+id/toolbar2" />
@ -95,6 +97,7 @@
android:layout_width="64dp" android:layout_width="64dp"
android:layout_height="64dp" android:layout_height="64dp"
android:visibility="invisible" android:visibility="invisible"
tools:visibility="visible"
style="?android:attr/borderlessButtonStyle" style="?android:attr/borderlessButtonStyle"
android:src="@drawable/ic_action_encrypt_file_24dp" /> android:src="@drawable/ic_action_encrypt_file_24dp" />
@ -103,6 +106,7 @@
android:layout_width="64dp" android:layout_width="64dp"
android:layout_height="64dp" android:layout_height="64dp"
android:visibility="invisible" android:visibility="invisible"
tools:visibility="visible"
style="?android:attr/borderlessButtonStyle" style="?android:attr/borderlessButtonStyle"
android:src="@drawable/ic_action_encrypt_text_24dp" /> android:src="@drawable/ic_action_encrypt_text_24dp" />
@ -111,6 +115,7 @@
android:layout_width="64dp" android:layout_width="64dp"
android:layout_height="64dp" android:layout_height="64dp"
android:visibility="invisible" android:visibility="invisible"
tools:visibility="visible"
style="?android:attr/borderlessButtonStyle" style="?android:attr/borderlessButtonStyle"
android:src="@drawable/ic_nfc_white_24dp" /> android:src="@drawable/ic_nfc_white_24dp" />
@ -120,6 +125,7 @@
android:id="@+id/view_key_status_image" android:id="@+id/view_key_status_image"
android:layout_width="96dp" android:layout_width="96dp"
android:visibility="invisible" android:visibility="invisible"
tools:visibility="visible"
android:src="@drawable/status_signature_unverified_cutout_96dp" android:src="@drawable/status_signature_unverified_cutout_96dp"
android:layout_height="96dp" android:layout_height="96dp"
android:layout_above="@id/toolbar2" android:layout_above="@id/toolbar2"
@ -139,6 +145,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:clickable="true" android:clickable="true"
android:foreground="?android:attr/selectableItemBackground" android:foreground="?android:attr/selectableItemBackground"
tools:visibility="invisible"
card_view:cardBackgroundColor="@android:color/white" card_view:cardBackgroundColor="@android:color/white"
card_view:cardElevation="2dp" card_view:cardElevation="2dp"
card_view:cardUseCompatPadding="true" card_view:cardUseCompatPadding="true"
@ -147,7 +154,8 @@
<ImageView <ImageView
android:id="@+id/view_key_qr_code" android:id="@+id/view_key_qr_code"
android:layout_width="96dp" android:layout_width="96dp"
android:layout_height="96dp" /> android:layout_height="96dp"
/>
</android.support.v7.widget.CardView> </android.support.v7.widget.CardView>
</RelativeLayout> </RelativeLayout>
@ -189,6 +197,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:visibility="invisible" android:visibility="invisible"
tools:visibility="visible"
android:elevation="4dp" android:elevation="4dp"
fab:fab_icon="@drawable/ic_qrcode_white_24dp" fab:fab_icon="@drawable/ic_qrcode_white_24dp"
fab:fab_colorNormal="@color/fab" fab:fab_colorNormal="@color/fab"

View File

@ -1,6 +1,10 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<declare-styleable name="ToolableViewAnimator">
<attr name="initialView" format="integer" />
</declare-styleable>
<declare-styleable name="FoldableLinearLayout"> <declare-styleable name="FoldableLinearLayout">
<attr name="foldedLabel" format="string" /> <attr name="foldedLabel" format="string" />
<attr name="unFoldedLabel" format="string" /> <attr name="unFoldedLabel" format="string" />

View File

@ -11,6 +11,7 @@ buildscript {
classpath 'com.novoda:bintray-release:0.2.7' classpath 'com.novoda:bintray-release:0.2.7'
classpath 'org.kt3k.gradle.plugin:coveralls-gradle-plugin:2.0.1' classpath 'org.kt3k.gradle.plugin:coveralls-gradle-plugin:2.0.1'
// classpath 'com.stanfy.spoon:spoon-gradle-plugin:1.0.2'
} }
} }

25
tools/android-wait-for-emulator Executable file
View File

@ -0,0 +1,25 @@
#!/bin/bash
# Originally written by Ralf Kistner <ralf@embarkmobile.com>, but placed in the public domain
set +e
bootanim=""
failcounter=0
timeout_in_sec=720
until [[ "$bootanim" =~ "stopped" ]]; do
bootanim=`adb -e shell getprop init.svc.bootanim 2>&1 &`
if [[ "$bootanim" =~ "device not found" || "$bootanim" =~ "device offline"
|| "$bootanim" =~ "running" ]]; then
let "failcounter += 1"
echo "Waiting for emulator to start"
if [[ $failcounter -gt timeout_in_sec ]]; then
echo "Timeout ($timeout_in_sec seconds) reached; failed to start emulator"
exit 1
fi
fi
sleep 10
done
echo "Emulator is ready"