diff --git a/OpenKeychain/build.gradle b/OpenKeychain/build.gradle index 3e92222c4..36d4afdb9 100644 --- a/OpenKeychain/build.gradle +++ b/OpenKeychain/build.gradle @@ -1,6 +1,7 @@ apply plugin: 'com.android.application' apply plugin: 'witness' apply plugin: 'jacoco' +apply plugin: 'com.squareup.sqldelight' // apply plugin: 'com.github.kt3k.coveralls' dependencies { @@ -8,12 +9,12 @@ dependencies { // NOTE: libraries are pinned to a specific build, see below // from local Android SDK - compile 'com.android.support:support-v4:27.0.2' - compile 'com.android.support:appcompat-v7:27.0.2' - compile 'com.android.support:design:27.0.2' - compile 'com.android.support:recyclerview-v7:27.0.2' - compile 'com.android.support:cardview-v7:27.0.2' - compile 'com.android.support:support-annotations:27.0.2' + compile 'com.android.support:support-v4:27.1.1' + compile 'com.android.support:appcompat-v7:27.1.1' + compile 'com.android.support:design:27.1.1' + compile 'com.android.support:recyclerview-v7:27.1.1' + compile 'com.android.support:cardview-v7:27.1.1' + compile 'com.android.support:support-annotations:27.1.1' // JCenter etc. compile 'com.journeyapps:zxing-android-embedded:3.4.0' @@ -27,15 +28,15 @@ dependencies { // UI compile 'org.sufficientlysecure:html-textview:3.1' - compile 'com.splitwise:tokenautocomplete:2.0.8@aar' compile 'com.jpardogo.materialtabstrip:library:1.1.1' compile 'com.getbase:floatingactionbutton:1.10.1' compile 'com.nispok:snackbar:2.11.0' compile 'com.cocosw:bottomsheet:1.3.1@aar' // RecyclerView - compile 'com.tonicartos:superslim:0.4.13' - compile 'com.futuremind.recyclerfastscroll:fastscroll:0.2.4' + compile 'eu.davidea:flexible-adapter:5.0.5' + compile 'eu.davidea:flexible-adapter-ui:1.0.0-b5' + compile 'eu.davidea:flexible-adapter-livedata:1.0.0-b2' // Material Drawer compile 'com.mikepenz:materialdrawer:5.6.0@aar' @@ -59,6 +60,9 @@ dependencies { implementation project(':extern:minidns') implementation project(':KeybaseLib') implementation project(':safeslinger-exchange') + implementation project(':extern:MaterialChipsInput') + + implementation "android.arch.work:work-runtime:1.0.0-alpha02" // Unit tests in the local JVM with Robolectric // https://developer.android.com/training/testing/unit-testing/local-unit-tests.html @@ -73,9 +77,9 @@ dependencies { // UI testing with Espresso // Force usage of support libs in the test app, since they are internally used by the runner module. // https://github.com/googlesamples/android-testing/blob/master/ui/espresso/BasicSample/app/build.gradle#L28 - androidTestCompile 'com.android.support:support-annotations:27.0.2' - androidTestCompile 'com.android.support:appcompat-v7:27.0.2' - androidTestCompile 'com.android.support:design:27.0.2' + androidTestCompile 'com.android.support:support-annotations:27.1.1' + androidTestCompile 'com.android.support:appcompat-v7:27.1.1' + androidTestCompile 'com.android.support:design:27.1.1' androidTestCompile 'com.android.support.test:runner:0.5' androidTestCompile 'com.android.support.test:rules:0.5' androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.2' @@ -96,62 +100,69 @@ dependencies { compile "android.arch.lifecycle:extensions:1.0.0" annotationProcessor "android.arch.lifecycle:compiler:1.0.0" + + compile "android.arch.persistence:db-framework:1.0.0" + + // for debugging the db. don't enable by default, this will expose the database no the network! + // debugImplementation 'com.amitshekhar.android:debug-db:1.0.3' } // Output of ./gradlew -q calculateChecksums // Comment out the libs referenced as git submodules! dependencyVerification { verify = [ - 'com.android.support:design:fa5c27a705310e95a8f4099c98777132ed901a0d69178942306bb34cd76f0d57', + 'eu.davidea:flexible-adapter-ui:7ed5327d15c823e5fcf7d6e1017d8a47d079d1adc7141858f3cb427517ef35cd', + 'com.android.support:design:7225973f7ee03765008a9c2f17a40b154c6885169fef022276e811c926a2202c', 'com.journeyapps:zxing-android-embedded:2422d83c2c09a7b645f516c8458ececba6a7da47b94e40778d876facf495c660', 'org.sufficientlysecure:donations:2be4183afa5e35263e37346344cfea48681f3c987e6832dd4acde227c13ccad6', - 'com.android.support:support-v4:1b2b37169fcccfef5e563d273749e3792decdce9818bc17932403a2363f537b4', - 'com.futuremind.recyclerfastscroll:fastscroll:ae655201885a9dbb5fabecb4adfefbb23ffdbca26a2b4ea255ec1bf6f214c606', + 'com.android.support:support-v4:4f41dfc3e89f2738e45c86264a85c0934d055ee8ebe2020e23c97f303b80a48b', 'com.mikepenz:fastadapter:21d4ecb5c128bcda37b14e7998d799ed52cfc768b72cdf3d5578bb6775769ebd', 'com.mikepenz:materialize:942ccf5e2aa1a46803aa884e8dc7bbaf2a9e8e9996a0cf92e3fe2f44a8592ba4', - 'com.android.support:appcompat-v7:b2825e8b47f665d3362d8481c8d147d1af9230d16f23a2b94f6ccbc53c68cec1', + 'com.android.support:appcompat-v7:0c7808fbbc5838d831e32e3c0a6f84e1f2c981deb8f11e010650f2b57923a335', 'com.nispok:snackbar:46b5eb9d630d329e13c2ce00ee9fb115ffb66c23c72cff32ee97eedd76824c6f', - 'com.tonicartos:superslim:ca89b5c674660cc6918a8f8fd385065bffeee27983e0d33c7c2f0ad7b34d2d49', - 'com.android.support:recyclerview-v7:3eb953930f10941f2b0447ec123a9b03d2746a42a99c523e82c3ece3308ca70b', - 'com.android.support:cardview-v7:57f867a3c8f33e2d4dc0a03e2dfa03cad6267a908179f04a725a68ea9f0b8ccf', + 'eu.davidea:flexible-adapter:560e940e8cf0f4ed8f632f5f89527deeda7a61cce5f02f42cc0983f7c0d2de5f', + 'com.android.support:recyclerview-v7:d735e4727878e99ef3980c10d15dc3468462fd509d4fb60cb8bd20b0f735085c', + 'com.android.support:cardview-v7:8ed955dd037d82a7b4bbcaedb4f896523c3e4c1bf3ca698ce807c350767a2886', 'org.sufficientlysecure:html-textview:ed740adf05cae2373999c7a3047c803183d9807b2cf66162902090d7c112a832', 'com.getbase:floatingactionbutton:3edefa511aac4d90794c7b0496aca59cff2eee1e32679247b4f85acbeee05240', - 'com.android.support:transition:1a7db0453c1467fc8fd815e6d50ca6bb475a7a9ba6b5f3b307329688a7c62a68', - 'com.android.support:support-media-compat:6dd9327ee9aa467cab479aad97df375072b2b6ba61eadffdaa5a88de3843c457', - 'com.android.support:support-fragment:e4358388022a2205777575a7251fe357334658e4123d5d6e3b082f5899d9b011', - 'com.android.support:support-core-utils:b69c6e1e7731b876b910fc7100bcadf40a57f27b32ca26b91400995542112c96', + 'android.arch.persistence:db-framework:e8310c66979f8823cfe583951abfde96824b176289ba77b750a25be00d25981a', + 'com.android.support:support-media-compat:55e9837dda88b74a8c812c63a78c63fd83c6c039a8c22d318492663a493585eb', 'com.jpardogo.materialtabstrip:library:4ee2f1211c302b45fb8c627cc5b240dc6b38b7aaaab1b8bffc81663e1b108013', - 'com.android.support:animated-vector-drawable:5b117a2c13a898c2a3c84c480d64edcfac2ef720aa9b742c29249fac774ffc48', - 'com.android.support:support-core-ui:2284072511a95d504c074de80c82cd33724c6d2754117833b98ba3a09994163e', - 'com.android.support:support-vector-drawable:bf4f4fcbf58b1380616581224e6487c230bfdb3434ec353d4adaa4b1f4865cfa', - 'com.android.support:support-compat:ed4d25d91a0b13d8b9def1c0de69ed03d7fb89d50fb37eb0e9b63b0cf7a42357', - 'com.android.support:support-annotations:af05330d997eb92a066534dbe0a3ea24347d26d7001221092113ae02a8f233da', + 'android.arch.lifecycle:extensions:851f718fd2afda1e7aa93537dae1a5c1fe47710db62dcd7cd24c4b3b14ef0d90', + 'com.android.support:support-fragment:ec72d6ac36a1a0e6523bbddba33d73ffad070b9b3dd246cc44d8727a41ddb5e6', + 'com.android.support:animated-vector-drawable:59670473f6e98fda792f7bef25dd7292b0a3106031c7a5e30eb020bf26f077bd', + 'com.android.support:support-core-ui:a3ae20e6d5dffba69ac97b99846d2738003af8563843d5f3c9dc4c35b4804241', + 'com.android.support:support-core-utils:61036832c54e8701aae954fc3bf96d1d80bf8d9dd531bff77d72def456ba087a', + 'com.android.support:support-vector-drawable:1c0f421114cf4627cf208776d6eb4f76340c78b7e96fe6e12b3e6eb950caf1b9', + 'com.android.support:transition:c0765b2f3c78696567ec5b3f519d22da1e3df11ac994625adf4bb4dc571caacc', + 'com.android.support:support-compat:880ce01ff5be42b233ff8ec0c61cefb7dc3dc9500fea9e24423214813ac27ea2', + 'android.arch.persistence:db:7c0a51d5fc890a8fb94a3370ff599243ec3485cca63daba3cc2bb197835dc521', + 'android.arch.lifecycle:runtime:094fd793924dd6a5136753e599ac8174a8147f4a401386b694ba7d818c223e2e', + 'android.arch.lifecycle:livedata-core:14e57ff8ffb65a80c7e72d91f2076acccdaf2970f234c6261e03a6127eb5206b', + 'android.arch.lifecycle:common:614e31cfd33255dc4d5f5d8e62cfa6be2fbbc2a35643a79dc3ed008004c30807', + 'android.arch.core:runtime:83400f7575bcfb8a2eeec64e05590f037bfaed1e56aa3a4214d20e55878445e3', + 'android.arch.core:common:d34824b794bc92ff8f647a9bb13a7c73de920de5b47075b5d2c4f0770e9b8bfd', + 'com.android.support:support-annotations:3365960206c3d2b09e845f555e7f88f8effc8d2f00b369e66c4be384029299cf', 'com.google.zxing:core:bba7724e02a997cec38213af77133ee8e24b0d5cf5fa7ecbc16a4fa93f11ee0d', 'org.commonjava.googlecode.markdown4j:markdown4j:28eb991f702c6d85d6cafd68c24d1ce841d1f5c995c943f25aedb433c0c13f60', 'com.squareup.okhttp3:okhttp-urlconnection:16a410e5c4457ab381759486df6f840fdc7cc426d67433d4da1b7d65ed2b3b33', 'com.squareup.okhttp3:okhttp:a0d01017a42bba26e507fc6d448bb36e536f4b6e612f7c42de30bbdac2b7785e', 'org.apache.james:apache-mime4j-dom:e18717fe6d36f32e5c5f7cbeea1a9bf04645fdabc84e7e8374d9da10fd52e78d', 'org.apache.james:apache-mime4j-core:561987f604911e1870b2b4eabf0b0658d666c66cb1e65fba3e9e4bffe63acab9', - 'com.splitwise:tokenautocomplete:f921f83ee26b5265f719b312c30452ef8e219557826c5ce5bf02e29647967939', 'com.cocosw:bottomsheet:85bd91fd837b02ebd7a888501cb26035c7cd985a6aa87303fca249da8231a2c3', + 'eu.davidea:flexible-adapter-livedata:c8718b46ff4fbf290ea18f0c5bfe8326badeadf5fd95899a1404c561a24f48a1', 'com.mikepenz:materialdrawer:8bba1428dcef5ad7c2decf49c612ad980b38e2f1031cbd66c152a8a104793929', 'com.mikepenz:iconics-core:478d7e245098f7c28b5b20a0e6b1e5cb108ef3eaf595af7190bc60f91063aa3d', 'com.mikepenz:google-material-typeface:f27c629ba5d2a90ecfbd7f221ff98cd363e1ee6be06b099b82bae490766e14a5', 'com.mikepenz:fontawesome-typeface:ee47b7fe97b90412f01f2fcdd78f65a4edb0ab00006f5ef59ed00516baca9309', 'com.mikepenz:community-material-typeface:d6035d261c5eba880cd7fe5dcb8cc00b09bfe6d41063b881b759e9897dc7b7c9', 'com.fidesmo:nordpol-android:9a992eca347ff7af6e99ff48078954b44b26f26fdc5463139e340234757a24f7', + 'com.jakewharton.timber:timber:d553d3d3e883ce7d061f1b21b95d6ee0840f3bfbf6d3bd51c5671f0b0f0b0091', 'org.glassfish:javax.annotation:339c876b928766329cc0657920366e75beb25f932b80bb3b26df6c0e687a9582', 'com.ryanharter.auto.value:auto-value-parcel-adapter:f730534497f7de81f62f1165df65e750522fdaedabd56031ee1c2d9da2544e17', 'com.squareup.okio:okio:734269c3ebc5090e3b23566db558f421f0b4027277c79ad5d176b8ec168bb850', 'com.fidesmo:nordpol-core:296e71b12884a9cd28cf00ab908973bbf776a90be1f23ac897380d91604e614d', - 'com.jakewharton.timber:timber:d553d3d3e883ce7d061f1b21b95d6ee0840f3bfbf6d3bd51c5671f0b0f0b0091', - 'android.arch.lifecycle:runtime:d0b36278878c82b838acc4308595bec61a3b5f6e7f2acc34172d7e071b2cf26d', - 'android.arch.lifecycle:common:ff0215b54e7cbaaa898f8fd00e265ed6ea198859e10604bc1c5e78477df48b5c', - 'android.arch.core:common:5192934cd73df32e2c15722ed7fc488dde90baaec9ae030010dd1a80fb4e74e1', - 'android.arch.lifecycle:runtime:d0b36278878c82b838acc4308595bec61a3b5f6e7f2acc34172d7e071b2cf26d', - 'android.arch.core:runtime:9e08fc5c4d6e48f58c6865b55ba0e72a88f907009407767274187a873e524734', - 'android.arch.core:common:5192934cd73df32e2c15722ed7fc488dde90baaec9ae030010dd1a80fb4e74e1', - 'android.arch.lifecycle:common:ff0215b54e7cbaaa898f8fd00e265ed6ea198859e10604bc1c5e78477df48b5c', + 'android.arch.lifecycle:viewmodel:6407c93a5ea9850661dca42a0068d6f3deccefd7228ee69bae1c35d70cbc2557', ] } diff --git a/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/TestHelpers.java b/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/AndroidTestHelpers.java similarity index 96% rename from OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/TestHelpers.java rename to OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/AndroidTestHelpers.java index 5a6509080..a29479bf2 100644 --- a/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/TestHelpers.java +++ b/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/AndroidTestHelpers.java @@ -38,8 +38,7 @@ import com.nispok.snackbar.Snackbar; import org.hamcrest.Matcher; import org.sufficientlysecure.keychain.pgp.UncachedKeyRing; import org.sufficientlysecure.keychain.pgp.UncachedKeyRing.IteratorWithIOThrow; -import org.sufficientlysecure.keychain.provider.KeyWritableRepository; -import org.sufficientlysecure.keychain.provider.KeychainDatabase; +import org.sufficientlysecure.keychain.daos.KeyWritableRepository; import org.sufficientlysecure.keychain.service.PassphraseCacheService; import org.sufficientlysecure.keychain.ui.util.Notify.Style; @@ -53,7 +52,7 @@ import static org.hamcrest.CoreMatchers.endsWith; import static org.sufficientlysecure.keychain.matcher.CustomMatchers.withSnackbarLineColor; -public class TestHelpers { +public class AndroidTestHelpers { public static void dismissSnackbar() { onView(withClassName(endsWith("Snackbar"))) @@ -152,7 +151,7 @@ public class TestHelpers { public static void cleanupForTests(Context context) throws Exception { - new KeychainDatabase(context).clearDatabase(); + KeychainDatabase.getInstance(context).clearDatabase(); // import these two, make sure they're there importKeysFromResource(context, "x.sec.asc"); @@ -161,5 +160,4 @@ public class TestHelpers { PassphraseCacheService.clearCachedPassphrases(context); } - } diff --git a/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/actions/CustomActions.java b/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/actions/CustomActions.java index 14fb14e35..65d39edb8 100644 --- a/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/actions/CustomActions.java +++ b/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/actions/CustomActions.java @@ -27,8 +27,7 @@ import android.view.View; import com.tokenautocomplete.TokenCompleteTextView; import org.hamcrest.Matcher; import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKeyRing; -import org.sufficientlysecure.keychain.provider.KeyWritableRepository; -import org.sufficientlysecure.keychain.ui.adapter.KeyAdapter; +import org.sufficientlysecure.keychain.daos.KeyWritableRepository; import static android.support.test.InstrumentationRegistry.getTargetContext; diff --git a/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/matcher/CustomMatchers.java b/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/matcher/CustomMatchers.java index c408c2266..fd44c4ad1 100644 --- a/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/matcher/CustomMatchers.java +++ b/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/matcher/CustomMatchers.java @@ -28,13 +28,9 @@ import android.widget.ViewAnimator; import com.nispok.snackbar.Snackbar; -import org.hamcrest.BaseMatcher; import org.hamcrest.Description; import org.hamcrest.Matcher; import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.ui.adapter.KeyAdapter.KeyItem; -import org.sufficientlysecure.keychain.ui.adapter.KeySectionedListAdapter; -import org.sufficientlysecure.keychain.ui.widget.EncryptKeyCompletionView; import static android.support.test.espresso.matcher.ViewMatchers.hasDescendant; import static android.support.test.espresso.matcher.ViewMatchers.isAssignableFrom; @@ -90,15 +86,14 @@ public abstract class CustomMatchers { } public static Matcher withKeyHolderId(final long keyId) { - return new BoundedMatcher - (KeySectionedListAdapter.KeyItemViewHolder.class) { + return new BoundedMatcher(RecyclerView.ViewHolder.class) { @Override public void describeTo(Description description) { description.appendText("with ViewHolder id: " + keyId); } @Override - protected boolean matchesSafely(KeySectionedListAdapter.KeyItemViewHolder item) { + protected boolean matchesSafely(View item) { return item.getItemId() == keyId; } }; diff --git a/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/remote/OpenPgpServiceTest.java b/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/remote/OpenPgpServiceTest.java index 1e84ea879..ccd83a777 100644 --- a/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/remote/OpenPgpServiceTest.java +++ b/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/remote/OpenPgpServiceTest.java @@ -9,7 +9,6 @@ import android.support.test.InstrumentationRegistry; import android.support.test.rule.ServiceTestRule; import android.support.test.runner.AndroidJUnit4; import android.test.suitebuilder.annotation.LargeTest; -import android.widget.AdapterView; import org.junit.Before; import org.junit.Rule; @@ -23,17 +22,14 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import static android.support.test.espresso.Espresso.closeSoftKeyboard; -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.matcher.ViewMatchers.isAssignableFrom; import static android.support.test.espresso.matcher.ViewMatchers.withId; import static android.support.test.espresso.matcher.ViewMatchers.withText; import static org.hamcrest.core.Is.is; import static org.junit.Assert.assertThat; -import static org.sufficientlysecure.keychain.TestHelpers.cleanupForTests; -import static org.sufficientlysecure.keychain.matcher.CustomMatchers.withKeyItemId; +import static org.sufficientlysecure.keychain.AndroidTestHelpers.cleanupForTests; @RunWith(AndroidJUnit4.class) diff --git a/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/ui/AsymmetricFileOperationTests.java b/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/ui/AsymmetricFileOperationTests.java index 0cd3681dd..0930ef7c4 100644 --- a/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/ui/AsymmetricFileOperationTests.java +++ b/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/ui/AsymmetricFileOperationTests.java @@ -32,7 +32,7 @@ import android.widget.AdapterView; import org.junit.Before; import org.junit.Rule; import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.TestHelpers; +import org.sufficientlysecure.keychain.AndroidTestHelpers; import org.sufficientlysecure.keychain.service.PassphraseCacheService; import org.sufficientlysecure.keychain.ui.util.Notify.Style; @@ -61,11 +61,11 @@ import static android.support.test.espresso.matcher.ViewMatchers.withText; import static org.hamcrest.CoreMatchers.allOf; import static org.hamcrest.CoreMatchers.hasItem; import static org.hamcrest.CoreMatchers.is; -import static org.sufficientlysecure.keychain.TestHelpers.checkSnackbar; -import static org.sufficientlysecure.keychain.TestHelpers.getImageNames; -import static org.sufficientlysecure.keychain.TestHelpers.importKeysFromResource; -import static org.sufficientlysecure.keychain.TestHelpers.pickRandom; -import static org.sufficientlysecure.keychain.TestHelpers.randomString; +import static org.sufficientlysecure.keychain.AndroidTestHelpers.checkSnackbar; +import static org.sufficientlysecure.keychain.AndroidTestHelpers.getImageNames; +import static org.sufficientlysecure.keychain.AndroidTestHelpers.importKeysFromResource; +import static org.sufficientlysecure.keychain.AndroidTestHelpers.pickRandom; +import static org.sufficientlysecure.keychain.AndroidTestHelpers.randomString; import static org.sufficientlysecure.keychain.actions.CustomActions.tokenEncryptViewAddToken; import static org.sufficientlysecure.keychain.matcher.CustomMatchers.isRecyclerItemView; import static org.sufficientlysecure.keychain.matcher.CustomMatchers.withDisplayedChild; @@ -95,7 +95,7 @@ public class AsymmetricFileOperationTests { public void setUp() throws Exception { Activity activity = mActivity.getActivity(); - TestHelpers.copyFiles(); + AndroidTestHelpers.copyFiles(); // import these two, make sure they're there importKeysFromResource(activity, "x.sec.asc"); diff --git a/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/ui/AsymmetricTextOperationTests.java b/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/ui/AsymmetricTextOperationTests.java index e2ea0c52f..b34ea6c81 100644 --- a/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/ui/AsymmetricTextOperationTests.java +++ b/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/ui/AsymmetricTextOperationTests.java @@ -43,9 +43,9 @@ 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.sufficientlysecure.keychain.TestHelpers.checkSnackbar; -import static org.sufficientlysecure.keychain.TestHelpers.importKeysFromResource; -import static org.sufficientlysecure.keychain.TestHelpers.randomString; +import static org.sufficientlysecure.keychain.AndroidTestHelpers.checkSnackbar; +import static org.sufficientlysecure.keychain.AndroidTestHelpers.importKeysFromResource; +import static org.sufficientlysecure.keychain.AndroidTestHelpers.randomString; import static org.sufficientlysecure.keychain.actions.CustomActions.tokenEncryptViewAddToken; import static org.sufficientlysecure.keychain.matcher.CustomMatchers.isRecyclerItemView; import static org.sufficientlysecure.keychain.matcher.CustomMatchers.withDisplayedChild; diff --git a/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/ui/EditKeyTest.java b/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/ui/EditKeyTest.java index 139967264..7857eab3f 100644 --- a/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/ui/EditKeyTest.java +++ b/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/ui/EditKeyTest.java @@ -20,37 +20,27 @@ package org.sufficientlysecure.keychain.ui; import android.app.Activity; import android.content.Intent; -import android.support.test.espresso.ViewAction; -import android.support.test.espresso.action.ViewActions; -import android.support.test.espresso.contrib.RecyclerViewActions; -import android.support.test.espresso.matcher.ViewMatchers; import android.support.test.rule.ActivityTestRule; import android.support.v7.widget.RecyclerView; -import android.widget.AdapterView; -import org.hamcrest.Description; -import org.hamcrest.Matcher; import org.junit.FixMethodOrder; import org.junit.Rule; import org.junit.runners.MethodSorters; import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.matcher.CustomMatchers; -import org.sufficientlysecure.keychain.provider.KeychainDatabase; +import org.sufficientlysecure.keychain.KeychainDatabase; 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.contrib.RecyclerViewActions.actionOnHolderItem; 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.AndroidTestHelpers.checkSnackbar; +import static org.sufficientlysecure.keychain.AndroidTestHelpers.importKeysFromResource; import static org.sufficientlysecure.keychain.matcher.CustomMatchers.withKeyHolderId; -import static org.sufficientlysecure.keychain.matcher.CustomMatchers.withKeyItemId; + //TODO This test is disabled because it needs to be fixed to work with updated code @FixMethodOrder(MethodSorters.NAME_ASCENDING) @@ -73,7 +63,7 @@ public class EditKeyTest { public void test01Edit() throws Exception { Activity activity = mActivity.getActivity(); - new KeychainDatabase(activity).clearDatabase(); + KeychainDatabase.getInstance(activity).clearDatabase(); // import key for testing, get a stable initial state importKeysFromResource(activity, "x.sec.asc"); diff --git a/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/ui/MiscCryptOperationTests.java b/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/ui/MiscCryptOperationTests.java index 784f26fe0..74b79f020 100644 --- a/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/ui/MiscCryptOperationTests.java +++ b/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/ui/MiscCryptOperationTests.java @@ -27,18 +27,15 @@ import android.content.Context; import android.content.Intent; import android.net.Uri; import android.os.Build.VERSION_CODES; -import android.support.test.espresso.contrib.RecyclerViewActions; import android.support.test.espresso.intent.Intents; import android.support.test.espresso.intent.rule.IntentsTestRule; import android.support.v7.widget.RecyclerView; -import android.widget.AdapterView; import org.junit.Before; import org.junit.Rule; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.TestHelpers; -import org.sufficientlysecure.keychain.matcher.CustomMatchers; +import org.sufficientlysecure.keychain.AndroidTestHelpers; import org.sufficientlysecure.keychain.service.PassphraseCacheService; import org.sufficientlysecure.keychain.ui.util.Notify.Style; import org.sufficientlysecure.keychain.util.Preferences; @@ -46,7 +43,6 @@ import org.sufficientlysecure.keychain.util.Preferences; import java.io.File; import static android.support.test.InstrumentationRegistry.getInstrumentation; -import static android.support.test.espresso.Espresso.onData; import static android.support.test.espresso.Espresso.onView; import static android.support.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu; import static android.support.test.espresso.Espresso.pressBack; @@ -67,16 +63,15 @@ import static android.support.test.espresso.matcher.ViewMatchers.withId; import static android.support.test.espresso.matcher.ViewMatchers.withText; import static org.hamcrest.CoreMatchers.allOf; import static org.hamcrest.CoreMatchers.hasItem; -import static org.sufficientlysecure.keychain.TestHelpers.checkSnackbar; -import static org.sufficientlysecure.keychain.TestHelpers.dismissSnackbar; -import static org.sufficientlysecure.keychain.TestHelpers.getImageNames; -import static org.sufficientlysecure.keychain.TestHelpers.importKeysFromResource; -import static org.sufficientlysecure.keychain.TestHelpers.pickRandom; -import static org.sufficientlysecure.keychain.TestHelpers.randomString; +import static org.sufficientlysecure.keychain.AndroidTestHelpers.checkSnackbar; +import static org.sufficientlysecure.keychain.AndroidTestHelpers.dismissSnackbar; +import static org.sufficientlysecure.keychain.AndroidTestHelpers.getImageNames; +import static org.sufficientlysecure.keychain.AndroidTestHelpers.importKeysFromResource; +import static org.sufficientlysecure.keychain.AndroidTestHelpers.pickRandom; +import static org.sufficientlysecure.keychain.AndroidTestHelpers.randomString; import static org.sufficientlysecure.keychain.matcher.CustomMatchers.isRecyclerItemView; import static org.sufficientlysecure.keychain.matcher.CustomMatchers.withDisplayedChild; import static org.sufficientlysecure.keychain.matcher.CustomMatchers.withKeyHolderId; -import static org.sufficientlysecure.keychain.matcher.CustomMatchers.withKeyItemId; import static org.sufficientlysecure.keychain.matcher.DrawableMatcher.withDrawable; //TODO This test is disabled because it needs to be fixed to work with updated code @@ -104,7 +99,7 @@ public class MiscCryptOperationTests { mActivity = mActivityRule.getActivity(); - TestHelpers.copyFiles(); + AndroidTestHelpers.copyFiles(); // import these two, make sure they're there importKeysFromResource(mActivity, "x.sec.asc"); diff --git a/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/ui/SymmetricTextOperationTests.java b/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/ui/SymmetricTextOperationTests.java index d3e4a8a86..b51be1e09 100644 --- a/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/ui/SymmetricTextOperationTests.java +++ b/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/ui/SymmetricTextOperationTests.java @@ -52,8 +52,8 @@ import static android.support.test.espresso.matcher.ViewMatchers.withId; import static android.support.test.espresso.matcher.ViewMatchers.withText; import static org.hamcrest.CoreMatchers.allOf; import static org.hamcrest.Matchers.equalTo; -import static org.sufficientlysecure.keychain.TestHelpers.checkSnackbar; -import static org.sufficientlysecure.keychain.TestHelpers.randomString; +import static org.sufficientlysecure.keychain.AndroidTestHelpers.checkSnackbar; +import static org.sufficientlysecure.keychain.AndroidTestHelpers.randomString; import static org.sufficientlysecure.keychain.matcher.CustomMatchers.isRecyclerItemView; import static org.sufficientlysecure.keychain.matcher.CustomMatchers.withEncryptionStatus; import static org.sufficientlysecure.keychain.matcher.CustomMatchers.withSignatureNone; diff --git a/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvShareTest.java b/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvShareTest.java deleted file mode 100644 index ebf5619ad..000000000 --- a/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvShareTest.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright (C) 2015 Vincent Breitmoser - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sufficientlysecure.keychain.ui; - - -import android.app.Activity; -import android.app.Instrumentation.ActivityResult; -import android.content.Intent; -import android.support.test.espresso.intent.rule.IntentsTestRule; - -import org.junit.Before; -import org.junit.FixMethodOrder; -import org.junit.Rule; -import org.junit.runners.MethodSorters; -import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.compatibility.ClipboardReflection; -import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; -import org.sufficientlysecure.keychain.provider.TemporaryFileProvider; -import org.sufficientlysecure.keychain.ui.util.Notify.Style; - -import static android.support.test.espresso.Espresso.onView; -import static android.support.test.espresso.action.ViewActions.click; -import static android.support.test.espresso.intent.Intents.intending; -import static android.support.test.espresso.intent.matcher.IntentMatchers.hasAction; -import static android.support.test.espresso.intent.matcher.IntentMatchers.hasExtra; -import static android.support.test.espresso.intent.matcher.IntentMatchers.hasType; -import static android.support.test.espresso.intent.matcher.UriMatchers.hasHost; -import static android.support.test.espresso.intent.matcher.UriMatchers.hasScheme; -import static android.support.test.espresso.matcher.ViewMatchers.assertThat; -import static android.support.test.espresso.matcher.ViewMatchers.withId; -import static org.hamcrest.CoreMatchers.allOf; -import static org.hamcrest.CoreMatchers.startsWith; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.is; -import static org.sufficientlysecure.keychain.TestHelpers.checkAndDismissSnackbar; -import static org.sufficientlysecure.keychain.TestHelpers.cleanupForTests; - -//TODO This test is disabled because it needs to be fixed to work with updated code -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -//@RunWith(AndroidJUnit4.class) -//@LargeTest -public class ViewKeyAdvShareTest { - - @Rule - public final IntentsTestRule mActivityRule - = new IntentsTestRule(ViewKeyAdvActivity.class) { - @Override - protected Intent getActivityIntent() { - Intent intent = super.getActivityIntent(); - intent.setData(KeyRings.buildGenericKeyRingUri(0x9D604D2F310716A3L)); - intent.putExtra(ViewKeyAdvActivity.EXTRA_SELECTED_TAB, ViewKeyAdvActivity.TAB_SHARE); - return intent; - } - }; - private Activity mActivity; - - @Before - public void setUp() throws Exception { - mActivity = mActivityRule.getActivity(); - - cleanupForTests(mActivity); - } - - //@Test - public void testShareOperations() throws Exception { - - // no-op should yield snackbar - onView(withId(R.id.view_key_action_fingerprint_clipboard)).perform(click()); - checkAndDismissSnackbar(Style.OK, R.string.fingerprint_copied_to_clipboard); - assertThat("clipboard data is fingerprint", ClipboardReflection.getClipboardText(mActivity), - is("c619d53f7a5f96f391a84ca79d604d2f310716a3")); - - intending(allOf( - hasAction("android.intent.action.CHOOSER"), - hasExtra(equalTo(Intent.EXTRA_INTENT), allOf( - hasAction(Intent.ACTION_SEND), - hasType("text/plain"), - hasExtra(is(Intent.EXTRA_TEXT), is("openpgp4fpr:c619d53f7a5f96f391a84ca79d604d2f310716a3")), - hasExtra(is(Intent.EXTRA_STREAM), - allOf(hasScheme("content"), hasHost(TemporaryFileProvider.AUTHORITY))) - )) - )).respondWith(new ActivityResult(Activity.RESULT_OK, null)); - onView(withId(R.id.view_key_action_fingerprint_share)).perform(click()); - - onView(withId(R.id.view_key_action_key_clipboard)).perform(click()); - checkAndDismissSnackbar(Style.OK, R.string.key_copied_to_clipboard); - assertThat("clipboard data is key", - ClipboardReflection.getClipboardText(mActivity), startsWith("----")); - - intending(allOf( - hasAction("android.intent.action.CHOOSER"), - hasExtra(equalTo(Intent.EXTRA_INTENT), allOf( - hasAction(Intent.ACTION_SEND), - hasType("text/plain"), - hasExtra(is(Intent.EXTRA_TEXT), startsWith("----")), - hasExtra(is(Intent.EXTRA_STREAM), - allOf(hasScheme("content"), hasHost(TemporaryFileProvider.AUTHORITY))) - )) - )).respondWith(new ActivityResult(Activity.RESULT_OK, null)); - onView(withId(R.id.view_key_action_key_share)).perform(click()); - - } - - -} diff --git a/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/ui/widget/EncryptKeyCompletionViewTest.java b/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/ui/widget/EncryptKeyCompletionViewTest.java index e82914652..554df0a51 100644 --- a/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/ui/widget/EncryptKeyCompletionViewTest.java +++ b/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/ui/widget/EncryptKeyCompletionViewTest.java @@ -40,7 +40,7 @@ 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.AndroidTestHelpers.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; diff --git a/OpenKeychain/src/debug/res/xml/shortcuts.xml b/OpenKeychain/src/debug/res/xml/shortcuts.xml new file mode 100644 index 000000000..10abca10b --- /dev/null +++ b/OpenKeychain/src/debug/res/xml/shortcuts.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/OpenKeychain/src/main/AndroidManifest.xml b/OpenKeychain/src/main/AndroidManifest.xml index 20c5f550b..b5ad62cd2 100644 --- a/OpenKeychain/src/main/AndroidManifest.xml +++ b/OpenKeychain/src/main/AndroidManifest.xml @@ -95,15 +95,6 @@ android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:theme="@style/Theme.Keychain.Light"> - - - - - - @@ -128,6 +119,8 @@ + + - - - + + + + + + - - - - - - - - - 1 && !isInEditMode()) { - throw new IllegalStateException( - "SwipeRefreshLayout can host only one direct child"); - } - mTarget = getChildAt(0); - mOriginalOffsetTop = mTarget.getTop() + getPaddingTop(); - } - if (mDistanceToTriggerSync == -1) { - if (getParent() != null && ((View)getParent()).getHeight() > 0) { - final DisplayMetrics metrics = getResources().getDisplayMetrics(); - mDistanceToTriggerSync = (int) Math.min( - ((View) getParent()) .getHeight() * MAX_SWIPE_DISTANCE_FACTOR, - REFRESH_TRIGGER_DISTANCE * metrics.density); - } - } - } - - @Override - public void draw(Canvas canvas) { - super.draw(canvas); - mProgressBar.draw(canvas); - } - - @Override - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - final int width = getMeasuredWidth(); - final int height = getMeasuredHeight(); - mProgressBar.setBounds(0, 0, width, mProgressBarHeight); - if (getChildCount() == 0) { - return; - } - final View child = getChildAt(0); - final int childLeft = getPaddingLeft(); - final int childTop = mCurrentTargetOffsetTop + getPaddingTop(); - final int childWidth = width - getPaddingLeft() - getPaddingRight(); - final int childHeight = height - getPaddingTop() - getPaddingBottom(); - child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight); - } - - @Override - public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - if (getChildCount() > 1 && !isInEditMode()) { - throw new IllegalStateException("SwipeRefreshLayout can host only one direct child"); - } - if (getChildCount() > 0) { - getChildAt(0).measure( - MeasureSpec.makeMeasureSpec( - getMeasuredWidth() - getPaddingLeft() - getPaddingRight(), - MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec( - getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), - MeasureSpec.EXACTLY)); - } - } - - /** - * @return Whether it is possible for the child view of this layout to - * scroll up. Override this if the child view is a custom view. - */ - public boolean canChildScrollUp() { - if (android.os.Build.VERSION.SDK_INT < 14) { - if (mTarget instanceof AbsListView) { - final AbsListView absListView = (AbsListView) mTarget; - return absListView.getChildCount() > 0 - && (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0) - .getTop() < absListView.getPaddingTop()); - } else { - return mTarget.getScrollY() > 0; - } - } else { - return ViewCompat.canScrollVertically(mTarget, -1); - } - } - - @Override - public boolean onInterceptTouchEvent(MotionEvent ev) { - ensureTarget(); - boolean handled = false; - if (mReturningToStart && ev.getAction() == MotionEvent.ACTION_DOWN) { - mReturningToStart = false; - } - if (isEnabled() && !mReturningToStart && !canChildScrollUp()) { - handled = onTouchEvent(ev); - } - return !handled ? super.onInterceptTouchEvent(ev) : handled; - } - - @Override - public void requestDisallowInterceptTouchEvent(boolean b) { - // Nope. - } - - @Override - public boolean onTouchEvent(MotionEvent event) { - final int action = event.getAction(); - boolean handled = false; - switch (action) { - case MotionEvent.ACTION_DOWN: - mCurrPercentage = 0; - mDownEvent = MotionEvent.obtain(event); - mPrevY = mDownEvent.getY(); - break; - case MotionEvent.ACTION_MOVE: - if (mDownEvent != null && !mReturningToStart) { - final float eventY = event.getY(); - float yDiff = eventY - mDownEvent.getY(); - if (yDiff > mTouchSlop) { - // User velocity passed min velocity; trigger a refresh - if (yDiff > mDistanceToTriggerSync) { - // User movement passed distance; trigger a refresh - startRefresh(); - handled = true; - break; - } else { - // Just track the user's movement - setTriggerPercentage( - mAccelerateInterpolator.getInterpolation( - yDiff / mDistanceToTriggerSync)); - float offsetTop = yDiff; - if (mPrevY > eventY) { - offsetTop = yDiff - mTouchSlop; - } - updateContentOffsetTop((int) (offsetTop)); - if (mPrevY > eventY && (mTarget.getTop() < mTouchSlop)) { - // If the user puts the view back at the top, we - // don't need to. This shouldn't be considered - // cancelling the gesture as the user can restart from the top. - removeCallbacks(mCancel); - } else { - updatePositionTimeout(); - } - mPrevY = event.getY(); - handled = true; - } - } - } - break; - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_CANCEL: - if (mDownEvent != null) { - mDownEvent.recycle(); - mDownEvent = null; - } - break; - } - return handled; - } - - private void startRefresh() { - removeCallbacks(mCancel); - mReturnToStartPosition.run(); - setRefreshing(true); - mListener.onRefresh(); - } - - private void updateContentOffsetTop(int targetTop) { - final int currentTop = mTarget.getTop(); - if (targetTop > mDistanceToTriggerSync) { - targetTop = (int) mDistanceToTriggerSync; - } else if (targetTop < 0) { - targetTop = 0; - } -// setTargetOffsetTopAndBottom(targetTop - currentTop); - } - - private void setTargetOffsetTopAndBottom(int offset) { - mTarget.offsetTopAndBottom(offset); - mCurrentTargetOffsetTop = mTarget.getTop(); - } - - private void updatePositionTimeout() { - removeCallbacks(mCancel); - postDelayed(mCancel, RETURN_TO_ORIGINAL_POSITION_TIMEOUT); - } - - /** - * Classes that wish to be notified when the swipe gesture correctly - * triggers a refresh should implement this interface. - */ - public interface OnRefreshListener { - void onRefresh(); - } - - /** - * Simple AnimationListener to avoid having to implement unneeded methods in - * AnimationListeners. - */ - private class BaseAnimationListener implements AnimationListener { - @Override - public void onAnimationStart(Animation animation) { - } - - @Override - public void onAnimationEnd(Animation animation) { - } - - @Override - public void onAnimationRepeat(Animation animation) { - } - } -} \ No newline at end of file diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/Constants.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/Constants.java index 9e1e676f0..c3b2687de 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/Constants.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/Constants.java @@ -37,6 +37,8 @@ public final class Constants { public static final boolean DEBUG_SYNC_REMOVE_CONTACTS = false; public static final boolean DEBUG_KEYSERVER_SYNC = false; + public static final boolean IS_RUNNING_UNITTEST = isRunningUnitTest(); + public static final String TAG = DEBUG ? "Keychain D" : "Keychain"; public static final String PACKAGE_NAME = "org.sufficientlysecure.keychain"; @@ -108,9 +110,14 @@ public final class Constants { public static final File APP_DIR = new File(Environment.getExternalStorageDirectory(), "OpenKeychain"); } - public static final class Notification { + public static final class NotificationIds { public static final int PASSPHRASE_CACHE = 1; public static final int KEYSERVER_SYNC_FAIL_ORBOT = 2; + public static final int KEYSERVER_SYNC = 3; + } + + public static final class NotificationChannels { + public static final String KEYSERVER_SYNC = "keyserverSync"; } public static final class Pref { @@ -145,6 +152,7 @@ public final class Constants { public static final String SYNC_CONTACTS = "syncContacts"; public static final String SYNC_KEYSERVER = "syncKeyserver"; public static final String ENABLE_WIFI_SYNC_ONLY = "enableWifiSyncOnly"; + public static final String SYNC_IS_SCHEDULED = "syncIsScheduled"; // other settings public static final String EXPERIMENTAL_ENABLE_LINKED_IDENTITIES = "experimentalEnableLinkedIdentities"; public static final String EXPERIMENTAL_ENABLE_KEYBASE = "experimentalEnableKeybase"; @@ -205,4 +213,12 @@ public final class Constants { public static final KeyFormat SECURITY_TOKEN_V2_DEC = new RSAKeyFormat(2048, ELEN, RSAKeyFormat.RSAAlgorithmFormat.CRT_WITH_MODULUS); public static final KeyFormat SECURITY_TOKEN_V2_AUTH = new RSAKeyFormat(2048, ELEN, RSAKeyFormat.RSAAlgorithmFormat.CRT_WITH_MODULUS); + private static boolean isRunningUnitTest() { + try { + Class.forName("org.sufficientlysecure.keychain.KeychainTestRunner"); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/KeychainApplication.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/KeychainApplication.java index c72a90a45..3cd3353ea 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/KeychainApplication.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/KeychainApplication.java @@ -24,13 +24,8 @@ import java.util.HashMap; import android.accounts.Account; import android.accounts.AccountManager; import android.app.Application; -import android.app.job.JobScheduler; import android.content.Context; import android.graphics.Bitmap; -import android.graphics.PorterDuff; -import android.graphics.drawable.Drawable; -import android.os.Build; -import android.os.Handler; import android.support.annotation.Nullable; import android.widget.Toast; @@ -38,8 +33,7 @@ import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.sufficientlysecure.keychain.network.TlsCertificatePinning; import org.sufficientlysecure.keychain.provider.TemporaryFileProvider; import org.sufficientlysecure.keychain.service.ContactSyncAdapterService; -import org.sufficientlysecure.keychain.service.KeyserverSyncAdapterService; -import org.sufficientlysecure.keychain.ui.util.FormattingUtils; +import org.sufficientlysecure.keychain.keysync.KeyserverSyncManager; import org.sufficientlysecure.keychain.util.PRNGFixes; import org.sufficientlysecure.keychain.util.Preferences; import timber.log.Timber; @@ -95,26 +89,22 @@ public class KeychainApplication extends Application { if (preferences.isAppExecutedFirstTime()) { preferences.setAppExecutedFirstTime(false); - KeyserverSyncAdapterService.enableKeyserverSync(this); ContactSyncAdapterService.enableContactsSync(this); preferences.setPrefVersionToCurrentVersion(); } - if (Preferences.getKeyserverSyncEnabled(this)) { - // will update a keyserver sync if the interval has changed - KeyserverSyncAdapterService.updateInterval(this); - } - // Upgrade preferences as needed - preferences.upgradePreferences(this); + preferences.upgradePreferences(); TlsCertificatePinning.addPinnedCertificate("hkps.pool.sks-keyservers.net", getAssets(), "hkps.pool.sks-keyservers.net.CA.cer"); TlsCertificatePinning.addPinnedCertificate("pgp.mit.edu", getAssets(), "pgp.mit.edu.cer"); TlsCertificatePinning.addPinnedCertificate("api.keybase.io", getAssets(), "api.keybase.io.CA.cer"); TlsCertificatePinning.addPinnedCertificate("keyserver.ubuntu.com", getAssets(), "DigiCertGlobalRootCA.cer"); - new Handler().postDelayed(() -> TemporaryFileProvider.cleanUp(getApplicationContext()), 1000); + KeyserverSyncManager.updateKeyserverSyncSchedule(this, Constants.DEBUG_KEYSERVER_SYNC); + + TemporaryFileProvider.scheduleCleanupImmediately(); } /** diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainDatabase.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/KeychainDatabase.java similarity index 52% rename from OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainDatabase.java rename to OpenKeychain/src/main/java/org/sufficientlysecure/keychain/KeychainDatabase.java index 8350b9da1..cbcf7e451 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainDatabase.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/KeychainDatabase.java @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package org.sufficientlysecure.keychain.provider; +package org.sufficientlysecure.keychain; import java.io.File; @@ -23,23 +23,20 @@ import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; +import android.arch.persistence.db.SupportSQLiteDatabase; +import android.arch.persistence.db.SupportSQLiteOpenHelper; +import android.arch.persistence.db.SupportSQLiteOpenHelper.Callback; +import android.arch.persistence.db.SupportSQLiteOpenHelper.Configuration; +import android.arch.persistence.db.framework.FrameworkSQLiteOpenHelperFactory; import android.content.Context; +import android.database.Cursor; import android.database.SQLException; -import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteException; -import android.database.sqlite.SQLiteOpenHelper; -import android.provider.BaseColumns; +import android.support.annotation.VisibleForTesting; -import org.sufficientlysecure.keychain.Constants; -import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAppsAllowedKeysColumns; -import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAppsColumns; -import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAutocryptPeerColumns; +import org.sufficientlysecure.keychain.daos.LocalSecretKeyStorage; import org.sufficientlysecure.keychain.provider.KeychainContract.CertsColumns; -import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRingsColumns; -import org.sufficientlysecure.keychain.provider.KeychainContract.KeySignaturesColumns; import org.sufficientlysecure.keychain.provider.KeychainContract.KeysColumns; -import org.sufficientlysecure.keychain.provider.KeychainContract.OverriddenWarnings; -import org.sufficientlysecure.keychain.provider.KeychainContract.UpdatedKeysColumns; import org.sufficientlysecure.keychain.provider.KeychainContract.UserPacketsColumns; import org.sufficientlysecure.keychain.util.Preferences; import timber.log.Timber; @@ -53,185 +50,86 @@ import timber.log.Timber; * - TEXT. The value is a text string, stored using the database encoding (UTF-8, UTF-16BE or UTF-16LE). * - BLOB. The value is a blob of data, stored exactly as it was input. */ -public class KeychainDatabase extends SQLiteOpenHelper { +public class KeychainDatabase { private static final String DATABASE_NAME = "openkeychain.db"; - private static final int DATABASE_VERSION = 25; - private Context mContext; + private static final int DATABASE_VERSION = 29; + private final SupportSQLiteOpenHelper supportSQLiteOpenHelper; + + private static KeychainDatabase sInstance; + + public static KeychainDatabase getInstance(Context context) { + if (sInstance == null || Constants.IS_RUNNING_UNITTEST) { + sInstance = new KeychainDatabase(context.getApplicationContext()); + } + return sInstance; + } public interface Tables { String KEY_RINGS_PUBLIC = "keyrings_public"; - String KEY_RINGS_SECRET = "keyrings_secret"; String KEYS = "keys"; - String UPDATED_KEYS = "updated_keys"; String KEY_SIGNATURES = "key_signatures"; String USER_PACKETS = "user_packets"; String CERTS = "certs"; - String API_APPS = "api_apps"; String API_ALLOWED_KEYS = "api_allowed_keys"; String OVERRIDDEN_WARNINGS = "overridden_warnings"; - String API_AUTOCRYPT_PEERS = "api_autocrypt_peers"; } - private static final String CREATE_KEYRINGS_PUBLIC = - "CREATE TABLE IF NOT EXISTS keyrings_public (" - + KeyRingsColumns.MASTER_KEY_ID + " INTEGER PRIMARY KEY," - + KeyRingsColumns.KEY_RING_DATA + " BLOB" - + ")"; + private KeychainDatabase(Context context) { + supportSQLiteOpenHelper = + new FrameworkSQLiteOpenHelperFactory() + .create(Configuration.builder(context).name(DATABASE_NAME).callback( + new Callback(DATABASE_VERSION) { + @Override + public void onCreate(SupportSQLiteDatabase db) { + KeychainDatabase.this.onCreate(db, context); + } - private static final String CREATE_KEYRINGS_SECRET = - "CREATE TABLE IF NOT EXISTS keyrings_secret (" - + KeyRingsColumns.MASTER_KEY_ID + " INTEGER PRIMARY KEY," - + KeyRingsColumns.KEY_RING_DATA + " BLOB, " - + "FOREIGN KEY(" + KeyRingsColumns.MASTER_KEY_ID + ") " - + "REFERENCES keyrings_public(" + KeyRingsColumns.MASTER_KEY_ID + ") ON DELETE CASCADE" - + ")"; + @Override + public void onUpgrade(SupportSQLiteDatabase db, int oldVersion, int newVersion) { + KeychainDatabase.this.onUpgrade(db, context, oldVersion, newVersion); + } - private static final String CREATE_KEYS = - "CREATE TABLE IF NOT EXISTS " + Tables.KEYS + " (" - + KeysColumns.MASTER_KEY_ID + " INTEGER, " - + KeysColumns.RANK + " INTEGER, " + @Override + public void onDowngrade(SupportSQLiteDatabase db, int oldVersion, int newVersion) { + KeychainDatabase.this.onDowngrade(); + } - + KeysColumns.KEY_ID + " INTEGER, " - + KeysColumns.KEY_SIZE + " INTEGER, " - + KeysColumns.KEY_CURVE_OID + " TEXT, " - + KeysColumns.ALGORITHM + " INTEGER, " - + KeysColumns.FINGERPRINT + " BLOB, " - - + KeysColumns.CAN_CERTIFY + " INTEGER, " - + KeysColumns.CAN_SIGN + " INTEGER, " - + KeysColumns.CAN_ENCRYPT + " INTEGER, " - + KeysColumns.CAN_AUTHENTICATE + " INTEGER, " - + KeysColumns.IS_REVOKED + " INTEGER, " - + KeysColumns.HAS_SECRET + " INTEGER, " - + KeysColumns.IS_SECURE + " INTEGER, " - - + KeysColumns.CREATION + " INTEGER, " - + KeysColumns.EXPIRY + " INTEGER, " - - + "PRIMARY KEY(" + KeysColumns.MASTER_KEY_ID + ", " + KeysColumns.RANK + ")," - + "FOREIGN KEY(" + KeysColumns.MASTER_KEY_ID + ") REFERENCES " - + Tables.KEY_RINGS_PUBLIC + "(" + KeyRingsColumns.MASTER_KEY_ID + ") ON DELETE CASCADE" - + ")"; - - private static final String CREATE_USER_PACKETS = - "CREATE TABLE IF NOT EXISTS " + Tables.USER_PACKETS + "(" - + UserPacketsColumns.MASTER_KEY_ID + " INTEGER, " - + UserPacketsColumns.TYPE + " INT, " - + UserPacketsColumns.USER_ID + " TEXT, " - + UserPacketsColumns.NAME + " TEXT, " - + UserPacketsColumns.EMAIL + " TEXT, " - + UserPacketsColumns.COMMENT + " TEXT, " - + UserPacketsColumns.ATTRIBUTE_DATA + " BLOB, " - - + UserPacketsColumns.IS_PRIMARY + " INTEGER, " - + UserPacketsColumns.IS_REVOKED + " INTEGER, " - + UserPacketsColumns.RANK+ " INTEGER, " - - + "PRIMARY KEY(" + UserPacketsColumns.MASTER_KEY_ID + ", " + UserPacketsColumns.RANK + "), " - + "FOREIGN KEY(" + UserPacketsColumns.MASTER_KEY_ID + ") REFERENCES " - + Tables.KEY_RINGS_PUBLIC + "(" + KeyRingsColumns.MASTER_KEY_ID + ") ON DELETE CASCADE" - + ")"; - - private static final String CREATE_CERTS = - "CREATE TABLE IF NOT EXISTS " + Tables.CERTS + "(" - + CertsColumns.MASTER_KEY_ID + " INTEGER," - + CertsColumns.RANK + " INTEGER, " // rank of certified uid - - + CertsColumns.KEY_ID_CERTIFIER + " INTEGER, " // certifying key - + CertsColumns.TYPE + " INTEGER, " - + CertsColumns.VERIFIED + " INTEGER, " - + CertsColumns.CREATION + " INTEGER, " - - + CertsColumns.DATA + " BLOB, " - - + "PRIMARY KEY(" + CertsColumns.MASTER_KEY_ID + ", " + CertsColumns.RANK + ", " - + CertsColumns.KEY_ID_CERTIFIER + "), " - + "FOREIGN KEY(" + CertsColumns.MASTER_KEY_ID + ") REFERENCES " - + Tables.KEY_RINGS_PUBLIC + "(" + KeyRingsColumns.MASTER_KEY_ID + ") ON DELETE CASCADE," - + "FOREIGN KEY(" + CertsColumns.MASTER_KEY_ID + ", " + CertsColumns.RANK + ") REFERENCES " - + Tables.USER_PACKETS + "(" + UserPacketsColumns.MASTER_KEY_ID + ", " + UserPacketsColumns.RANK + ") ON DELETE CASCADE" - + ")"; - - private static final String CREATE_UPDATE_KEYS = - "CREATE TABLE IF NOT EXISTS " + Tables.UPDATED_KEYS + " (" - + UpdatedKeysColumns.MASTER_KEY_ID + " INTEGER PRIMARY KEY, " - + UpdatedKeysColumns.LAST_UPDATED + " INTEGER, " - + UpdatedKeysColumns.SEEN_ON_KEYSERVERS + " INTEGER, " - + "FOREIGN KEY(" + UpdatedKeysColumns.MASTER_KEY_ID + ") REFERENCES " - + Tables.KEY_RINGS_PUBLIC + "(" + KeyRingsColumns.MASTER_KEY_ID + ") ON DELETE CASCADE" - + ")"; - - private static final String CREATE_KEY_SIGNATURES = - "CREATE TABLE IF NOT EXISTS " + Tables.KEY_SIGNATURES + " (" - + KeySignaturesColumns.MASTER_KEY_ID + " INTEGER NOT NULL, " - + KeySignaturesColumns.SIGNER_KEY_ID + " INTEGER NOT NULL, " - + "PRIMARY KEY(" + KeySignaturesColumns.MASTER_KEY_ID + ", " + KeySignaturesColumns.SIGNER_KEY_ID + "), " - + "FOREIGN KEY(" + KeySignaturesColumns.MASTER_KEY_ID + ") REFERENCES " - + Tables.KEY_RINGS_PUBLIC + "(" + KeyRingsColumns.MASTER_KEY_ID + ") ON DELETE CASCADE" - + ")"; - - private static final String CREATE_API_AUTOCRYPT_PEERS = - "CREATE TABLE IF NOT EXISTS " + Tables.API_AUTOCRYPT_PEERS + " (" - + ApiAutocryptPeerColumns.PACKAGE_NAME + " TEXT NOT NULL, " - + ApiAutocryptPeerColumns.IDENTIFIER + " TEXT NOT NULL, " - + ApiAutocryptPeerColumns.LAST_SEEN + " INTEGER, " - + ApiAutocryptPeerColumns.LAST_SEEN_KEY + " INTEGER NULL, " - + ApiAutocryptPeerColumns.IS_MUTUAL + " INTEGER NULL, " - + ApiAutocryptPeerColumns.MASTER_KEY_ID + " INTEGER NULL, " - + ApiAutocryptPeerColumns.GOSSIP_MASTER_KEY_ID + " INTEGER NULL, " - + ApiAutocryptPeerColumns.GOSSIP_LAST_SEEN_KEY + " INTEGER NULL, " - + ApiAutocryptPeerColumns.GOSSIP_ORIGIN + " INTEGER NULL, " - + "PRIMARY KEY(" + ApiAutocryptPeerColumns.PACKAGE_NAME + ", " - + ApiAutocryptPeerColumns.IDENTIFIER + "), " - + "FOREIGN KEY(" + ApiAutocryptPeerColumns.PACKAGE_NAME + ") REFERENCES " - + Tables.API_APPS + "(" + ApiAppsColumns.PACKAGE_NAME + ") ON DELETE CASCADE" - + ")"; - - private static final String CREATE_API_APPS = - "CREATE TABLE IF NOT EXISTS " + Tables.API_APPS + " (" - + BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " - + ApiAppsColumns.PACKAGE_NAME + " TEXT NOT NULL UNIQUE, " - + ApiAppsColumns.PACKAGE_CERTIFICATE + " BLOB" - + ")"; - - private static final String CREATE_API_APPS_ALLOWED_KEYS = - "CREATE TABLE IF NOT EXISTS " + Tables.API_ALLOWED_KEYS + " (" - + BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " - + ApiAppsAllowedKeysColumns.KEY_ID + " INTEGER, " - + ApiAppsAllowedKeysColumns.PACKAGE_NAME + " TEXT NOT NULL, " - - + "UNIQUE(" + ApiAppsAllowedKeysColumns.KEY_ID + ", " - + ApiAppsAllowedKeysColumns.PACKAGE_NAME + "), " - + "FOREIGN KEY(" + ApiAppsAllowedKeysColumns.PACKAGE_NAME + ") REFERENCES " - + Tables.API_APPS + "(" + ApiAppsAllowedKeysColumns.PACKAGE_NAME + ") ON DELETE CASCADE" - + ")"; - - private static final String CREATE_OVERRIDDEN_WARNINGS = - "CREATE TABLE IF NOT EXISTS " + Tables.OVERRIDDEN_WARNINGS + " (" - + BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " - + OverriddenWarnings.IDENTIFIER + " TEXT NOT NULL UNIQUE " - + ")"; - - public KeychainDatabase(Context context) { - super(context, DATABASE_NAME, null, DATABASE_VERSION); - mContext = context; + @Override + public void onOpen(SupportSQLiteDatabase db) { + super.onOpen(db); + if (!db.isReadOnly()) { + // Enable foreign key constraints + db.execSQL("PRAGMA foreign_keys=ON;"); + if (Constants.DEBUG) { + recreateUnifiedKeyView(db); + } + } + } + }).build()); } - @Override - public void onCreate(SQLiteDatabase db) { + public SupportSQLiteDatabase getReadableDatabase() { + return supportSQLiteOpenHelper.getReadableDatabase(); + } + + public SupportSQLiteDatabase getWritableDatabase() { + return supportSQLiteOpenHelper.getWritableDatabase(); + } + + private void onCreate(SupportSQLiteDatabase db, Context context) { Timber.w("Creating database..."); - db.execSQL(CREATE_KEYRINGS_PUBLIC); - db.execSQL(CREATE_KEYRINGS_SECRET); - db.execSQL(CREATE_KEYS); - db.execSQL(CREATE_USER_PACKETS); - db.execSQL(CREATE_CERTS); - db.execSQL(CREATE_UPDATE_KEYS); - db.execSQL(CREATE_KEY_SIGNATURES); - db.execSQL(CREATE_API_APPS); - db.execSQL(CREATE_API_APPS_ALLOWED_KEYS); - db.execSQL(CREATE_OVERRIDDEN_WARNINGS); - db.execSQL(CREATE_API_AUTOCRYPT_PEERS); + db.execSQL(KeyRingsPublicModel.CREATE_TABLE); + db.execSQL(KeysModel.CREATE_TABLE); + db.execSQL(UserPacketsModel.CREATE_TABLE); + db.execSQL(CertsModel.CREATE_TABLE); + db.execSQL(KeyMetadataModel.CREATE_TABLE); + db.execSQL(KeySignaturesModel.CREATE_TABLE); + db.execSQL(ApiAppsModel.CREATE_TABLE); + db.execSQL(OverriddenWarningsModel.CREATE_TABLE); + db.execSQL(AutocryptPeersModel.CREATE_TABLE); + db.execSQL(ApiAllowedKeysModel.CREATE_TABLE); + db.execSQL(KeysModel.UNIFIEDKEYVIEW); db.execSQL("CREATE INDEX keys_by_rank ON keys (" + KeysColumns.RANK + ", " + KeysColumns.MASTER_KEY_ID + ");"); db.execSQL("CREATE INDEX uids_by_rank ON user_packets (" + UserPacketsColumns.RANK + ", " @@ -241,20 +139,10 @@ public class KeychainDatabase extends SQLiteOpenHelper { db.execSQL("CREATE INDEX uids_by_email ON user_packets (" + UserPacketsColumns.EMAIL + ");"); - Preferences.getPreferences(mContext).setKeySignaturesTableInitialized(); + Preferences.getPreferences(context).setKeySignaturesTableInitialized(); } - @Override - public void onOpen(SQLiteDatabase db) { - super.onOpen(db); - if (!db.isReadOnly()) { - // Enable foreign key constraints - db.execSQL("PRAGMA foreign_keys=ON;"); - } - } - - @Override - public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + private void onUpgrade(SupportSQLiteDatabase db, Context context, int oldVersion, int newVersion) { Timber.d("Upgrading db from " + oldVersion + " to " + newVersion); switch (oldVersion) { @@ -293,7 +181,7 @@ public class KeychainDatabase extends SQLiteOpenHelper { case 7: // new table for allowed key ids in API try { - db.execSQL(CREATE_API_APPS_ALLOWED_KEYS); + db.execSQL(ApiAppsModel.CREATE_TABLE); } catch (Exception e) { // never mind, the column probably already existed } @@ -302,37 +190,37 @@ public class KeychainDatabase extends SQLiteOpenHelper { db.execSQL("DROP TABLE IF EXISTS certs"); db.execSQL("DROP TABLE IF EXISTS user_ids"); db.execSQL("CREATE TABLE IF NOT EXISTS user_packets(" - + "master_key_id INTEGER, " - + "type INT, " - + "user_id TEXT, " - + "attribute_data BLOB, " + + "master_key_id INTEGER, " + + "type INT, " + + "user_id TEXT, " + + "attribute_data BLOB, " - + "is_primary INTEGER, " - + "is_revoked INTEGER, " - + "rank INTEGER, " + + "is_primary INTEGER, " + + "is_revoked INTEGER, " + + "rank INTEGER, " - + "PRIMARY KEY(master_key_id, rank), " - + "FOREIGN KEY(master_key_id) REFERENCES " + + "PRIMARY KEY(master_key_id, rank), " + + "FOREIGN KEY(master_key_id) REFERENCES " + "keyrings_public(master_key_id) ON DELETE CASCADE" - + ")"); + + ")"); db.execSQL("CREATE TABLE IF NOT EXISTS certs(" - + "master_key_id INTEGER," - + "rank INTEGER, " // rank of certified uid + + "master_key_id INTEGER," + + "rank INTEGER, " // rank of certified uid - + "key_id_certifier INTEGER, " // certifying key - + "type INTEGER, " - + "verified INTEGER, " - + "creation INTEGER, " + + "key_id_certifier INTEGER, " // certifying key + + "type INTEGER, " + + "verified INTEGER, " + + "creation INTEGER, " - + "data BLOB, " + + "data BLOB, " - + "PRIMARY KEY(master_key_id, rank, " + + "PRIMARY KEY(master_key_id, rank, " + "key_id_certifier), " - + "FOREIGN KEY(master_key_id) REFERENCES " + + "FOREIGN KEY(master_key_id) REFERENCES " + "keyrings_public(master_key_id) ON DELETE CASCADE," - + "FOREIGN KEY(master_key_id, rank) REFERENCES " + + "FOREIGN KEY(master_key_id, rank) REFERENCES " + "user_packets(master_key_id, rank) ON DELETE CASCADE" - + ")"); + + ")"); case 9: // do nothing here, just consolidate case 10: @@ -371,20 +259,12 @@ public class KeychainDatabase extends SQLiteOpenHelper { case 19: // emergency fix for crashing consolidate db.execSQL("UPDATE keys SET is_secure = 1;"); - /* TODO actually drop this table. leaving it around for now! case 20: - db.execSQL("DROP TABLE api_accounts"); - if (oldVersion == 20) { - // no need to consolidate - return; - } - */ - case 20: - db.execSQL( - "CREATE TABLE IF NOT EXISTS overridden_warnings (" - + "_id INTEGER PRIMARY KEY AUTOINCREMENT, " - + "identifier TEXT NOT NULL UNIQUE " - + ")"); + db.execSQL( + "CREATE TABLE IF NOT EXISTS overridden_warnings (" + + "_id INTEGER PRIMARY KEY AUTOINCREMENT, " + + "identifier TEXT NOT NULL UNIQUE " + + ")"); case 21: try { @@ -400,10 +280,10 @@ public class KeychainDatabase extends SQLiteOpenHelper { + "last_updated INTEGER NOT NULL, " + "last_seen_key INTEGER NOT NULL, " + "state INTEGER NOT NULL, " - + "master_key_id INTEGER NULL, " + + "master_key_id INTEGER, " + "PRIMARY KEY(package_name, identifier), " + "FOREIGN KEY(package_name) REFERENCES api_apps(package_name) ON DELETE CASCADE" - + ")"); + + ")"); case 23: db.execSQL("CREATE TABLE IF NOT EXISTS key_signatures (" @@ -413,7 +293,7 @@ public class KeychainDatabase extends SQLiteOpenHelper { + "FOREIGN KEY(master_key_id) REFERENCES keyrings_public(master_key_id) ON DELETE CASCADE" + ")"); - case 24: + case 24: { try { db.beginTransaction(); db.execSQL("ALTER TABLE api_autocrypt_peers RENAME TO tmp"); @@ -456,11 +336,74 @@ public class KeychainDatabase extends SQLiteOpenHelper { db.execSQL("CREATE INDEX IF NOT EXISTS uids_by_email ON user_packets (email);"); db.execSQL("DROP INDEX keys_by_rank"); db.execSQL("CREATE INDEX keys_by_rank ON keys(rank, master_key_id);"); + } + + case 25: { + try { + migrateSecretKeysFromDbToLocalStorage(db, context); + } catch (IOException e) { + throw new IllegalStateException("Error migrating secret keys! This is bad!!"); + } + } + + case 26: + migrateUpdatedKeysToKeyMetadataTable(db); + + case 27: + renameApiAutocryptPeersTable(db); + + case 28: + recreateUnifiedKeyView(db); + // drop old table from version 20 + db.execSQL("DROP TABLE IF EXISTS api_accounts"); } } - @Override - public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { + private void recreateUnifiedKeyView(SupportSQLiteDatabase db) { + // noinspection deprecation + db.execSQL("DROP VIEW IF EXISTS " + KeysModel.UNIFIEDKEYVIEW_VIEW_NAME); + db.execSQL(KeysModel.UNIFIEDKEYVIEW); + } + + private void migrateSecretKeysFromDbToLocalStorage(SupportSQLiteDatabase db, Context context) throws IOException { + LocalSecretKeyStorage localSecretKeyStorage = LocalSecretKeyStorage.getInstance(context); + Cursor cursor = db.query("SELECT master_key_id, key_ring_data FROM keyrings_secret"); + while (cursor.moveToNext()) { + long masterKeyId = cursor.getLong(0); + byte[] secretKeyBlob = cursor.getBlob(1); + localSecretKeyStorage.writeSecretKey(masterKeyId, secretKeyBlob); + } + cursor.close(); + + // we'll keep this around for now, but make sure to delete when migration looks ok!! + // db.execSQL("DROP TABLE keyrings_secret"); + } + + private void migrateUpdatedKeysToKeyMetadataTable(SupportSQLiteDatabase db) { + try { + db.execSQL("ALTER TABLE updated_keys RENAME TO key_metadata;"); + } catch (SQLException e) { + if (Constants.DEBUG) { + Timber.e(e, "Ignoring migration exception, this probably happened before"); + return; + } + throw e; + } + } + + private void renameApiAutocryptPeersTable(SupportSQLiteDatabase db) { + try { + db.execSQL("ALTER TABLE api_autocrypt_peers RENAME TO autocrypt_peers;"); + } catch (SQLException e) { + if (Constants.DEBUG) { + Timber.e(e, "Ignoring migration exception, this probably happened before"); + return; + } + throw e; + } + } + + private void onDowngrade() { // Downgrade is ok for the debug version, makes it easier to work with branches if (Constants.DEBUG) { return; @@ -512,9 +455,9 @@ public class KeychainDatabase extends SQLiteOpenHelper { // DANGEROUS, use in test code ONLY! public void clearDatabase() { - getWritableDatabase().execSQL("delete from " + Tables.KEY_RINGS_PUBLIC); - getWritableDatabase().execSQL("delete from " + Tables.API_ALLOWED_KEYS); - getWritableDatabase().execSQL("delete from " + Tables.API_APPS); + getWritableDatabase().execSQL("delete from " + KeyRingsPublicModel.TABLE_NAME); + getWritableDatabase().execSQL("delete from " + ApiAllowedKeysModel.TABLE_NAME); + getWritableDatabase().execSQL("delete from api_apps"); } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/daos/AbstractDao.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/daos/AbstractDao.java new file mode 100644 index 000000000..2daedcdcd --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/daos/AbstractDao.java @@ -0,0 +1,67 @@ +package org.sufficientlysecure.keychain.daos; + + +import java.util.ArrayList; +import java.util.List; + +import android.arch.persistence.db.SupportSQLiteDatabase; +import android.arch.persistence.db.SupportSQLiteQuery; +import android.database.Cursor; + +import org.sufficientlysecure.keychain.KeychainDatabase; +import org.sufficientlysecure.keychain.daos.KeyRepository.NotFoundException; + + +class AbstractDao { + private final KeychainDatabase db; + private final DatabaseNotifyManager databaseNotifyManager; + + AbstractDao(KeychainDatabase db, DatabaseNotifyManager databaseNotifyManager) { + this.db = db; + this.databaseNotifyManager = databaseNotifyManager; + } + + SupportSQLiteDatabase getReadableDb() { + return db.getReadableDatabase(); + } + + SupportSQLiteDatabase getWritableDb() { + return db.getWritableDatabase(); + } + + DatabaseNotifyManager getDatabaseNotifyManager() { + return databaseNotifyManager; + } + + List mapAllRows(SupportSQLiteQuery query, Mapper mapper) { + ArrayList result = new ArrayList<>(); + try (Cursor cursor = getReadableDb().query(query)) { + while (cursor.moveToNext()) { + T item = mapper.map(cursor); + result.add(item); + } + } + return result; + } + + T mapSingleRowOrThrow(SupportSQLiteQuery query, Mapper mapper) throws NotFoundException { + T result = mapSingleRow(query, mapper); + if (result == null) { + throw new NotFoundException(); + } + return result; + } + + T mapSingleRow(SupportSQLiteQuery query, Mapper mapper) { + try (Cursor cursor = getReadableDb().query(query)) { + if (cursor.moveToNext()) { + return mapper.map(cursor); + } + } + return null; + } + + interface Mapper { + T map(Cursor cursor); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/daos/ApiAppDao.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/daos/ApiAppDao.java new file mode 100644 index 000000000..79e87bf81 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/daos/ApiAppDao.java @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2017 Schürmann & Breitmoser GbR + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.daos; + + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import android.content.Context; +import android.database.Cursor; + +import com.squareup.sqldelight.SqlDelightQuery; +import org.sufficientlysecure.keychain.ApiAllowedKeysModel.InsertAllowedKey; +import org.sufficientlysecure.keychain.ApiAppsModel; +import org.sufficientlysecure.keychain.ApiAppsModel.DeleteByPackageName; +import org.sufficientlysecure.keychain.ApiAppsModel.InsertApiApp; +import org.sufficientlysecure.keychain.KeychainDatabase; +import org.sufficientlysecure.keychain.model.ApiAllowedKey; +import org.sufficientlysecure.keychain.model.ApiApp; + + +public class ApiAppDao extends AbstractDao { + public static ApiAppDao getInstance(Context context) { + KeychainDatabase keychainDatabase = KeychainDatabase.getInstance(context); + DatabaseNotifyManager databaseNotifyManager = DatabaseNotifyManager.create(context); + + return new ApiAppDao(keychainDatabase, databaseNotifyManager); + } + + private ApiAppDao(KeychainDatabase keychainDatabase, DatabaseNotifyManager databaseNotifyManager) { + super(keychainDatabase, databaseNotifyManager); + } + + public ApiApp getApiApp(String packageName) { + try (Cursor cursor = getReadableDb().query(ApiApp.FACTORY.selectByPackageName(packageName))) { + if (cursor.moveToFirst()) { + return ApiApp.FACTORY.selectByPackageNameMapper().map(cursor); + } + return null; + } + } + + public byte[] getApiAppCertificate(String packageName) { + try (Cursor cursor = getReadableDb().query(ApiApp.FACTORY.getCertificate(packageName))) { + if (cursor.moveToFirst()) { + return ApiApp.FACTORY.getCertificateMapper().map(cursor); + } + return null; + } + } + + public void insertApiApp(ApiApp apiApp) { + ApiApp existingApiApp = getApiApp(apiApp.package_name()); + if (existingApiApp != null) { + if (!Arrays.equals(existingApiApp.package_signature(), apiApp.package_signature())) { + throw new IllegalStateException("Inserting existing api with different signature?!"); + } + return; + } + InsertApiApp statement = new ApiAppsModel.InsertApiApp(getWritableDb()); + statement.bind(apiApp.package_name(), apiApp.package_signature()); + statement.executeInsert(); + + getDatabaseNotifyManager().notifyApiAppChange(apiApp.package_name()); + } + + public void deleteApiApp(String packageName) { + DeleteByPackageName deleteByPackageName = new DeleteByPackageName(getWritableDb()); + deleteByPackageName.bind(packageName); + deleteByPackageName.executeUpdateDelete(); + + getDatabaseNotifyManager().notifyApiAppChange(packageName); + } + + public HashSet getAllowedKeyIdsForApp(String packageName) { + SqlDelightQuery allowedKeys = ApiAllowedKey.FACTORY.getAllowedKeys(packageName); + HashSet keyIds = new HashSet<>(); + try (Cursor cursor = getReadableDb().query(allowedKeys)) { + while (cursor.moveToNext()) { + long allowedKeyId = ApiAllowedKey.FACTORY.getAllowedKeysMapper().map(cursor); + keyIds.add(allowedKeyId); + } + } + return keyIds; + } + + public void saveAllowedKeyIdsForApp(String packageName, Set allowedKeyIds) { + ApiAllowedKey.DeleteByPackageName deleteByPackageName = new ApiAllowedKey.DeleteByPackageName(getWritableDb()); + deleteByPackageName.bind(packageName); + deleteByPackageName.executeUpdateDelete(); + + InsertAllowedKey statement = new InsertAllowedKey(getWritableDb()); + for (Long keyId : allowedKeyIds) { + statement.bind(packageName, keyId); + statement.execute(); + } + + getDatabaseNotifyManager().notifyApiAppChange(packageName); + } + + public void addAllowedKeyIdForApp(String packageName, long allowedKeyId) { + InsertAllowedKey statement = new InsertAllowedKey(getWritableDb()); + statement.bind(packageName, allowedKeyId); + statement.execute(); + + getDatabaseNotifyManager().notifyApiAppChange(packageName); + } + + public List getAllApiApps() { + SqlDelightQuery query = ApiApp.FACTORY.selectAll(); + + ArrayList result = new ArrayList<>(); + try (Cursor cursor = getReadableDb().query(query)) { + while (cursor.moveToNext()) { + ApiApp apiApp = ApiApp.FACTORY.selectAllMapper().map(cursor); + result.add(apiApp); + } + } + return result; + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/daos/AutocryptPeerDao.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/daos/AutocryptPeerDao.java new file mode 100644 index 000000000..f70d3638f --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/daos/AutocryptPeerDao.java @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2017 Schürmann & Breitmoser GbR + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.daos; + + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import android.content.Context; +import android.database.Cursor; +import android.support.annotation.Nullable; + +import com.squareup.sqldelight.SqlDelightQuery; +import org.sufficientlysecure.keychain.AutocryptPeersModel.DeleteByIdentifier; +import org.sufficientlysecure.keychain.AutocryptPeersModel.DeleteByMasterKeyId; +import org.sufficientlysecure.keychain.AutocryptPeersModel.InsertPeer; +import org.sufficientlysecure.keychain.AutocryptPeersModel.UpdateGossipKey; +import org.sufficientlysecure.keychain.AutocryptPeersModel.UpdateKey; +import org.sufficientlysecure.keychain.AutocryptPeersModel.UpdateLastSeen; +import org.sufficientlysecure.keychain.KeychainDatabase; +import org.sufficientlysecure.keychain.model.AutocryptPeer; +import org.sufficientlysecure.keychain.model.AutocryptPeer.AutocryptKeyStatus; +import org.sufficientlysecure.keychain.model.AutocryptPeer.GossipOrigin; + + +public class AutocryptPeerDao extends AbstractDao { + public static AutocryptPeerDao getInstance(Context context) { + KeychainDatabase keychainDatabase = KeychainDatabase.getInstance(context); + DatabaseNotifyManager databaseNotifyManager = DatabaseNotifyManager.create(context); + + return new AutocryptPeerDao(keychainDatabase, databaseNotifyManager); + } + + private AutocryptPeerDao(KeychainDatabase database, DatabaseNotifyManager databaseNotifyManager) { + super(database, databaseNotifyManager); + } + + public Long getMasterKeyIdForAutocryptPeer(String autocryptId) { + SqlDelightQuery query = AutocryptPeer.FACTORY.selectMasterKeyIdByIdentifier(autocryptId); + try (Cursor cursor = getReadableDb().query(query)) { + if (cursor.moveToFirst()) { + return AutocryptPeer.FACTORY.selectMasterKeyIdByIdentifierMapper().map(cursor); + } + } + return null; + } + + @Nullable + public AutocryptPeer getAutocryptPeer(String packageName, String autocryptId) { + List autocryptPeers = getAutocryptPeers(packageName, autocryptId); + if (!autocryptPeers.isEmpty()) { + return autocryptPeers.get(0); + } + return null; + } + + private List getAutocryptPeers(String packageName, String... autocryptId) { + ArrayList result = new ArrayList<>(autocryptId.length); + SqlDelightQuery query = AutocryptPeer.FACTORY.selectByIdentifiers(packageName, autocryptId); + try (Cursor cursor = getReadableDb().query(query)) { + if (cursor.moveToNext()) { + AutocryptPeer autocryptPeer = AutocryptPeer.PEER_MAPPER.map(cursor); + result.add(autocryptPeer); + } + } + return result; + } + + public List getAutocryptKeyStatus(String packageName, String[] autocryptIds) { + ArrayList result = new ArrayList<>(autocryptIds.length); + SqlDelightQuery query = AutocryptPeer.FACTORY.selectAutocryptKeyStatus(packageName, autocryptIds, System.currentTimeMillis()); + try (Cursor cursor = getReadableDb().query(query)) { + if (cursor.moveToNext()) { + AutocryptKeyStatus autocryptPeer = AutocryptPeer.KEY_STATUS_MAPPER.map(cursor); + result.add(autocryptPeer); + } + } + return result; + } + + private void ensureAutocryptPeerExists(String packageName, String autocryptId) { + InsertPeer insertStatement = new InsertPeer(getWritableDb()); + insertStatement.bind(packageName, autocryptId); + insertStatement.executeInsert(); + } + + public void insertOrUpdateLastSeen(String packageName, String autocryptId, Date date) { + ensureAutocryptPeerExists(packageName, autocryptId); + + UpdateLastSeen updateStatement = new UpdateLastSeen(getWritableDb(), AutocryptPeer.FACTORY); + updateStatement.bind(packageName, autocryptId, date); + updateStatement.executeUpdateDelete(); + } + + public void updateKey(String packageName, String autocryptId, Date effectiveDate, long masterKeyId, + boolean isMutual) { + ensureAutocryptPeerExists(packageName, autocryptId); + + UpdateKey updateStatement = new UpdateKey(getWritableDb(), AutocryptPeer.FACTORY); + updateStatement.bind(packageName, autocryptId, effectiveDate, masterKeyId, isMutual); + updateStatement.executeUpdateDelete(); + + getDatabaseNotifyManager().notifyAutocryptUpdate(autocryptId, masterKeyId); + } + + public void updateKeyGossip(String packageName, String autocryptId, Date effectiveDate, long masterKeyId, + GossipOrigin origin) { + ensureAutocryptPeerExists(packageName, autocryptId); + + UpdateGossipKey updateStatement = new UpdateGossipKey(getWritableDb(), AutocryptPeer.FACTORY); + updateStatement.bind(packageName, autocryptId, effectiveDate, masterKeyId, origin); + updateStatement.executeUpdateDelete(); + + getDatabaseNotifyManager().notifyAutocryptUpdate(autocryptId, masterKeyId); + } + + public List getAutocryptPeersForKey(long masterKeyId) { + ArrayList result = new ArrayList<>(); + SqlDelightQuery query = AutocryptPeer.FACTORY.selectByMasterKeyId(masterKeyId); + try (Cursor cursor = getReadableDb().query(query)) { + if (cursor.moveToNext()) { + AutocryptPeer autocryptPeer = AutocryptPeer.PEER_MAPPER.map(cursor); + result.add(autocryptPeer); + } + } + return result; + } + + public void deleteByIdentifier(String packageName, String autocryptId) { + Long masterKeyId = getMasterKeyIdForAutocryptPeer(autocryptId); + DeleteByIdentifier deleteStatement = new DeleteByIdentifier(getReadableDb()); + deleteStatement.bind(packageName, autocryptId); + deleteStatement.execute(); + if (masterKeyId != null) { + getDatabaseNotifyManager().notifyAutocryptDelete(autocryptId, masterKeyId); + } + } + + public void deleteByMasterKeyId(long masterKeyId) { + DeleteByMasterKeyId deleteStatement = new DeleteByMasterKeyId(getReadableDb()); + deleteStatement.bind(masterKeyId); + deleteStatement.execute(); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/daos/CertificationDao.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/daos/CertificationDao.java new file mode 100644 index 000000000..8343a55f8 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/daos/CertificationDao.java @@ -0,0 +1,35 @@ +package org.sufficientlysecure.keychain.daos; + + +import android.content.Context; +import android.database.Cursor; + +import com.squareup.sqldelight.SqlDelightQuery; +import org.sufficientlysecure.keychain.KeychainDatabase; +import org.sufficientlysecure.keychain.model.Certification; +import org.sufficientlysecure.keychain.model.Certification.CertDetails; + + +public class CertificationDao extends AbstractDao { + public static CertificationDao getInstance(Context context) { + KeychainDatabase keychainDatabase = KeychainDatabase.getInstance(context); + DatabaseNotifyManager databaseNotifyManager = DatabaseNotifyManager.create(context); + + return new CertificationDao(keychainDatabase, databaseNotifyManager); + } + + private CertificationDao(KeychainDatabase keychainDatabase, DatabaseNotifyManager databaseNotifyManager) { + super(keychainDatabase, databaseNotifyManager); + } + + public CertDetails getVerifyingCertDetails(long masterKeyId, int userPacketRank) { + SqlDelightQuery query = Certification.FACTORY.selectVerifyingCertDetails(masterKeyId, userPacketRank); + try (Cursor cursor = getReadableDb().query(query)) { + if (cursor.moveToFirst()) { + return Certification.CERT_DETAILS_MAPPER.map(cursor); + } + } + return null; + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/daos/DatabaseNotifyManager.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/daos/DatabaseNotifyManager.java new file mode 100644 index 000000000..ee81a5f14 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/daos/DatabaseNotifyManager.java @@ -0,0 +1,66 @@ +package org.sufficientlysecure.keychain.daos; + + +import android.content.ContentResolver; +import android.content.Context; +import android.net.Uri; + +import org.sufficientlysecure.keychain.Constants; + + +public class DatabaseNotifyManager { + private static final Uri URI_KEYS = Uri.parse("content://" + Constants.PROVIDER_AUTHORITY + "/keys"); + private static final Uri URI_APPS = Uri.parse("content://" + Constants.PROVIDER_AUTHORITY + "/apps"); + + private ContentResolver contentResolver; + + public static DatabaseNotifyManager create(Context context) { + ContentResolver contentResolver = context.getContentResolver(); + return new DatabaseNotifyManager(contentResolver); + } + + private DatabaseNotifyManager(ContentResolver contentResolver) { + this.contentResolver = contentResolver; + } + + public void notifyKeyChange(long masterKeyId) { + Uri uri = getNotifyUriMasterKeyId(masterKeyId); + contentResolver.notifyChange(uri, null); + } + + public void notifyAutocryptDelete(String autocryptId, Long masterKeyId) { + Uri uri = getNotifyUriMasterKeyId(masterKeyId); + contentResolver.notifyChange(uri, null); + } + + public void notifyAutocryptUpdate(String autocryptId, long masterKeyId) { + Uri uri = getNotifyUriMasterKeyId(masterKeyId); + contentResolver.notifyChange(uri, null); + } + + public void notifyKeyMetadataChange(long masterKeyId) { + Uri uri = getNotifyUriMasterKeyId(masterKeyId); + contentResolver.notifyChange(uri, null); + } + + public void notifyApiAppChange(String apiApp) { + Uri uri = getNotifyUriPackageName(apiApp); + contentResolver.notifyChange(uri, null); + } + + public static Uri getNotifyUriAllKeys() { + return URI_KEYS; + } + + public static Uri getNotifyUriMasterKeyId(long masterKeyId) { + return URI_KEYS.buildUpon().appendPath(Long.toString(masterKeyId)).build(); + } + + public static Uri getNotifyUriAllApps() { + return URI_APPS; + } + + public static Uri getNotifyUriPackageName(String packageName) { + return URI_APPS.buildUpon().appendPath(packageName).build(); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/daos/KeyMetadataDao.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/daos/KeyMetadataDao.java new file mode 100644 index 000000000..bd2c37f3d --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/daos/KeyMetadataDao.java @@ -0,0 +1,64 @@ +package org.sufficientlysecure.keychain.daos; + + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import android.content.Context; +import android.database.Cursor; + +import com.squareup.sqldelight.SqlDelightQuery; +import org.sufficientlysecure.keychain.KeyMetadataModel.ReplaceKeyMetadata; +import org.sufficientlysecure.keychain.KeychainDatabase; +import org.sufficientlysecure.keychain.model.KeyMetadata; + + +public class KeyMetadataDao extends AbstractDao { + public static KeyMetadataDao create(Context context) { + KeychainDatabase database = KeychainDatabase.getInstance(context); + DatabaseNotifyManager databaseNotifyManager = DatabaseNotifyManager.create(context); + + return new KeyMetadataDao(database, databaseNotifyManager); + } + + private KeyMetadataDao(KeychainDatabase database, DatabaseNotifyManager databaseNotifyManager) { + super(database, databaseNotifyManager); + } + + public KeyMetadata getKeyMetadata(long masterKeyId) { + SqlDelightQuery query = KeyMetadata.FACTORY.selectByMasterKeyId(masterKeyId); + try (Cursor cursor = getReadableDb().query(query)) { + if (cursor.moveToFirst()) { + return KeyMetadata.FACTORY.selectByMasterKeyIdMapper().map(cursor); + } + } + return null; + } + + public void resetAllLastUpdatedTimes() { + new KeyMetadata.DeleteAllLastUpdatedTimes(getWritableDb()).execute(); + } + + public void renewKeyLastUpdatedTime(long masterKeyId, boolean seenOnKeyservers) { + ReplaceKeyMetadata replaceStatement = new ReplaceKeyMetadata(getWritableDb(), KeyMetadata.FACTORY); + replaceStatement.bind(masterKeyId, new Date(), seenOnKeyservers); + replaceStatement.executeInsert(); + + getDatabaseNotifyManager().notifyKeyMetadataChange(masterKeyId); + } + + public List getFingerprintsForKeysOlderThan(long olderThan, TimeUnit timeUnit) { + SqlDelightQuery query = KeyMetadata.FACTORY.selectFingerprintsForKeysOlderThan(new Date(timeUnit.toMillis(olderThan))); + + List fingerprintList = new ArrayList<>(); + try (Cursor cursor = getReadableDb().query(query)) { + while (cursor.moveToNext()) { + byte[] fingerprint = KeyMetadata.FACTORY.selectFingerprintsForKeysOlderThanMapper().map(cursor); + fingerprintList.add(fingerprint); + } + } + return fingerprintList; + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/daos/KeyRepository.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/daos/KeyRepository.java new file mode 100644 index 000000000..d31340d4d --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/daos/KeyRepository.java @@ -0,0 +1,275 @@ +/* + * Copyright (C) 2017 Schürmann & Breitmoser GbR + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.daos; + + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.List; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.Cursor; +import android.support.annotation.WorkerThread; + +import com.squareup.sqldelight.SqlDelightQuery; +import org.bouncycastle.bcpg.ArmoredOutputStream; +import org.sufficientlysecure.keychain.KeychainDatabase; +import org.sufficientlysecure.keychain.model.Certification; +import org.sufficientlysecure.keychain.model.KeyRingPublic; +import org.sufficientlysecure.keychain.model.KeySignature; +import org.sufficientlysecure.keychain.model.SubKey; +import org.sufficientlysecure.keychain.model.SubKey.UnifiedKeyInfo; +import org.sufficientlysecure.keychain.model.UserPacket; +import org.sufficientlysecure.keychain.model.UserPacket.UserId; +import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType; +import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; +import org.sufficientlysecure.keychain.pgp.CanonicalizedKeyRing.VerificationStatus; +import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKeyRing; +import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey.SecretKeyType; +import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKeyRing; +import timber.log.Timber; + + +@WorkerThread +public class KeyRepository extends AbstractDao { + final ContentResolver contentResolver; + final LocalPublicKeyStorage mLocalPublicKeyStorage; + final LocalSecretKeyStorage localSecretKeyStorage; + + OperationLog mLog; + int mIndent; + + public static KeyRepository create(Context context) { + ContentResolver contentResolver = context.getContentResolver(); + LocalPublicKeyStorage localPublicKeyStorage = LocalPublicKeyStorage.getInstance(context); + LocalSecretKeyStorage localSecretKeyStorage = LocalSecretKeyStorage.getInstance(context); + KeychainDatabase database = KeychainDatabase.getInstance(context); + DatabaseNotifyManager databaseNotifyManager = DatabaseNotifyManager.create(context); + + return new KeyRepository(contentResolver, database, databaseNotifyManager, localPublicKeyStorage, localSecretKeyStorage); + } + + private KeyRepository(ContentResolver contentResolver, KeychainDatabase database, + DatabaseNotifyManager databaseNotifyManager, + LocalPublicKeyStorage localPublicKeyStorage, + LocalSecretKeyStorage localSecretKeyStorage) { + this(contentResolver, database, databaseNotifyManager, localPublicKeyStorage, localSecretKeyStorage, new OperationLog(), 0); + } + + KeyRepository(ContentResolver contentResolver, KeychainDatabase database, + DatabaseNotifyManager databaseNotifyManager, + LocalPublicKeyStorage localPublicKeyStorage, + LocalSecretKeyStorage localSecretKeyStorage, + OperationLog log, int indent) { + super(database, databaseNotifyManager); + this.contentResolver = contentResolver; + mLocalPublicKeyStorage = localPublicKeyStorage; + this.localSecretKeyStorage = localSecretKeyStorage; + mIndent = indent; + mLog = log; + } + + public OperationLog getLog() { + return mLog; + } + + public void log(LogType type) { + if (mLog != null) { + mLog.add(type, mIndent); + } + } + + public void log(LogType type, Object... parameters) { + if (mLog != null) { + mLog.add(type, mIndent, parameters); + } + } + + public void clearLog() { + mLog = new OperationLog(); + } + + public CanonicalizedPublicKeyRing getCanonicalizedPublicKeyRing(long masterKeyId) throws NotFoundException { + UnifiedKeyInfo unifiedKeyInfo = getUnifiedKeyInfo(masterKeyId); + if (unifiedKeyInfo == null) { + throw new NotFoundException(); + } + + byte[] publicKeyData = loadPublicKeyRingData(masterKeyId); + return new CanonicalizedPublicKeyRing(publicKeyData, unifiedKeyInfo.verified()); + } + + public CanonicalizedSecretKeyRing getCanonicalizedSecretKeyRing(long masterKeyId) throws NotFoundException { + UnifiedKeyInfo unifiedKeyInfo = getUnifiedKeyInfo(masterKeyId); + if (unifiedKeyInfo == null || !unifiedKeyInfo.has_any_secret()) { + throw new NotFoundException(); + } + byte[] secretKeyData = loadSecretKeyRingData(masterKeyId); + if (secretKeyData == null) { + throw new IllegalStateException("Missing expected secret key data!"); + } + return new CanonicalizedSecretKeyRing(secretKeyData, unifiedKeyInfo.verified()); + } + + public List getAllMasterKeyIds() { + SqlDelightQuery query = KeyRingPublic.FACTORY.selectAllMasterKeyIds(); + return mapAllRows(query, KeyRingPublic.FACTORY.selectAllMasterKeyIdsMapper()::map); + } + + public List getMasterKeyIdsBySigner(List signerMasterKeyIds) { + long[] signerKeyIds = getLongListAsArray(signerMasterKeyIds); + SqlDelightQuery query = KeySignature.FACTORY.selectMasterKeyIdsBySigner(signerKeyIds); + return mapAllRows(query, KeySignature.FACTORY.selectMasterKeyIdsBySignerMapper()::map); + } + + public Long getMasterKeyIdBySubkeyId(long subKeyId) { + SqlDelightQuery query = SubKey.FACTORY.selectMasterKeyIdBySubkey(subKeyId); + return mapSingleRow(query, SubKey.FACTORY.selectMasterKeyIdBySubkeyMapper()::map); + } + + public UnifiedKeyInfo getUnifiedKeyInfo(long masterKeyId) { + SqlDelightQuery query = SubKey.FACTORY.selectUnifiedKeyInfoByMasterKeyId(masterKeyId); + return mapSingleRow(query, SubKey.UNIFIED_KEY_INFO_MAPPER::map); + } + + public List getUnifiedKeyInfo(long... masterKeyIds) { + SqlDelightQuery query = SubKey.FACTORY.selectUnifiedKeyInfoByMasterKeyIds(masterKeyIds); + return mapAllRows(query, SubKey.UNIFIED_KEY_INFO_MAPPER::map); + } + + public List getUnifiedKeyInfosByMailAddress(String mailAddress) { + SqlDelightQuery query = SubKey.FACTORY.selectUnifiedKeyInfoSearchMailAddress('%' + mailAddress + '%'); + return mapAllRows(query, SubKey.UNIFIED_KEY_INFO_MAPPER::map); + } + + public List getAllUnifiedKeyInfo() { + SqlDelightQuery query = SubKey.FACTORY.selectAllUnifiedKeyInfo(); + return mapAllRows(query, SubKey.UNIFIED_KEY_INFO_MAPPER::map); + } + + public List getAllUnifiedKeyInfoWithSecret() { + SqlDelightQuery query = SubKey.FACTORY.selectAllUnifiedKeyInfoWithSecret(); + return mapAllRows(query, SubKey.UNIFIED_KEY_INFO_MAPPER::map); + } + + public List getUserIds(long... masterKeyIds) { + SqlDelightQuery query = UserPacket.FACTORY.selectUserIdsByMasterKeyId(masterKeyIds); + return mapAllRows(query, UserPacket.USER_ID_MAPPER::map); + } + + public List getConfirmedUserIds(long masterKeyId) { + SqlDelightQuery query = UserPacket.FACTORY.selectUserIdsByMasterKeyIdAndVerification( + Certification.FACTORY, masterKeyId, VerificationStatus.VERIFIED_SECRET); + return mapAllRows(query, (cursor) -> UserPacket.USER_ID_MAPPER.map(cursor).user_id()); + } + + public List getSubKeysByMasterKeyId(long masterKeyId) { + SqlDelightQuery query = SubKey.FACTORY.selectSubkeysByMasterKeyId(masterKeyId); + return mapAllRows(query, SubKey.SUBKEY_MAPPER::map); + } + + public SecretKeyType getSecretKeyType(long keyId) throws NotFoundException { + SqlDelightQuery query = SubKey.FACTORY.selectSecretKeyType(keyId); + return mapSingleRowOrThrow(query, SubKey.SKT_MAPPER::map); + } + + public byte[] getFingerprintByKeyId(long keyId) throws NotFoundException { + SqlDelightQuery query = SubKey.FACTORY.selectFingerprintByKeyId(keyId); + return mapSingleRowOrThrow(query, SubKey.FACTORY.selectFingerprintByKeyIdMapper()::map); + } + + private byte[] getKeyRingAsArmoredData(byte[] data) throws IOException { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + ArmoredOutputStream aos = new ArmoredOutputStream(bos); + + aos.write(data); + aos.close(); + + return bos.toByteArray(); + } + + public String getPublicKeyRingAsArmoredString(long masterKeyId) throws NotFoundException, IOException { + byte[] data = loadPublicKeyRingData(masterKeyId); + byte[] armoredData = getKeyRingAsArmoredData(data); + return new String(armoredData); + } + + public byte[] getSecretKeyRingAsArmoredData(long masterKeyId) throws NotFoundException, IOException { + byte[] data = loadSecretKeyRingData(masterKeyId); + return getKeyRingAsArmoredData(data); + } + + public ContentResolver getContentResolver() { + return contentResolver; + } + + public final byte[] loadPublicKeyRingData(long masterKeyId) throws NotFoundException { + SqlDelightQuery query = KeyRingPublic.FACTORY.selectByMasterKeyId(masterKeyId); + try (Cursor cursor = getReadableDb().query(query)) { + if (cursor.moveToFirst()) { + KeyRingPublic keyRingPublic = KeyRingPublic.MAPPER.map(cursor); + byte[] keyRingData = keyRingPublic.key_ring_data(); + if (keyRingData == null) { + keyRingData = mLocalPublicKeyStorage.readPublicKey(masterKeyId); + } + return keyRingData; + } + } catch (IOException e) { + Timber.e(e, "Error reading public key from storage!"); + } + throw new NotFoundException(); + } + + public final byte[] loadSecretKeyRingData(long masterKeyId) throws NotFoundException { + try { + return localSecretKeyStorage.readSecretKey(masterKeyId); + } catch (IOException e) { + Timber.e(e, "Error reading secret key from storage!"); + throw new NotFoundException(); + } + } + + public long getSecretSignId(long masterKeyId) throws NotFoundException { + SqlDelightQuery query = SubKey.FACTORY.selectEffectiveSignKeyIdByMasterKeyId(masterKeyId); + return mapSingleRowOrThrow(query, SubKey.FACTORY.selectEffectiveSignKeyIdByMasterKeyIdMapper()::map); + } + + public long getSecretAuthenticationId(long masterKeyId) throws NotFoundException { + SqlDelightQuery query = SubKey.FACTORY.selectEffectiveAuthKeyIdByMasterKeyId(masterKeyId); + return mapSingleRowOrThrow(query, SubKey.FACTORY.selectEffectiveAuthKeyIdByMasterKeyIdMapper()::map); + } + + public static class NotFoundException extends Exception { + public NotFoundException() { + } + + public NotFoundException(String name) { + super(name); + } + } + + private long[] getLongListAsArray(List longList) { + long[] longs = new long[longList.size()]; + int i = 0; + for (Long aLong : longList) { + longs[i++] = aLong; + } + return longs; + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeyWritableRepository.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/daos/KeyWritableRepository.java similarity index 85% rename from OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeyWritableRepository.java rename to OpenKeychain/src/main/java/org/sufficientlysecure/keychain/daos/KeyWritableRepository.java index 373332d26..4a4d2a58f 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeyWritableRepository.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/daos/KeyWritableRepository.java @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package org.sufficientlysecure.keychain.provider; +package org.sufficientlysecure.keychain.daos; import java.io.IOException; @@ -29,20 +29,23 @@ import android.content.ContentProviderOperation; import android.content.ContentValues; import android.content.Context; import android.content.OperationApplicationException; -import android.database.Cursor; import android.net.Uri; import android.os.RemoteException; import android.support.annotation.NonNull; -import android.support.annotation.VisibleForTesting; import android.support.v4.util.LongSparseArray; import org.openintents.openpgp.util.OpenPgpUtils; +import org.sufficientlysecure.keychain.KeyRingsPublicModel.DeleteByMasterKeyId; +import org.sufficientlysecure.keychain.KeychainDatabase; import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.model.CustomColumnAdapters; +import org.sufficientlysecure.keychain.model.SubKey.UnifiedKeyInfo; import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType; import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; import org.sufficientlysecure.keychain.operations.results.SaveKeyringResult; import org.sufficientlysecure.keychain.operations.results.UpdateTrustResult; import org.sufficientlysecure.keychain.pgp.CanonicalizedKeyRing; +import org.sufficientlysecure.keychain.pgp.CanonicalizedKeyRing.VerificationStatus; import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKey; import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKeyRing; import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey; @@ -55,13 +58,11 @@ import org.sufficientlysecure.keychain.pgp.UncachedPublicKey; import org.sufficientlysecure.keychain.pgp.WrappedSignature; import org.sufficientlysecure.keychain.pgp.WrappedUserAttribute; import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; -import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAutocryptPeer; +import org.sufficientlysecure.keychain.provider.KeychainContract; import org.sufficientlysecure.keychain.provider.KeychainContract.Certs; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRingData; -import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; import org.sufficientlysecure.keychain.provider.KeychainContract.KeySignatures; import org.sufficientlysecure.keychain.provider.KeychainContract.Keys; -import org.sufficientlysecure.keychain.provider.KeychainContract.UpdatedKeys; import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; import org.sufficientlysecure.keychain.util.IterableIterator; @@ -84,69 +85,56 @@ public class KeyWritableRepository extends KeyRepository { private static final int MAX_CACHED_KEY_SIZE = 1024 * 50; private final Context context; - private final LastUpdateInteractor lastUpdateInteractor; - private DatabaseNotifyManager databaseNotifyManager; + private final DatabaseNotifyManager databaseNotifyManager; + private AutocryptPeerDao autocryptPeerDao; public static KeyWritableRepository create(Context context) { LocalPublicKeyStorage localPublicKeyStorage = LocalPublicKeyStorage.getInstance(context); - LastUpdateInteractor lastUpdateInteractor = LastUpdateInteractor.create(context); + LocalSecretKeyStorage localSecretKeyStorage = LocalSecretKeyStorage.getInstance(context); DatabaseNotifyManager databaseNotifyManager = DatabaseNotifyManager.create(context); + AutocryptPeerDao autocryptPeerDao = AutocryptPeerDao.getInstance(context); + KeychainDatabase database = KeychainDatabase.getInstance(context); - return new KeyWritableRepository(context, localPublicKeyStorage, lastUpdateInteractor, - databaseNotifyManager); + return new KeyWritableRepository(context, database, + localPublicKeyStorage, localSecretKeyStorage, databaseNotifyManager, autocryptPeerDao); + } + + private KeyWritableRepository(Context context, + KeychainDatabase database, LocalPublicKeyStorage localPublicKeyStorage, + LocalSecretKeyStorage localSecretKeyStorage, + DatabaseNotifyManager databaseNotifyManager, AutocryptPeerDao autocryptPeerDao) { + this(context, database, localPublicKeyStorage, localSecretKeyStorage, databaseNotifyManager, new OperationLog(), 0, + autocryptPeerDao); } - @VisibleForTesting - KeyWritableRepository(Context context, LocalPublicKeyStorage localPublicKeyStorage, - LastUpdateInteractor lastUpdateInteractor, DatabaseNotifyManager databaseNotifyManager) { - this(context, localPublicKeyStorage, lastUpdateInteractor, new OperationLog(), 0, - databaseNotifyManager); - } - - private KeyWritableRepository(Context context, LocalPublicKeyStorage localPublicKeyStorage, - LastUpdateInteractor lastUpdateInteractor, OperationLog log, int indent, - DatabaseNotifyManager databaseNotifyManager) { - super(context.getContentResolver(), localPublicKeyStorage, log, indent); + private KeyWritableRepository(Context context, KeychainDatabase database, + LocalPublicKeyStorage localPublicKeyStorage, + LocalSecretKeyStorage localSecretKeyStorage, DatabaseNotifyManager databaseNotifyManager, + OperationLog log, int indent, AutocryptPeerDao autocryptPeerDao) { + super(context.getContentResolver(), database, databaseNotifyManager, localPublicKeyStorage, localSecretKeyStorage, log, indent); this.context = context; this.databaseNotifyManager = databaseNotifyManager; - this.lastUpdateInteractor = lastUpdateInteractor; + this.autocryptPeerDao = autocryptPeerDao; } private LongSparseArray getTrustedMasterKeys() { - Cursor cursor = contentResolver.query(KeyRings.buildUnifiedKeyRingsUri(), new String[] { - KeyRings.MASTER_KEY_ID, - // we pick from cache only information that is not easily available from keyrings - KeyRings.HAS_ANY_SECRET, KeyRings.VERIFIED - }, KeyRings.HAS_ANY_SECRET + " = 1", null, null); + LongSparseArray result = new LongSparseArray<>(); - try { - LongSparseArray result = new LongSparseArray<>(); - - if (cursor == null) { - return result; - } - - while (cursor.moveToNext()) { - try { - long masterKeyId = cursor.getLong(0); - int verified = cursor.getInt(2); - byte[] blob = loadPublicKeyRingData(masterKeyId); - if (blob != null) { - result.put(masterKeyId, new CanonicalizedPublicKeyRing(blob, verified).getPublicKey()); - } - } catch (NotFoundException e) { - throw new IllegalStateException("Error reading secret key data, this should not happen!", e); + List unifiedKeyInfoWithSecret = getAllUnifiedKeyInfoWithSecret(); + for (UnifiedKeyInfo unifiedKeyInfo : unifiedKeyInfoWithSecret) { + try { + byte[] blob = loadPublicKeyRingData(unifiedKeyInfo.master_key_id()); + if (blob != null) { + result.put(unifiedKeyInfo.master_key_id(), + new CanonicalizedPublicKeyRing(blob, unifiedKeyInfo.verified()).getPublicKey()); } - } - - return result; - } finally { - if (cursor != null) { - cursor.close(); + } catch (NotFoundException e) { + throw new IllegalStateException("Error reading secret key data, this should not happen!", e); } } + return result; } // bits, in order: CESA. make SURE these are correct, we will get bad log entries otherwise!! @@ -322,15 +310,17 @@ public class KeyWritableRepository extends KeyRepository { } - // do we have a trusted key for this? - if (trustedKeys.indexOfKey(certId) < 0) { - if (!signerKeyIds.contains(certId)) { - operations.add(ContentProviderOperation.newInsert(KeySignatures.CONTENT_URI) - .withValue(KeySignatures.MASTER_KEY_ID, masterKeyId) - .withValue(KeySignatures.SIGNER_KEY_ID, certId) - .build()); - signerKeyIds.add(certId); - } + // keep a note about the issuer of this key signature + if (!signerKeyIds.contains(certId)) { + operations.add(ContentProviderOperation.newInsert(KeySignatures.CONTENT_URI) + .withValue(KeySignatures.MASTER_KEY_ID, masterKeyId) + .withValue(KeySignatures.SIGNER_KEY_ID, certId) + .build()); + signerKeyIds.add(certId); + } + + boolean isSignatureFromTrustedKey = trustedKeys.indexOfKey(certId) >= 0; + if (!isSignatureFromTrustedKey) { unknownCerts += 1; continue; } @@ -495,7 +485,7 @@ public class KeyWritableRepository extends KeyRepository { if (item.selfRevocation != null) { operations.add(buildCertOperations(masterKeyId, userIdRank, item.selfRevocation, - Certs.VERIFIED_SELF)); + VerificationStatus.VERIFIED_SELF)); // don't bother with trusted certs if the uid is revoked, anyways continue; } @@ -505,7 +495,7 @@ public class KeyWritableRepository extends KeyRepository { } operations.add(buildCertOperations(masterKeyId, userIdRank, item.selfCert, - selfCertsAreTrusted ? Certs.VERIFIED_SECRET : Certs.VERIFIED_SELF)); + selfCertsAreTrusted ? VerificationStatus.VERIFIED_SECRET : VerificationStatus.VERIFIED_SELF)); // iterate over signatures for (int i = 0; i < item.trustedCerts.size(); i++) { @@ -517,7 +507,7 @@ public class KeyWritableRepository extends KeyRepository { } // otherwise, build database operation operations.add(buildCertOperations( - masterKeyId, userIdRank, sig, Certs.VERIFIED_SECRET)); + masterKeyId, userIdRank, sig, VerificationStatus.VERIFIED_SECRET)); } } @@ -529,16 +519,13 @@ public class KeyWritableRepository extends KeyRepository { mIndent -= 1; } - ContentProviderOperation lastUpdateReinsertOp = getLastUpdatedReinsertOperationByMasterKeyId(masterKeyId); - if (lastUpdateReinsertOp != null) { - operations.add(lastUpdateReinsertOp); - } - try { // delete old version of this keyRing (from database only!), which also deletes all keys and userIds on cascade - int deleted = contentResolver.delete( - KeyRingData.buildPublicKeyRingUri(masterKeyId), null, null); - if (deleted > 0) { + DeleteByMasterKeyId deleteStatement = new DeleteByMasterKeyId(getWritableDb()); + deleteStatement.bind(masterKeyId); + int deletedRows = deleteStatement.executeUpdateDelete(); + + if (deletedRows > 0) { log(LogType.MSG_IP_DELETE_OLD_OK); result |= SaveKeyringResult.UPDATED; } else { @@ -564,26 +551,6 @@ public class KeyWritableRepository extends KeyRepository { } - private ContentProviderOperation getLastUpdatedReinsertOperationByMasterKeyId(long masterKeyId) { - Long lastUpdateTime = getLastUpdateTime(masterKeyId); - if (lastUpdateTime == null) { - return null; - } - - Boolean seenOnKeyservers = lastUpdateInteractor.getSeenOnKeyservers(masterKeyId); - - ContentValues lastUpdatedEntry = new ContentValues(2); - lastUpdatedEntry.put(UpdatedKeys.MASTER_KEY_ID, masterKeyId); - lastUpdatedEntry.put(UpdatedKeys.LAST_UPDATED, lastUpdateTime); - if (seenOnKeyservers != null){ - lastUpdatedEntry.put(UpdatedKeys.SEEN_ON_KEYSERVERS, seenOnKeyservers); - } - return ContentProviderOperation - .newInsert(UpdatedKeys.CONTENT_URI) - .withValues(lastUpdatedEntry) - .build(); - } - private void writePublicKeyRing(CanonicalizedPublicKeyRing keyRing, long masterKeyId, ArrayList operations) throws IOException { byte[] encodedKey = keyRing.getEncoded(); @@ -601,27 +568,24 @@ public class KeyWritableRepository extends KeyRepository { operations.add(ContentProviderOperation.newInsert(uri).withValues(values).build()); } - private Uri writeSecretKeyRing(CanonicalizedSecretKeyRing keyRing, long masterKeyId) throws IOException { + private void writeSecretKeyRing(CanonicalizedSecretKeyRing keyRing, long masterKeyId) throws IOException { byte[] encodedKey = keyRing.getEncoded(); - - ContentValues values = new ContentValues(); - values.put(KeyRingData.MASTER_KEY_ID, masterKeyId); - values.put(KeyRingData.KEY_RING_DATA, encodedKey); - - // insert new version of this keyRing - Uri uri = KeyRingData.buildSecretKeyRingUri(masterKeyId); - return contentResolver.insert(uri, values); + localSecretKeyStorage.writeSecretKey(masterKeyId, encodedKey); } public boolean deleteKeyRing(long masterKeyId) { try { mLocalPublicKeyStorage.deletePublicKey(masterKeyId); + localSecretKeyStorage.deleteSecretKey(masterKeyId); } catch (IOException e) { Timber.e(e, "Could not delete file!"); return false; } - contentResolver.delete(ApiAutocryptPeer.buildByMasterKeyId(masterKeyId),null, null); - int deletedRows = contentResolver.delete(KeyRingData.buildPublicKeyRingUri(masterKeyId), null, null); + autocryptPeerDao.deleteByMasterKeyId(masterKeyId); + + DeleteByMasterKeyId deleteStatement = new DeleteByMasterKeyId(getWritableDb()); + deleteStatement.bind(masterKeyId); + int deletedRows = deleteStatement.executeUpdateDelete(); databaseNotifyManager.notifyKeyChange(masterKeyId); @@ -683,11 +647,7 @@ public class KeyWritableRepository extends KeyRepository { // save secret keyring try { - Uri insertedUri = writeSecretKeyRing(keyRing, masterKeyId); - if (insertedUri == null) { - log(LogType.MSG_IS_DB_EXCEPTION); - return SaveKeyringResult.RESULT_ERROR; - } + writeSecretKeyRing(keyRing, masterKeyId); } catch (IOException e) { Timber.e(e, "Failed to encode key!"); log(LogType.MSG_IS_ERROR_IO_EXC); @@ -1039,30 +999,18 @@ public class KeyWritableRepository extends KeyRepository { log.add(LogType.MSG_TRUST, 0); - Cursor cursor; Preferences preferences = Preferences.getPreferences(context); boolean isTrustDbInitialized = preferences.isKeySignaturesTableInitialized(); + + List masterKeyIds; if (!isTrustDbInitialized) { log.add(LogType.MSG_TRUST_INITIALIZE, 1); - cursor = contentResolver.query(KeyRings.buildUnifiedKeyRingsUri(), - new String[] { KeyRings.MASTER_KEY_ID }, null, null, null); + masterKeyIds = getAllMasterKeyIds(); } else { - String[] signerMasterKeyIdStrings = new String[signerMasterKeyIds.size()]; - int i = 0; - for (Long masterKeyId : signerMasterKeyIds) { - log.add(LogType.MSG_TRUST_KEY, 1, KeyFormattingUtils.beautifyKeyId(masterKeyId)); - signerMasterKeyIdStrings[i++] = Long.toString(masterKeyId); - } - - cursor = contentResolver.query(KeyRings.buildUnifiedKeyRingsFilterBySigner(), - new String[] { KeyRings.MASTER_KEY_ID }, null, signerMasterKeyIdStrings, null); + masterKeyIds = getMasterKeyIdsBySigner(signerMasterKeyIds); } - if (cursor == null) { - throw new IllegalStateException(); - } - - int totalKeys = cursor.getCount(); + int totalKeys = masterKeyIds.size(); int processedKeys = 0; if (totalKeys == 0) { @@ -1072,41 +1020,37 @@ public class KeyWritableRepository extends KeyRepository { log.add(LogType.MSG_TRUST_COUNT, 1, totalKeys); } - try { - while (cursor.moveToNext()) { - try { - long masterKeyId = cursor.getLong(0); + for (long masterKeyId : masterKeyIds) { + try { + log.add(LogType.MSG_TRUST_KEY, 1, KeyFormattingUtils.beautifyKeyId(masterKeyId)); - byte[] pubKeyData = loadPublicKeyRingData(masterKeyId); - UncachedKeyRing uncachedKeyRing = UncachedKeyRing.decodeFromData(pubKeyData); + byte[] pubKeyData = loadPublicKeyRingData(masterKeyId); + UncachedKeyRing uncachedKeyRing = UncachedKeyRing.decodeFromData(pubKeyData); - clearLog(); - SaveKeyringResult result = savePublicKeyRing(uncachedKeyRing, true); + clearLog(); + SaveKeyringResult result = savePublicKeyRing(uncachedKeyRing, true); - log.add(result, 1); - progress.setProgress(processedKeys++, totalKeys); - } catch (NotFoundException | PgpGeneralException | IOException e) { - Timber.e(e, "Error updating trust database"); - return new UpdateTrustResult(UpdateTrustResult.RESULT_ERROR, log); - } + log.add(result, 1); + progress.setProgress(processedKeys++, totalKeys); + } catch (NotFoundException | PgpGeneralException | IOException e) { + Timber.e(e, "Error updating trust database"); + return new UpdateTrustResult(UpdateTrustResult.RESULT_ERROR, log); } - - if (!isTrustDbInitialized) { - preferences.setKeySignaturesTableInitialized(); - } - - log.add(LogType.MSG_TRUST_OK, 1); - return new UpdateTrustResult(UpdateTrustResult.RESULT_OK, log); - } finally { - cursor.close(); } + + if (!isTrustDbInitialized) { + preferences.setKeySignaturesTableInitialized(); + } + + log.add(LogType.MSG_TRUST_OK, 1); + return new UpdateTrustResult(UpdateTrustResult.RESULT_OK, log); } /** * Build ContentProviderOperation to add PGPPublicKey to database corresponding to a keyRing */ private ContentProviderOperation - buildCertOperations(long masterKeyId, int rank, WrappedSignature cert, int verified) + buildCertOperations(long masterKeyId, int rank, WrappedSignature cert, VerificationStatus verificationStatus) throws IOException { ContentValues values = new ContentValues(); values.put(Certs.MASTER_KEY_ID, masterKeyId); @@ -1114,7 +1058,7 @@ public class KeyWritableRepository extends KeyRepository { values.put(Certs.KEY_ID_CERTIFIER, cert.getKeyId()); values.put(Certs.TYPE, cert.getSignatureType()); values.put(Certs.CREATION, cert.getCreationTime().getTime() / 1000); - values.put(Certs.VERIFIED, verified); + values.put(Certs.VERIFIED, CustomColumnAdapters.VERIFICATON_STATUS_ADAPTER.encode(verificationStatus)); values.put(Certs.DATA, cert.getEncoded()); Uri uri = Certs.buildCertsUri(masterKeyId); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/LocalPublicKeyStorage.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/daos/LocalPublicKeyStorage.java similarity index 98% rename from OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/LocalPublicKeyStorage.java rename to OpenKeychain/src/main/java/org/sufficientlysecure/keychain/daos/LocalPublicKeyStorage.java index ff900f80f..61576e79f 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/LocalPublicKeyStorage.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/daos/LocalPublicKeyStorage.java @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package org.sufficientlysecure.keychain.provider; +package org.sufficientlysecure.keychain.daos; import java.io.ByteArrayOutputStream; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/daos/LocalSecretKeyStorage.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/daos/LocalSecretKeyStorage.java new file mode 100644 index 000000000..76516b24a --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/daos/LocalSecretKeyStorage.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2017 Schürmann & Breitmoser GbR + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.daos; + + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; + +import android.content.Context; + +import okhttp3.internal.Util; + + +public class LocalSecretKeyStorage { + private static final String FORMAT_STR_SECRET_KEY = "0x%016x.sec"; + private static final String SECRET_KEYS_DIR_NAME = "secret_keys"; + + + private final File localSecretKeysDir; + + + public static LocalSecretKeyStorage getInstance(Context context) { + File localSecretKeysDir = new File(context.getFilesDir(), SECRET_KEYS_DIR_NAME); + return new LocalSecretKeyStorage(localSecretKeysDir); + } + + private LocalSecretKeyStorage(File localSecretKeysDir) { + this.localSecretKeysDir = localSecretKeysDir; + } + + private File getSecretKeyFile(long masterKeyId) throws IOException { + if (!localSecretKeysDir.exists()) { + localSecretKeysDir.mkdir(); + } + if (!localSecretKeysDir.isDirectory()) { + throw new IOException("Failed creating public key directory!"); + } + + String keyFilename = String.format(FORMAT_STR_SECRET_KEY, masterKeyId); + return new File(localSecretKeysDir, keyFilename); + } + + public void writeSecretKey(long masterKeyId, byte[] encoded) throws IOException { + File publicKeyFile = getSecretKeyFile(masterKeyId); + + FileOutputStream fileOutputStream = new FileOutputStream(publicKeyFile); + try { + fileOutputStream.write(encoded); + } finally { + Util.closeQuietly(fileOutputStream); + } + } + + byte[] readSecretKey(long masterKeyId) throws IOException { + File publicKeyFile = getSecretKeyFile(masterKeyId); + + try { + FileInputStream fileInputStream = new FileInputStream(publicKeyFile); + return readIntoByteArray(fileInputStream); + } catch (FileNotFoundException e) { + return null; + } + } + + private static byte[] readIntoByteArray(FileInputStream fileInputStream) throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + byte[] buf = new byte[128]; + int bytesRead; + while ((bytesRead = fileInputStream.read(buf)) != -1) { + baos.write(buf, 0, bytesRead); + } + + return baos.toByteArray(); + } + + void deleteSecretKey(long masterKeyId) throws IOException { + File publicKeyFile = getSecretKeyFile(masterKeyId); + if (publicKeyFile.exists()) { + boolean deleteSuccess = publicKeyFile.delete(); + if (!deleteSuccess) { + throw new IOException("File exists, but could not be deleted!"); + } + } + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/daos/OverriddenWarningsDao.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/daos/OverriddenWarningsDao.java new file mode 100644 index 000000000..12554f1a3 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/daos/OverriddenWarningsDao.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2017 Schürmann & Breitmoser GbR + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.daos; + + +import android.content.Context; + +import com.squareup.sqldelight.SqlDelightQuery; +import org.sufficientlysecure.keychain.KeychainDatabase; +import org.sufficientlysecure.keychain.OverriddenWarningsModel.DeleteByIdentifier; +import org.sufficientlysecure.keychain.OverriddenWarningsModel.InsertIdentifier; +import org.sufficientlysecure.keychain.model.OverriddenWarning; + + +public class OverriddenWarningsDao extends AbstractDao { + public static OverriddenWarningsDao create(Context context) { + KeychainDatabase database = KeychainDatabase.getInstance(context); + DatabaseNotifyManager databaseNotifyManager = DatabaseNotifyManager.create(context); + + return new OverriddenWarningsDao(database, databaseNotifyManager); + } + + private OverriddenWarningsDao(KeychainDatabase db, DatabaseNotifyManager databaseNotifyManager) { + super(db, databaseNotifyManager); + } + + public boolean isWarningOverridden(String identifier) { + SqlDelightQuery query = OverriddenWarning.FACTORY.selectCountByIdentifier(identifier); + Long result = mapSingleRow(query, OverriddenWarning.FACTORY.selectCountByIdentifierMapper()::map); + return result != null && result > 0; + } + + public void putOverride(String identifier) { + InsertIdentifier statement = new InsertIdentifier(getWritableDb()); + statement.bind(identifier); + statement.executeInsert(); + } + + public void deleteOverride(String identifier) { + DeleteByIdentifier statement = new DeleteByIdentifier(getWritableDb()); + statement.bind(identifier); + statement.executeInsert(); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/processing/ImportKeysListLoader.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/processing/ImportKeysListLoader.java index 0e5b50168..a53e82118 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/processing/ImportKeysListLoader.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/processing/ImportKeysListLoader.java @@ -123,11 +123,13 @@ public class ImportKeysListLoader } } catch (IOException e) { Timber.e(e, "IOException on parsing key file! Return NoValidKeysException!"); - OperationResult.OperationLog log = new OperationResult.OperationLog(); - log.add(OperationResult.LogType.MSG_GET_NO_VALID_KEYS, 0); - GetKeyResult getKeyResult = new GetKeyResult(GetKeyResult.RESULT_ERROR_NO_VALID_KEYS, log); - mData.clear(); - mEntryListWrapper = new AsyncTaskResultWrapper<>(mData, getKeyResult); + if (mData.isEmpty()) { + OperationResult.OperationLog log = new OperationResult.OperationLog(); + log.add(OperationResult.LogType.MSG_GET_NO_VALID_KEYS, 0); + GetKeyResult getKeyResult = new GetKeyResult(GetKeyResult.RESULT_ERROR_NO_VALID_KEYS, log); + mData.clear(); + mEntryListWrapper = new AsyncTaskResultWrapper<>(mData, getKeyResult); + } } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keysync/KeyserverSyncManager.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keysync/KeyserverSyncManager.java new file mode 100644 index 000000000..ada2614bf --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keysync/KeyserverSyncManager.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2017 Schürmann & Breitmoser GbR + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.keysync; + + +import java.util.concurrent.TimeUnit; + +import android.content.Context; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; + +import androidx.work.Constraints.Builder; +import androidx.work.NetworkType; +import androidx.work.OneTimeWorkRequest; +import androidx.work.PeriodicWorkRequest; +import androidx.work.WorkManager; +import org.sufficientlysecure.keychain.util.Preferences; +import timber.log.Timber; + + +public class KeyserverSyncManager { + private static final long SYNC_INTERVAL = 3; + private static final TimeUnit SYNC_INTERVAL_UNIT = TimeUnit.DAYS; + + private static final String PERIODIC_WORK_TAG = "keyserverSync"; + + public static void updateKeyserverSyncSchedule(Context context, boolean forceReschedule) { + Preferences prefs = Preferences.getPreferences(context); + if (!forceReschedule && prefs.isKeyserverSyncScheduled() != prefs.isKeyserverSyncEnabled()) { + return; + } + WorkManager workManager = WorkManager.getInstance(); + if (workManager == null) { + Timber.e("WorkManager unavailable!"); + return; + } + workManager.cancelAllWorkByTag(PERIODIC_WORK_TAG); + + if (!prefs.isKeyserverSyncEnabled()) { + return; + } + + /* Periodic syncs can't be unique, so we just use this to launch a uniquely queued worker */ + + Builder constraints = new Builder() + .setRequiredNetworkType(prefs.getWifiOnlySync() ? NetworkType.UNMETERED : NetworkType.CONNECTED) + .setRequiresBatteryNotLow(true); + if (VERSION.SDK_INT >= VERSION_CODES.M) { + constraints.setRequiresDeviceIdle(true); + } + + PeriodicWorkRequest workRequest = + new PeriodicWorkRequest.Builder(KeyserverSyncWorker.class, SYNC_INTERVAL, SYNC_INTERVAL_UNIT) + .setConstraints(constraints.build()) + .addTag(PERIODIC_WORK_TAG) + .build(); + workManager.enqueue(workRequest); + + prefs.setKeyserverSyncScheduled(true); + } + + public static void debugRunSyncNow() { + WorkManager workManager = WorkManager.getInstance(); + OneTimeWorkRequest workRequest = new OneTimeWorkRequest.Builder(KeyserverSyncWorker.class).build(); + workManager.enqueue(workRequest); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keysync/KeyserverSyncWorker.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keysync/KeyserverSyncWorker.java new file mode 100644 index 000000000..cad95eb21 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keysync/KeyserverSyncWorker.java @@ -0,0 +1,145 @@ +package org.sufficientlysecure.keychain.keysync; + + +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.content.Context; +import android.os.Build; +import android.support.annotation.NonNull; +import android.support.v4.app.NotificationCompat; +import android.support.v4.app.NotificationCompat.Builder; +import android.support.v4.os.CancellationSignal; + +import androidx.work.Worker; +import org.sufficientlysecure.keychain.Constants.NotificationChannels; +import org.sufficientlysecure.keychain.Constants.NotificationIds; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.network.orbot.OrbotHelper; +import org.sufficientlysecure.keychain.operations.KeySyncOperation; +import org.sufficientlysecure.keychain.operations.KeySyncParcel; +import org.sufficientlysecure.keychain.operations.results.ImportKeyResult; +import org.sufficientlysecure.keychain.pgp.Progressable; +import org.sufficientlysecure.keychain.daos.KeyWritableRepository; +import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; +import org.sufficientlysecure.keychain.ui.OrbotRequiredDialogActivity; +import org.sufficientlysecure.keychain.util.ResourceUtils; +import timber.log.Timber; + + +public class KeyserverSyncWorker extends Worker { + private CancellationSignal cancellationSignal = new CancellationSignal(); + + @NonNull + @Override + public WorkerResult doWork() { + KeyWritableRepository keyWritableRepository = KeyWritableRepository.create(getApplicationContext()); + + Timber.d("Starting key sync…"); + Progressable notificationProgressable = notificationShowForProgress(); + KeySyncOperation keySync = new KeySyncOperation(getApplicationContext(), keyWritableRepository, notificationProgressable, cancellationSignal); + ImportKeyResult result = keySync.execute(KeySyncParcel.createRefreshOutdated(), CryptoInputParcel.createCryptoInputParcel()); + return handleUpdateResult(result); + } + + /** + * Since we're returning START_REDELIVER_INTENT in onStartCommand, we need to remember to call + * stopSelf(int) to prevent the Intent from being redelivered if our work is already done + * + * @param result + * result of keyserver sync + */ + private WorkerResult handleUpdateResult(ImportKeyResult result) { + if (result.isPending()) { + Timber.d("Orbot required for sync but not running, attempting to start"); + // result is pending due to Orbot not being started + // try to start it silently, if disabled show notifications + new OrbotHelper.SilentStartManager() { + @Override + protected void onOrbotStarted() { + } + + @Override + protected void onSilentStartDisabled() { + OrbotRequiredDialogActivity.showOrbotRequiredNotification(getApplicationContext()); + } + }.startOrbotAndListen(getApplicationContext(), false); + return WorkerResult.RETRY; + } else if (isStopped()) { + Timber.d("Keyserver sync cancelled"); + return WorkerResult.FAILURE; + } else { + Timber.d("Keyserver sync completed: Updated: %d, Failed: %d", result.mUpdatedKeys, result.mBadKeys); + return WorkerResult.SUCCESS; + } + } + + private Progressable notificationShowForProgress() { + final Context context = getApplicationContext(); + NotificationManager notificationManager = + (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + if (notificationManager == null) { + return null; + } + + createNotificationChannelsIfNecessary(context, notificationManager); + + NotificationCompat.Builder builder = new Builder(context, NotificationChannels.KEYSERVER_SYNC) + .setSmallIcon(R.drawable.ic_stat_notify_24dp) + .setLargeIcon(ResourceUtils.getDrawableAsNotificationBitmap(context, R.mipmap.ic_launcher)) + .setContentTitle(context.getString(R.string.notify_title_keysync)) + .setPriority(NotificationCompat.PRIORITY_MIN) + .setTimeoutAfter(5000) + .setVibrate(null) + .setSound(null) + .setProgress(0, 0, true); + + return new Progressable() { + @Override + public void setProgress(String message, int current, int total) { + setProgress(current, total); + } + + @Override + public void setProgress(int resourceId, int current, int total) { + setProgress(current, total); + } + + @Override + public void setProgress(int current, int total) { + if (total == 0) { + notificationManager.cancel(NotificationIds.KEYSERVER_SYNC); + return; + } + + builder.setProgress(total, current, false); + if (current == total) { + builder.setContentTitle(context.getString(R.string.notify_title_keysync_finished, total)); + builder.setContentText(null); + } else { + builder.setContentText(context.getString(R.string.notify_content_keysync, current, total)); + } + notificationManager.notify(NotificationIds.KEYSERVER_SYNC, builder.build()); + } + + @Override + public void setPreventCancel() { + } + }; + } + + private void createNotificationChannelsIfNecessary(Context context, + NotificationManager notificationManager) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + CharSequence name = context.getString(R.string.notify_channel_keysync); + NotificationChannel channel = new NotificationChannel( + NotificationChannels.KEYSERVER_SYNC, name, NotificationManager.IMPORTANCE_MIN); + notificationManager.createNotificationChannel(channel); + } + } + + @Override + public void onStopped() { + super.onStopped(); + cancellationSignal.cancel(); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/livedata/ApiAppsLiveData.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/livedata/ApiAppsLiveData.java new file mode 100644 index 000000000..374b53dd6 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/livedata/ApiAppsLiveData.java @@ -0,0 +1,113 @@ +package org.sufficientlysecure.keychain.livedata; + + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.graphics.drawable.Drawable; + +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.daos.ApiAppDao; +import org.sufficientlysecure.keychain.daos.DatabaseNotifyManager; +import org.sufficientlysecure.keychain.livedata.ApiAppsLiveData.ListedApp; +import org.sufficientlysecure.keychain.model.ApiApp; +import org.sufficientlysecure.keychain.ui.keyview.loader.AsyncTaskLiveData; + + +public class ApiAppsLiveData extends AsyncTaskLiveData> { + private final ApiAppDao apiAppDao; + private final PackageManager packageManager; + + public ApiAppsLiveData(Context context) { + super(context, DatabaseNotifyManager.getNotifyUriAllApps()); + + packageManager = getContext().getPackageManager(); + apiAppDao = ApiAppDao.getInstance(context); + } + + @Override + protected List asyncLoadData() { + ArrayList result = new ArrayList<>(); + + loadRegisteredApps(result); + addPlaceholderApps(result); + + Collections.sort(result, (o1, o2) -> o1.readableName.compareTo(o2.readableName)); + return result; + } + + private void loadRegisteredApps(ArrayList result) { + List registeredApiApps = apiAppDao.getAllApiApps(); + + for (ApiApp apiApp : registeredApiApps) { + ListedApp listedApp; + try { + ApplicationInfo ai = packageManager.getApplicationInfo(apiApp.package_name(), 0); + CharSequence applicationLabel = packageManager.getApplicationLabel(ai); + Drawable applicationIcon = packageManager.getApplicationIcon(ai); + + listedApp = new ListedApp(apiApp.package_name(), true, true, applicationLabel, applicationIcon, null); + } catch (PackageManager.NameNotFoundException e) { + listedApp = new ListedApp(apiApp.package_name(), false, true, apiApp.package_name(), null, null); + } + result.add(listedApp); + } + } + + private void addPlaceholderApps(ArrayList result) { + for (ListedApp placeholderApp : PLACERHOLDER_APPS) { + if (!containsByPackageName(result, placeholderApp.packageName)) { + try { + packageManager.getApplicationInfo(placeholderApp.packageName, 0); + result.add(placeholderApp.withIsInstalled()); + } catch (PackageManager.NameNotFoundException e) { + result.add(placeholderApp); + } + } + } + } + + private boolean containsByPackageName(ArrayList result, String packageName) { + for (ListedApp app : result) { + if (packageName.equals(app.packageName)) { + return true; + } + } + return false; + } + + + public static class ListedApp { + public final String packageName; + public final boolean isInstalled; + public final boolean isRegistered; + public final String readableName; + public final Drawable applicationIcon; + public final Integer applicationIconRes; + + ListedApp(String packageName, boolean isInstalled, boolean isRegistered, CharSequence readableName, + Drawable applicationIcon, Integer applicationIconRes) { + this.packageName = packageName; + this.isInstalled = isInstalled; + this.isRegistered = isRegistered; + this.readableName = readableName.toString(); + this.applicationIcon = applicationIcon; + this.applicationIconRes = applicationIconRes; + } + + public ListedApp withIsInstalled() { + return new ListedApp(packageName, true, isRegistered, readableName, applicationIcon, applicationIconRes); + } + } + + private static final ListedApp[] PLACERHOLDER_APPS = { + new ListedApp("com.fsck.k9", false, false, "K-9 Mail", null, R.drawable.apps_k9), + new ListedApp("com.zeapo.pwdstore", false, false, "Password Store", null, R.drawable.apps_password_store), + new ListedApp("eu.siacs.conversations", false, false, "Conversations (Instant Messaging)", null, + R.drawable.apps_conversations) + }; +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/livedata/GenericLiveData.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/livedata/GenericLiveData.java new file mode 100644 index 000000000..bd9d70ff2 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/livedata/GenericLiveData.java @@ -0,0 +1,37 @@ +package org.sufficientlysecure.keychain.livedata; + + +import android.content.Context; +import android.net.Uri; + +import org.sufficientlysecure.keychain.daos.DatabaseNotifyManager; +import org.sufficientlysecure.keychain.ui.keyview.loader.AsyncTaskLiveData; + + +public class GenericLiveData extends AsyncTaskLiveData { + private GenericDataLoader genericDataLoader; + + public GenericLiveData(Context context, GenericDataLoader genericDataLoader) { + super(context, null); + this.genericDataLoader = genericDataLoader; + } + + public GenericLiveData(Context context, Uri notifyUri, GenericDataLoader genericDataLoader) { + super(context, notifyUri); + this.genericDataLoader = genericDataLoader; + } + + public GenericLiveData(Context context, long notifyMasterKeyId, GenericDataLoader genericDataLoader) { + super(context, DatabaseNotifyManager.getNotifyUriMasterKeyId(notifyMasterKeyId)); + this.genericDataLoader = genericDataLoader; + } + + @Override + protected T asyncLoadData() { + return genericDataLoader.loadData(); + } + + public interface GenericDataLoader { + T loadData(); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/livedata/KeyInfoInteractor.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/livedata/KeyInfoInteractor.java deleted file mode 100644 index 173e58ac5..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/livedata/KeyInfoInteractor.java +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Copyright (C) 2017 Schürmann & Breitmoser GbR - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sufficientlysecure.keychain.livedata; - - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import android.content.ContentResolver; -import android.database.Cursor; -import android.net.Uri; -import android.support.annotation.Nullable; - -import com.google.auto.value.AutoValue; -import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; -import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables; -import org.sufficientlysecure.keychain.util.DatabaseUtil; - - -public class KeyInfoInteractor { - // These are the rows that we will retrieve. - private String[] QUERY_PROJECTION = new String[]{ - KeyRings._ID, - KeyRings.MASTER_KEY_ID, - KeyRings.CREATION, - KeyRings.HAS_ENCRYPT, - KeyRings.HAS_AUTHENTICATE_SECRET, - KeyRings.HAS_ANY_SECRET, - KeyRings.VERIFIED, - KeyRings.NAME, - KeyRings.EMAIL, - KeyRings.COMMENT, - KeyRings.IS_EXPIRED, - KeyRings.IS_REVOKED, - }; - private static final int INDEX_MASTER_KEY_ID = 1; - private static final int INDEX_CREATION = 2; - private static final int INDEX_HAS_ENCRYPT = 3; - private static final int INDEX_HAS_AUTHENTICATE = 4; - private static final int INDEX_HAS_ANY_SECRET = 5; - private static final int INDEX_VERIFIED = 6; - private static final int INDEX_NAME = 7; - private static final int INDEX_EMAIL = 8; - private static final int INDEX_COMMENT = 9; - - private static final String QUERY_WHERE_ALL = Tables.KEYS + "." + KeyRings.IS_REVOKED + - " = 0 AND " + KeyRings.IS_EXPIRED + " = 0"; - private static final String QUERY_WHERE_SECRET = Tables.KEYS + "." + KeyRings.IS_REVOKED + - " = 0 AND " + KeyRings.IS_EXPIRED + " = 0" + " AND " + KeyRings.HAS_ANY_SECRET + " != 0"; - private static final String QUERY_ORDER = Tables.KEYS + "." + KeyRings.CREATION + " DESC"; - - private final ContentResolver contentResolver; - - - public KeyInfoInteractor(ContentResolver contentResolver) { - this.contentResolver = contentResolver; - } - - public List loadKeyInfo(KeySelector keySelector) { - ArrayList keyInfos = new ArrayList<>(); - Cursor cursor; - - String selection = keySelector.isOnlySecret() ? QUERY_WHERE_SECRET : QUERY_WHERE_ALL; - String additionalSelection = keySelector.getSelection(); - - selection = DatabaseUtil.concatenateWhere(selection, additionalSelection); - cursor = contentResolver.query(keySelector.getKeyRingUri(), QUERY_PROJECTION, selection, null, QUERY_ORDER); - - if (cursor == null) { - return null; - } - - while (cursor.moveToNext()) { - KeyInfo keyInfo = KeyInfo.fromCursor(cursor); - keyInfos.add(keyInfo); - } - - return Collections.unmodifiableList(keyInfos); - } - - @AutoValue - public abstract static class KeyInfo { - public abstract long getMasterKeyId(); - public abstract long getCreationDate(); - public abstract boolean getHasEncrypt(); - public abstract boolean getHasAuthenticate(); - public abstract boolean getHasAnySecret(); - public abstract boolean getIsVerified(); - - @Nullable - public abstract String getName(); - @Nullable - public abstract String getEmail(); - @Nullable - public abstract String getComment(); - - static KeyInfo fromCursor(Cursor cursor) { - long masterKeyId = cursor.getLong(INDEX_MASTER_KEY_ID); - long creationDate = cursor.getLong(INDEX_CREATION) * 1000L; - boolean hasEncrypt = cursor.getInt(INDEX_HAS_ENCRYPT) != 0; - boolean hasAuthenticate = cursor.getInt(INDEX_HAS_AUTHENTICATE) != 0; - boolean hasAnySecret = cursor.getInt(INDEX_HAS_ANY_SECRET) != 0; - boolean isVerified = cursor.getInt(INDEX_VERIFIED) == 2; - - String name = cursor.getString(INDEX_NAME); - String email = cursor.getString(INDEX_EMAIL); - String comment = cursor.getString(INDEX_COMMENT); - - return new AutoValue_KeyInfoInteractor_KeyInfo( - masterKeyId, creationDate, hasEncrypt, hasAuthenticate, hasAnySecret, isVerified, name, email, comment); - } - } - - @AutoValue - public abstract static class KeySelector { - public abstract Uri getKeyRingUri(); - @Nullable - public abstract String getSelection(); - public abstract boolean isOnlySecret(); - - public static KeySelector create(Uri keyRingUri, String selection) { - return new AutoValue_KeyInfoInteractor_KeySelector(keyRingUri, selection, false); - } - - public static KeySelector createOnlySecret(Uri keyRingUri, String selection) { - return new AutoValue_KeyInfoInteractor_KeySelector(keyRingUri, selection, true); - } - } -} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/livedata/KeyInfoLiveData.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/livedata/KeyInfoLiveData.java deleted file mode 100644 index 502ab2f20..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/livedata/KeyInfoLiveData.java +++ /dev/null @@ -1,38 +0,0 @@ -package org.sufficientlysecure.keychain.livedata; - - -import java.util.List; - -import android.content.ContentResolver; -import android.content.Context; - -import org.sufficientlysecure.keychain.livedata.KeyInfoInteractor.KeyInfo; -import org.sufficientlysecure.keychain.livedata.KeyInfoInteractor.KeySelector; -import org.sufficientlysecure.keychain.ui.keyview.loader.AsyncTaskLiveData; - - -public class KeyInfoLiveData extends AsyncTaskLiveData> { - private final KeyInfoInteractor keyInfoInteractor; - - private KeySelector keySelector; - - public KeyInfoLiveData(Context context, ContentResolver contentResolver) { - super(context, null); - - this.keyInfoInteractor = new KeyInfoInteractor(contentResolver); - } - - public void setKeySelector(KeySelector keySelector) { - this.keySelector = keySelector; - - updateDataInBackground(); - } - - @Override - protected List asyncLoadData() { - if (keySelector == null) { - return null; - } - return keyInfoInteractor.loadKeyInfo(keySelector); - } -} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/model/ApiAllowedKey.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/model/ApiAllowedKey.java new file mode 100644 index 000000000..d635d2931 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/model/ApiAllowedKey.java @@ -0,0 +1,11 @@ +package org.sufficientlysecure.keychain.model; + + +import com.google.auto.value.AutoValue; +import org.sufficientlysecure.keychain.ApiAllowedKeysModel; + + +@AutoValue +public abstract class ApiAllowedKey implements ApiAllowedKeysModel { + public static final Factory FACTORY = new Factory(AutoValue_ApiAllowedKey::new); +} \ No newline at end of file diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/compatibility/ListFragmentWorkaround.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/model/ApiApp.java similarity index 58% rename from OpenKeychain/src/main/java/org/sufficientlysecure/keychain/compatibility/ListFragmentWorkaround.java rename to OpenKeychain/src/main/java/org/sufficientlysecure/keychain/model/ApiApp.java index 0fa97c8f8..2e84094cb 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/compatibility/ListFragmentWorkaround.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/model/ApiApp.java @@ -15,23 +15,19 @@ * along with this program. If not, see . */ -package org.sufficientlysecure.keychain.compatibility; +package org.sufficientlysecure.keychain.model; -import android.support.v4.app.ListFragment; -import android.view.View; -import android.widget.ListView; -/** - * Bug on Android >= 4.1 - *

- * http://code.google.com/p/android/issues/detail?id=35885 - *

- * Items are not checked in layout - */ -public class ListFragmentWorkaround extends ListFragment { +import com.google.auto.value.AutoValue; +import org.sufficientlysecure.keychain.ApiAppsModel; - @Override - public void onListItemClick(ListView l, View v, int position, long id) { - l.setItemChecked(position, l.isItemChecked(position)); + +@AutoValue +public abstract class ApiApp implements ApiAppsModel { + public static final ApiAppsModel.Factory FACTORY = + new ApiAppsModel.Factory(AutoValue_ApiApp::new); + + public static ApiApp create(String packageName, byte[] packageSignature) { + return new AutoValue_ApiApp(null, packageName, packageSignature); } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/model/AutocryptPeer.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/model/AutocryptPeer.java new file mode 100644 index 000000000..f9c5e1783 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/model/AutocryptPeer.java @@ -0,0 +1,57 @@ +package org.sufficientlysecure.keychain.model; + + +import com.google.auto.value.AutoValue; +import com.squareup.sqldelight.RowMapper; +import org.sufficientlysecure.keychain.AutocryptPeersModel; + + +@AutoValue +public abstract class AutocryptPeer implements AutocryptPeersModel { + + public enum GossipOrigin { + GOSSIP_HEADER, SIGNATURE, DEDUP + } + + public static final Factory FACTORY = new Factory(AutoValue_AutocryptPeer::new, + CustomColumnAdapters.DATE_ADAPTER, CustomColumnAdapters.DATE_ADAPTER, CustomColumnAdapters.DATE_ADAPTER, + CustomColumnAdapters.GOSSIP_ORIGIN_ADAPTER); + + public static final RowMapper PEER_MAPPER = FACTORY.selectByIdentifiersMapper(); + public static final RowMapper KEY_STATUS_MAPPER = + FACTORY.selectAutocryptKeyStatusMapper(AutoValue_AutocryptPeer_AutocryptKeyStatus::new); + + @AutoValue + public static abstract class AutocryptKeyStatus implements SelectAutocryptKeyStatusModel { + public boolean hasGossipKey() { + return autocryptPeer().gossip_master_key_id() != null; + } + + public boolean isGossipKeyRevoked() { + Boolean gossip_key_is_revoked = gossip_key_is_revoked_int(); + return gossip_key_is_revoked != null && gossip_key_is_revoked; + } + + public boolean isGossipKeyExpired() { + return gossip_key_is_expired_int() != 0; + } + + public boolean isGossipKeyVerified() { + return gossip_key_is_verified_int() != 0; + } + + public boolean isKeyRevoked() { + Boolean revoked = key_is_revoked_int(); + return revoked != null && revoked; + } + + public boolean isKeyExpired() { + return key_is_expired_int() != 0; + } + + public boolean isKeyVerified() { + return key_is_verified_int() != 0; + } + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/model/Certification.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/model/Certification.java new file mode 100644 index 000000000..ee42c460e --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/model/Certification.java @@ -0,0 +1,20 @@ +package org.sufficientlysecure.keychain.model; + + +import com.google.auto.value.AutoValue; +import org.sufficientlysecure.keychain.CertsModel; + + +@AutoValue +public abstract class Certification implements CertsModel { + public static final CertsModel.Factory FACTORY = + new CertsModel.Factory<>(AutoValue_Certification::new, CustomColumnAdapters.VERIFICATON_STATUS_ADAPTER); + + public static final SelectVerifyingCertDetailsMapper CERT_DETAILS_MAPPER = + new SelectVerifyingCertDetailsMapper<>(AutoValue_Certification_CertDetails::new); + + @AutoValue + public static abstract class CertDetails implements CertsModel.SelectVerifyingCertDetailsModel { + + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/model/CustomColumnAdapters.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/model/CustomColumnAdapters.java new file mode 100644 index 000000000..bfaa83caa --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/model/CustomColumnAdapters.java @@ -0,0 +1,93 @@ +package org.sufficientlysecure.keychain.model; + + +import java.util.Date; + +import android.support.annotation.NonNull; + +import com.squareup.sqldelight.ColumnAdapter; +import org.sufficientlysecure.keychain.model.AutocryptPeer.GossipOrigin; +import org.sufficientlysecure.keychain.pgp.CanonicalizedKeyRing.VerificationStatus; +import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey.SecretKeyType; + + +public final class CustomColumnAdapters { + + private CustomColumnAdapters() { } + + static final ColumnAdapter DATE_ADAPTER = new ColumnAdapter() { + @NonNull + @Override + public Date decode(Long databaseValue) { + // Both SQLite and OpenPGP prefer a second granularity for timestamps - so we'll translate here + return new Date(databaseValue * 1000); + } + + @Override + public Long encode(@NonNull Date value) { + return value.getTime() / 1000; + } + }; + + static final ColumnAdapter GOSSIP_ORIGIN_ADAPTER = new ColumnAdapter() { + @NonNull + @Override + public GossipOrigin decode(Long databaseValue) { + switch (databaseValue.intValue()) { + case 0: return GossipOrigin.GOSSIP_HEADER; + case 10: return GossipOrigin.SIGNATURE; + case 20: return GossipOrigin.DEDUP; + default: throw new IllegalArgumentException("Unhandled database value!"); + } + } + + @Override + public Long encode(@NonNull GossipOrigin value) { + switch (value) { + case GOSSIP_HEADER: return 0L; + case SIGNATURE: return 10L; + case DEDUP: return 20L; + default: throw new IllegalArgumentException("Unhandled database value!"); + } + } + }; + + public static final ColumnAdapter SECRET_KEY_TYPE_ADAPTER = new ColumnAdapter() { + @NonNull + @Override + public SecretKeyType decode(Long databaseValue) { + return databaseValue == null ? SecretKeyType.UNAVAILABLE : SecretKeyType.fromNum(databaseValue.intValue()); + } + + @Override + public Long encode(@NonNull SecretKeyType value) { + return (long) value.getNum(); + } + }; + + public static final ColumnAdapter VERIFICATON_STATUS_ADAPTER = new ColumnAdapter() { + @NonNull + @Override + public VerificationStatus decode(Long databaseValue) { + if (databaseValue == null) { + return VerificationStatus.UNVERIFIED; + } + switch (databaseValue.intValue()) { + case 0: return VerificationStatus.UNVERIFIED; + case 1: return VerificationStatus.VERIFIED_SECRET; + case 2: return VerificationStatus.VERIFIED_SELF; + default: throw new IllegalArgumentException(); + } + } + + @Override + public Long encode(@NonNull VerificationStatus value) { + switch (value) { + case UNVERIFIED: return 0L; + case VERIFIED_SECRET: return 1L; + case VERIFIED_SELF: return 2L; + default: throw new IllegalArgumentException(); + } + } + }; +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/model/KeyMetadata.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/model/KeyMetadata.java new file mode 100644 index 000000000..c1df957b8 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/model/KeyMetadata.java @@ -0,0 +1,24 @@ +package org.sufficientlysecure.keychain.model; + + +import com.google.auto.value.AutoValue; +import org.sufficientlysecure.keychain.KeyMetadataModel; + + +@AutoValue +public abstract class KeyMetadata implements KeyMetadataModel { + public static final Factory FACTORY = new Factory<>( + AutoValue_KeyMetadata::new, CustomColumnAdapters.DATE_ADAPTER); + + public boolean hasBeenUpdated() { + return last_updated() != null; + } + + public boolean isPublished() { + if (last_updated() == null) { + throw new IllegalStateException("Cannot get publication state if key has never been updated!"); + } + Boolean seenOnKeyservers = seen_on_keyservers(); + return seenOnKeyservers != null && seenOnKeyservers; + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/model/KeyRingPublic.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/model/KeyRingPublic.java new file mode 100644 index 000000000..65a7cf6dd --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/model/KeyRingPublic.java @@ -0,0 +1,12 @@ +package org.sufficientlysecure.keychain.model; + + +import com.google.auto.value.AutoValue; +import org.sufficientlysecure.keychain.KeyRingsPublicModel; + +@AutoValue +public abstract class KeyRingPublic implements KeyRingsPublicModel { + public static final Factory FACTORY = new Factory<>(AutoValue_KeyRingPublic::new); + + public static final Mapper MAPPER = new Mapper<>(FACTORY); +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/model/KeySignature.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/model/KeySignature.java new file mode 100644 index 000000000..ed3caa1bc --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/model/KeySignature.java @@ -0,0 +1,13 @@ +package org.sufficientlysecure.keychain.model; + + +import com.google.auto.value.AutoValue; +import org.sufficientlysecure.keychain.KeySignaturesModel; + + +@AutoValue +public abstract class KeySignature implements KeySignaturesModel { + public static final Factory FACTORY = new Factory<>(AutoValue_KeySignature::new); + + public static final Mapper MAPPER = new Mapper<>(FACTORY); +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/model/OverriddenWarning.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/model/OverriddenWarning.java new file mode 100644 index 000000000..5de6b385b --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/model/OverriddenWarning.java @@ -0,0 +1,14 @@ +package org.sufficientlysecure.keychain.model; + + +import com.google.auto.value.AutoValue; +import org.sufficientlysecure.keychain.KeySignaturesModel; +import org.sufficientlysecure.keychain.OverriddenWarningsModel; + + +@AutoValue +public abstract class OverriddenWarning implements OverriddenWarningsModel { + public static final Factory FACTORY = new Factory<>(AutoValue_OverriddenWarning::new); + + public static final Mapper MAPPER = new Mapper<>(FACTORY); +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/model/SubKey.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/model/SubKey.java new file mode 100644 index 000000000..5937ab318 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/model/SubKey.java @@ -0,0 +1,84 @@ +package org.sufficientlysecure.keychain.model; + + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import com.google.auto.value.AutoValue; +import com.squareup.sqldelight.RowMapper; +import org.sufficientlysecure.keychain.KeysModel; +import org.sufficientlysecure.keychain.pgp.CanonicalizedKeyRing.VerificationStatus; +import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey.SecretKeyType; + + +@AutoValue +public abstract class SubKey implements KeysModel { + public static final Factory FACTORY = + new Factory<>(AutoValue_SubKey::new, CustomColumnAdapters.SECRET_KEY_TYPE_ADAPTER); + public static final UnifiedKeyViewMapper UNIFIED_KEY_INFO_MAPPER = + FACTORY.selectAllUnifiedKeyInfoMapper( + AutoValue_SubKey_UnifiedKeyInfo::new, Certification.FACTORY); + public static Mapper SUBKEY_MAPPER = new Mapper<>(FACTORY); + public static RowMapper SKT_MAPPER = FACTORY.selectSecretKeyTypeMapper(); + + public boolean expires() { + return expiry() != null; + } + + @AutoValue + public static abstract class UnifiedKeyInfo implements KeysModel.UnifiedKeyViewModel { + private List autocryptPackageNames; + private String cachedUidSearchString; + + public boolean is_expired() { + Long expiry = expiry(); + return expiry != null && expiry * 1000 < System.currentTimeMillis(); + } + + public boolean has_any_secret() { + return has_any_secret_int() != 0; + } + + public boolean is_verified() { + VerificationStatus verified = verified(); + return verified != null && verified == VerificationStatus.VERIFIED_SECRET; + } + + public boolean has_duplicate() { + return has_duplicate_int() != 0; + } + + public List autocrypt_package_names() { + if (autocryptPackageNames == null) { + String csv = autocrypt_package_names_csv(); + autocryptPackageNames = csv == null ? Collections.emptyList() : + Arrays.asList(csv.split(",")); + } + return autocryptPackageNames; + } + + public boolean has_auth_key() { + return has_auth_key_int() != 0; + } + + public boolean has_encrypt_key() { + return has_encrypt_key_int() != 0; + } + + public boolean has_sign_key() { + return has_sign_key_int() != 0; + } + + public String uidSearchString() { + if (cachedUidSearchString == null) { + cachedUidSearchString = user_id_list(); + if (cachedUidSearchString == null) { + cachedUidSearchString = ""; + } + cachedUidSearchString = cachedUidSearchString.toLowerCase(); + } + return cachedUidSearchString; + } + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/model/UserPacket.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/model/UserPacket.java new file mode 100644 index 000000000..cf33fc66d --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/model/UserPacket.java @@ -0,0 +1,31 @@ +package org.sufficientlysecure.keychain.model; + + +import com.google.auto.value.AutoValue; +import org.sufficientlysecure.keychain.UserPacketsModel; +import org.sufficientlysecure.keychain.pgp.CanonicalizedKeyRing.VerificationStatus; +import org.sufficientlysecure.keychain.provider.KeychainContract.Certs; + + +@AutoValue +public abstract class UserPacket implements UserPacketsModel { + public static final Factory FACTORY = new Factory<>(AutoValue_UserPacket::new); + public static final SelectUserIdsByMasterKeyIdMapper USER_ID_MAPPER = + FACTORY.selectUserIdsByMasterKeyIdMapper(AutoValue_UserPacket_UserId::new, Certification.FACTORY); + public static final SelectUserAttributesByTypeAndMasterKeyIdMapper USER_ATTRIBUTE_MAPPER = + FACTORY.selectUserAttributesByTypeAndMasterKeyIdMapper(AutoValue_UserPacket_UserAttribute::new, Certification.FACTORY); + + @AutoValue + public static abstract class UserId implements SelectUserIdsByMasterKeyIdModel { + public boolean isVerified() { + return verified() == VerificationStatus.VERIFIED_SECRET; + } + } + + @AutoValue + public static abstract class UserAttribute implements SelectUserAttributesByTypeAndMasterKeyIdModel { + public boolean isVerified() { + return verified() == VerificationStatus.VERIFIED_SECRET; + } + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/network/NetworkReceiver.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/network/NetworkReceiver.java deleted file mode 100644 index ec3cebf0f..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/network/NetworkReceiver.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright (C) 2017 Schürmann & Breitmoser GbR - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sufficientlysecure.keychain.network; - -import android.content.BroadcastReceiver; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; - -import org.sufficientlysecure.keychain.service.KeyserverSyncAdapterService; -import timber.log.Timber; - - -public class NetworkReceiver extends BroadcastReceiver { - - @Override - public void onReceive(Context context, Intent intent) { - - ConnectivityManager conn = (ConnectivityManager) - context.getSystemService(Context.CONNECTIVITY_SERVICE); - NetworkInfo networkInfo = conn.getActiveNetworkInfo(); - boolean isTypeWifi = (networkInfo != null) && - (networkInfo.getType() == ConnectivityManager.TYPE_WIFI); - boolean isConnected = (networkInfo != null) && networkInfo.isConnected(); - - if (isTypeWifi && isConnected) { - - // broadcaster receiver disabled - setWifiReceiverComponent(false, context); - Intent serviceIntent = new Intent(context, KeyserverSyncAdapterService.class); - serviceIntent.setAction(KeyserverSyncAdapterService.ACTION_SYNC_NOW); - context.startService(serviceIntent); - } - } - - public void setWifiReceiverComponent(Boolean isEnabled, Context context) { - - PackageManager pm = context.getPackageManager(); - ComponentName compName = new ComponentName(context, - NetworkReceiver.class); - - if (isEnabled) { - pm.setComponentEnabledSetting(compName, - PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP); - Timber.d("Wifi Receiver is enabled!"); - } else { - pm.setComponentEnabledSetting(compName, - PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP); - Timber.d("Wifi Receiver is disabled!"); - } - } -} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/BackupOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/BackupOperation.java index f88731800..eb613c27c 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/BackupOperation.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/BackupOperation.java @@ -24,23 +24,21 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.text.SimpleDateFormat; -import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Locale; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.regex.Pattern; import android.content.Context; -import android.database.Cursor; import android.net.Uri; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import android.text.TextUtils; +import android.support.v4.os.CancellationSignal; import org.bouncycastle.bcpg.ArmoredOutputStream; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.model.SubKey.UnifiedKeyInfo; import org.sufficientlysecure.keychain.operations.results.ExportResult; import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType; import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; @@ -52,17 +50,15 @@ import org.sufficientlysecure.keychain.pgp.PgpSignEncryptOperation; import org.sufficientlysecure.keychain.pgp.Progressable; import org.sufficientlysecure.keychain.pgp.UncachedKeyRing; import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; -import org.sufficientlysecure.keychain.provider.KeyRepository; -import org.sufficientlysecure.keychain.provider.KeyRepository.NotFoundException; -import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; -import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables; +import org.sufficientlysecure.keychain.daos.KeyRepository; +import org.sufficientlysecure.keychain.daos.KeyRepository.NotFoundException; import org.sufficientlysecure.keychain.provider.TemporaryFileProvider; import org.sufficientlysecure.keychain.service.BackupKeyringParcel; import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; -import org.sufficientlysecure.keychain.util.Numeric9x4PassphraseUtil; import org.sufficientlysecure.keychain.util.CountingOutputStream; import org.sufficientlysecure.keychain.util.InputData; +import org.sufficientlysecure.keychain.util.Numeric9x4PassphraseUtil; import org.sufficientlysecure.keychain.util.Passphrase; import timber.log.Timber; @@ -78,14 +74,6 @@ import timber.log.Timber; * either the name of a file or an output uri to write to. */ public class BackupOperation extends BaseOperation { - - private static final String[] PROJECTION = new String[] { - KeyRings.MASTER_KEY_ID, - KeyRings.HAS_ANY_SECRET - }; - private static final int INDEX_MASTER_KEY_ID = 0; - private static final int INDEX_HAS_ANY_SECRET = 1; - // this is a very simple matcher, we only need basic sanitization private static final Pattern HEADER_PATTERN = Pattern.compile("[a-zA-Z0-9_-]+: [^\\n]+"); @@ -95,7 +83,7 @@ public class BackupOperation extends BaseOperation { } public BackupOperation(Context context, KeyRepository keyRepository, - Progressable progressable, AtomicBoolean cancelled) { + Progressable progressable, CancellationSignal cancelled) { super(context, keyRepository, progressable, cancelled); } @@ -223,42 +211,36 @@ public class BackupOperation extends BaseOperation { OutputStream outStream, List extraSecretKeyHeaders) { // noinspection unused TODO use these in a log entry int okSecret = 0, okPublic = 0; - int progress = 0; - Cursor cursor = queryForKeys(masterKeyIds); - - if (cursor == null || !cursor.moveToFirst()) { - log.add(LogType.MSG_BACKUP_ERROR_DB, 1); - return false; // new ExportResult(ExportResult.RESULT_ERROR, log); - } - try { - - int numKeys = cursor.getCount(); + List unifiedKeyInfos; + if (masterKeyIds == null) { + unifiedKeyInfos = mKeyRepository.getAllUnifiedKeyInfo(); + } else { + unifiedKeyInfos = mKeyRepository.getUnifiedKeyInfo(masterKeyIds); + } + int numKeys = unifiedKeyInfos.size(); updateProgress(mContext.getResources().getQuantityString(R.plurals.progress_exporting_key, numKeys), 0, numKeys); // For each public masterKey id - while (!cursor.isAfterLast()) { - - long masterKeyId = cursor.getLong(INDEX_MASTER_KEY_ID); - log.add(LogType.MSG_BACKUP_PUBLIC, 1, KeyFormattingUtils.beautifyKeyId(masterKeyId)); + for (UnifiedKeyInfo keyInfo : unifiedKeyInfos) { + log.add(LogType.MSG_BACKUP_PUBLIC, 1, KeyFormattingUtils.beautifyKeyId(keyInfo.master_key_id())); boolean publicKeyWriteOk = false; if (exportPublic) { - publicKeyWriteOk = writePublicKeyToStream(masterKeyId, log, outStream); + publicKeyWriteOk = writePublicKeyToStream(keyInfo.master_key_id(), log, outStream); if (publicKeyWriteOk) { okPublic += 1; } } if (publicKeyWriteOk || !exportPublic) { - boolean hasSecret = cursor.getInt(INDEX_HAS_ANY_SECRET) > 0; - if (exportSecret && hasSecret) { - log.add(LogType.MSG_BACKUP_SECRET, 2, KeyFormattingUtils.beautifyKeyId(masterKeyId)); - if (writeSecretKeyToStream(masterKeyId, log, outStream, extraSecretKeyHeaders)) { + if (exportSecret && keyInfo.has_any_secret()) { + log.add(LogType.MSG_BACKUP_SECRET, 2, KeyFormattingUtils.beautifyKeyId(keyInfo.master_key_id())); + if (writeSecretKeyToStream(keyInfo.master_key_id(), log, outStream, extraSecretKeyHeaders)) { okSecret += 1; } extraSecretKeyHeaders = null; @@ -266,7 +248,6 @@ public class BackupOperation extends BaseOperation { } updateProgress(progress++, numKeys); - cursor.moveToNext(); } updateProgress(R.string.progress_done, numKeys, numKeys); @@ -281,7 +262,6 @@ public class BackupOperation extends BaseOperation { } catch (Exception e) { Timber.e(e, "error closing stream"); } - cursor.close(); } return true; @@ -341,29 +321,4 @@ public class BackupOperation extends BaseOperation { } } - private Cursor queryForKeys(long[] masterKeyIds) { - String selection = null, selectionArgs[] = null; - - if (masterKeyIds != null) { - // convert long[] to String[] - selectionArgs = new String[masterKeyIds.length]; - for (int i = 0; i < masterKeyIds.length; i++) { - selectionArgs[i] = Long.toString(masterKeyIds[i]); - } - - // generates ?,?,? as placeholders for selectionArgs - String placeholders = TextUtils.join(",", - Collections.nCopies(masterKeyIds.length, "?")); - - // put together selection string - selection = Tables.KEYS + "." + KeyRings.MASTER_KEY_ID - + " IN (" + placeholders + ")"; - } - - return mKeyRepository.getContentResolver().query( - KeyRings.buildUnifiedKeyRingsUri(), PROJECTION, selection, selectionArgs, - Tables.KEYS + "." + KeyRings.MASTER_KEY_ID - ); - } - } \ No newline at end of file diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/BaseOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/BaseOperation.java index 3559ea2a4..f757eb6dd 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/BaseOperation.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/BaseOperation.java @@ -18,19 +18,17 @@ package org.sufficientlysecure.keychain.operations; -import java.util.concurrent.atomic.AtomicBoolean; - import android.content.Context; import android.os.Parcelable; import android.support.annotation.NonNull; import android.support.annotation.StringRes; +import android.support.v4.os.CancellationSignal; import org.sufficientlysecure.keychain.Constants.key; import org.sufficientlysecure.keychain.operations.results.OperationResult; import org.sufficientlysecure.keychain.pgp.PassphraseCacheInterface; import org.sufficientlysecure.keychain.pgp.Progressable; -import org.sufficientlysecure.keychain.provider.KeyRepository; -import org.sufficientlysecure.keychain.provider.KeyRepository.NotFoundException; +import org.sufficientlysecure.keychain.daos.KeyRepository; import org.sufficientlysecure.keychain.service.PassphraseCacheService; import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import org.sufficientlysecure.keychain.util.Passphrase; @@ -39,7 +37,7 @@ public abstract class BaseOperation implements PassphraseC final public Context mContext; final public Progressable mProgressable; - final public AtomicBoolean mCancelled; + final public CancellationSignal mCancelled; final public KeyRepository mKeyRepository; @@ -73,7 +71,7 @@ public abstract class BaseOperation implements PassphraseC } public BaseOperation(Context context, KeyRepository keyRepository, - Progressable progressable, AtomicBoolean cancelled) { + Progressable progressable, CancellationSignal cancelled) { mContext = context; mProgressable = progressable; mKeyRepository = keyRepository; @@ -102,7 +100,7 @@ public abstract class BaseOperation implements PassphraseC } protected boolean checkCancelled() { - return mCancelled != null && mCancelled.get(); + return mCancelled != null && mCancelled.isCanceled(); } protected void setPreventCancel () { @@ -113,15 +111,14 @@ public abstract class BaseOperation implements PassphraseC @Override public Passphrase getCachedPassphrase(long subKeyId) throws NoSecretKeyException { - try { - if (subKeyId != key.symmetric) { - long masterKeyId = mKeyRepository.getMasterKeyId(subKeyId); - return getCachedPassphrase(masterKeyId, subKeyId); + if (subKeyId != key.symmetric) { + Long masterKeyId = mKeyRepository.getMasterKeyIdBySubkeyId(subKeyId); + if (masterKeyId == null) { + throw new PassphraseCacheInterface.NoSecretKeyException(); } - return getCachedPassphrase(key.symmetric, key.symmetric); - } catch (NotFoundException e) { - throw new PassphraseCacheInterface.NoSecretKeyException(); + return getCachedPassphrase(masterKeyId, subKeyId); } + return getCachedPassphrase(key.symmetric, key.symmetric); } @Override diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/BaseReadWriteOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/BaseReadWriteOperation.java index 155441cdc..43df20fb1 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/BaseReadWriteOperation.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/BaseReadWriteOperation.java @@ -17,16 +17,16 @@ package org.sufficientlysecure.keychain.operations; -import java.util.concurrent.atomic.AtomicBoolean; import android.content.Context; import android.os.Parcelable; +import android.support.v4.os.CancellationSignal; import org.sufficientlysecure.keychain.pgp.Progressable; -import org.sufficientlysecure.keychain.provider.KeyWritableRepository; +import org.sufficientlysecure.keychain.daos.KeyWritableRepository; -abstract class BaseReadWriteOperation extends BaseOperation { - final KeyWritableRepository mKeyWritableRepository; +public abstract class BaseReadWriteOperation extends BaseOperation { + protected final KeyWritableRepository mKeyWritableRepository; BaseReadWriteOperation(Context context, KeyWritableRepository databaseInteractor, @@ -36,8 +36,8 @@ abstract class BaseReadWriteOperation extends BaseOperatio mKeyWritableRepository = databaseInteractor; } - BaseReadWriteOperation(Context context, KeyWritableRepository databaseInteractor, - Progressable progressable, AtomicBoolean cancelled) { + protected BaseReadWriteOperation(Context context, KeyWritableRepository databaseInteractor, + Progressable progressable, CancellationSignal cancelled) { super(context, databaseInteractor, progressable, cancelled); mKeyWritableRepository = databaseInteractor; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/BenchmarkOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/BenchmarkOperation.java index 65edf45cd..353ecc5d1 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/BenchmarkOperation.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/BenchmarkOperation.java @@ -42,7 +42,7 @@ import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyOperation; import org.sufficientlysecure.keychain.pgp.PgpSignEncryptData; import org.sufficientlysecure.keychain.pgp.Progressable; import org.sufficientlysecure.keychain.pgp.SignEncryptParcel; -import org.sufficientlysecure.keychain.provider.KeyWritableRepository; +import org.sufficientlysecure.keychain.daos.KeyWritableRepository; import org.sufficientlysecure.keychain.service.BenchmarkInputParcel; import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import org.sufficientlysecure.keychain.util.Passphrase; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/CertifyOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/CertifyOperation.java index e2372fdd8..b9a657cd8 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/CertifyOperation.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/CertifyOperation.java @@ -19,10 +19,10 @@ package org.sufficientlysecure.keychain.operations; import java.util.ArrayList; -import java.util.concurrent.atomic.AtomicBoolean; import android.content.Context; import android.support.annotation.NonNull; +import android.support.v4.os.CancellationSignal; import org.sufficientlysecure.keychain.operations.results.CertifyResult; import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType; @@ -38,10 +38,9 @@ import org.sufficientlysecure.keychain.pgp.PgpCertifyOperation.PgpCertifyResult; import org.sufficientlysecure.keychain.pgp.Progressable; import org.sufficientlysecure.keychain.pgp.UncachedKeyRing; import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; -import org.sufficientlysecure.keychain.provider.CachedPublicKeyRing; -import org.sufficientlysecure.keychain.provider.KeyRepository.NotFoundException; -import org.sufficientlysecure.keychain.provider.KeyWritableRepository; -import org.sufficientlysecure.keychain.provider.LastUpdateInteractor; +import org.sufficientlysecure.keychain.daos.KeyMetadataDao; +import org.sufficientlysecure.keychain.daos.KeyRepository.NotFoundException; +import org.sufficientlysecure.keychain.daos.KeyWritableRepository; import org.sufficientlysecure.keychain.service.CertifyActionsParcel; import org.sufficientlysecure.keychain.service.CertifyActionsParcel.CertifyAction; import org.sufficientlysecure.keychain.service.ContactSyncAdapterService; @@ -62,13 +61,13 @@ import org.sufficientlysecure.keychain.util.Passphrase; * @see CertifyActionsParcel */ public class CertifyOperation extends BaseReadWriteOperation { - private final LastUpdateInteractor lastUpdateInteractor; + private final KeyMetadataDao keyMetadataDao; - public CertifyOperation(Context context, KeyWritableRepository databaseInteractor, Progressable progressable, AtomicBoolean + public CertifyOperation(Context context, KeyWritableRepository keyWritableRepository, Progressable progressable, CancellationSignal cancelled) { - super(context, databaseInteractor, progressable, cancelled); + super(context, keyWritableRepository, progressable, cancelled); - this.lastUpdateInteractor = LastUpdateInteractor.create(context); + this.keyMetadataDao = KeyMetadataDao.create(context); } @NonNull @@ -85,10 +84,8 @@ public class CertifyOperation extends BaseReadWriteOperation { - private final LastUpdateInteractor lastUpdateInteractor; + private final KeyMetadataDao keyMetadataDao; public EditKeyOperation(Context context, KeyWritableRepository databaseInteractor, - Progressable progressable, AtomicBoolean cancelled) { + Progressable progressable, CancellationSignal cancelled) { super(context, databaseInteractor, progressable, cancelled); - this.lastUpdateInteractor = LastUpdateInteractor.create(context); + this.keyMetadataDao = KeyMetadataDao.create(context); } /** @@ -171,8 +171,8 @@ public class EditKeyOperation extends BaseReadWriteOperation SaveKeyringResult saveResult = mKeyWritableRepository.saveSecretKeyRing(ring); log.add(saveResult, 1); - if (isNewKey) { - lastUpdateInteractor.renewKeyLastUpdatedTime(ring.getMasterKeyId(), saveParcel.isShouldUpload()); + if (isNewKey || saveParcel.isShouldUpload()) { + keyMetadataDao.renewKeyLastUpdatedTime(ring.getMasterKeyId(), saveParcel.isShouldUpload()); } // If the save operation didn't succeed, exit here diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ImportOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ImportOperation.java index 1bee1c825..e1c1db5cf 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ImportOperation.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ImportOperation.java @@ -29,11 +29,11 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; import android.content.Context; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.v4.os.CancellationSignal; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.keyimport.FacebookKeyserverClient; @@ -54,8 +54,8 @@ import org.sufficientlysecure.keychain.pgp.CanonicalizedKeyRing; import org.sufficientlysecure.keychain.pgp.Progressable; import org.sufficientlysecure.keychain.pgp.UncachedKeyRing; import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; -import org.sufficientlysecure.keychain.provider.KeyWritableRepository; -import org.sufficientlysecure.keychain.provider.LastUpdateInteractor; +import org.sufficientlysecure.keychain.daos.KeyMetadataDao; +import org.sufficientlysecure.keychain.daos.KeyWritableRepository; import org.sufficientlysecure.keychain.service.ContactSyncAdapterService; import org.sufficientlysecure.keychain.service.ImportKeyringParcel; import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; @@ -90,7 +90,7 @@ public class ImportOperation extends BaseReadWriteOperation public static final String CACHE_FILE_NAME = "key_import.pcl"; - private final LastUpdateInteractor lastUpdateInteractor; + private final KeyMetadataDao keyMetadataDao; private FacebookKeyserverClient facebookServer; private KeybaseKeyserverClient keybaseServer; @@ -98,14 +98,14 @@ public class ImportOperation extends BaseReadWriteOperation public ImportOperation(Context context, KeyWritableRepository databaseInteractor, Progressable progressable) { super(context, databaseInteractor, progressable); - this.lastUpdateInteractor = LastUpdateInteractor.create(context); + this.keyMetadataDao = KeyMetadataDao.create(context); } public ImportOperation(Context context, KeyWritableRepository databaseInteractor, - Progressable progressable, AtomicBoolean cancelled) { + Progressable progressable, CancellationSignal cancelled) { super(context, databaseInteractor, progressable, cancelled); - this.lastUpdateInteractor = LastUpdateInteractor.create(context); + this.keyMetadataDao = KeyMetadataDao.create(context); } // Overloaded functions for using progressable supplied in constructor during import @@ -200,7 +200,7 @@ public class ImportOperation extends BaseReadWriteOperation byte[] fingerprintHex = entry.getExpectedFingerprint(); if (fingerprintHex != null) { - lastUpdateInteractor.renewKeyLastUpdatedTime( + keyMetadataDao.renewKeyLastUpdatedTime( KeyFormattingUtils.getKeyIdFromFingerprint(fingerprintHex), false); } continue; @@ -249,8 +249,8 @@ public class ImportOperation extends BaseReadWriteOperation importedMasterKeyIds.add(key.getMasterKeyId()); } - if (!skipSave) { - lastUpdateInteractor.renewKeyLastUpdatedTime(key.getMasterKeyId(), keyWasDownloaded); + if (!skipSave && keyWasDownloaded) { + keyMetadataDao.renewKeyLastUpdatedTime(key.getMasterKeyId(), true); } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/InputDataOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/InputDataOperation.java index ca5ec93c7..222e75d7a 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/InputDataOperation.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/InputDataOperation.java @@ -50,7 +50,7 @@ import org.sufficientlysecure.keychain.operations.results.OperationResult.Operat import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyInputParcel; import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyOperation; import org.sufficientlysecure.keychain.pgp.Progressable; -import org.sufficientlysecure.keychain.provider.KeyRepository; +import org.sufficientlysecure.keychain.daos.KeyRepository; import org.sufficientlysecure.keychain.provider.TemporaryFileProvider; import org.sufficientlysecure.keychain.service.InputDataParcel; import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/KeySyncOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/KeySyncOperation.java new file mode 100644 index 000000000..f332eeee2 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/KeySyncOperation.java @@ -0,0 +1,158 @@ +package org.sufficientlysecure.keychain.operations; + + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.concurrent.TimeUnit; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.v4.os.CancellationSignal; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing; +import org.sufficientlysecure.keychain.operations.results.ImportKeyResult; +import org.sufficientlysecure.keychain.operations.results.OperationResult; +import org.sufficientlysecure.keychain.pgp.Progressable; +import org.sufficientlysecure.keychain.daos.KeyMetadataDao; +import org.sufficientlysecure.keychain.daos.KeyWritableRepository; +import org.sufficientlysecure.keychain.service.ImportKeyringParcel; +import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; +import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; +import org.sufficientlysecure.keychain.util.Preferences; +import timber.log.Timber; + + +public class KeySyncOperation extends BaseReadWriteOperation { + // time since last update after which a key should be updated again, in s + private static final long KEY_STALE_THRESHOLD_MILLIS = + Constants.DEBUG_KEYSERVER_SYNC ? 1 : TimeUnit.DAYS.toMillis(7); + // Time taken by Orbot before a new circuit is created + private static final int ORBOT_CIRCUIT_TIMEOUT_SECONDS = + Constants.DEBUG_KEYSERVER_SYNC ? 2 : (int) TimeUnit.MINUTES.toSeconds(10); + + private final KeyMetadataDao keyMetadataDao; + private final Preferences preferences; + + public KeySyncOperation(Context context, KeyWritableRepository databaseInteractor, + Progressable progressable, CancellationSignal cancellationSignal) { + super(context, databaseInteractor, progressable, cancellationSignal); + + keyMetadataDao = KeyMetadataDao.create(context); + preferences = Preferences.getPreferences(context); + } + + @NonNull + @Override + public ImportKeyResult execute(KeySyncParcel input, CryptoInputParcel cryptoInput) { + long staleKeyThreshold = System.currentTimeMillis() - (input.getRefreshAll() ? 0 : KEY_STALE_THRESHOLD_MILLIS); + List staleKeyFingerprints = + keyMetadataDao.getFingerprintsForKeysOlderThan(staleKeyThreshold, TimeUnit.MILLISECONDS); + List staleKeyParcelableKeyRings = fingerprintListToParcelableKeyRings(staleKeyFingerprints); + + if (checkCancelled()) { // if we've already been cancelled + return new ImportKeyResult(OperationResult.RESULT_CANCELLED, new OperationResult.OperationLog()); + } + + // no explicit proxy, retrieve from preferences. Check if we should do a staggered sync + CryptoInputParcel cryptoInputParcel = CryptoInputParcel.createCryptoInputParcel(); + + ImportKeyResult importKeyResult; + if (preferences.getParcelableProxy().isTorEnabled()) { + importKeyResult = staggeredUpdate(staleKeyParcelableKeyRings, cryptoInputParcel); + } else { + importKeyResult = + directUpdate(staleKeyParcelableKeyRings, cryptoInputParcel); + } + return importKeyResult; + } + + private List fingerprintListToParcelableKeyRings(List staleKeyFingerprints) { + ArrayList result = new ArrayList<>(staleKeyFingerprints.size()); + for (byte[] fingerprint : staleKeyFingerprints) { + Timber.d("Keyserver sync: Updating %s", KeyFormattingUtils.beautifyKeyId(fingerprint)); + result.add(ParcelableKeyRing.createFromReference(fingerprint, null, null, null)); + } + return result; + } + + private ImportKeyResult directUpdate(List keyList, + CryptoInputParcel cryptoInputParcel) { + Timber.d("Starting normal update"); + ImportOperation importOp = new ImportOperation(mContext, mKeyWritableRepository, mProgressable, mCancelled); + return importOp.execute( + ImportKeyringParcel.createImportKeyringParcel(keyList, preferences.getPreferredKeyserver()), + cryptoInputParcel + ); + } + + + /** + * will perform a staggered update of user's keys using delays to ensure new Tor circuits, as + * performed by parcimonie. Relevant issue and method at: + * https://github.com/open-keychain/open-keychain/issues/1337 + * + * @return result of the sync + */ + private ImportKeyResult staggeredUpdate(List keyList, + CryptoInputParcel cryptoInputParcel) { + Timber.d("Starting staggered update"); + // final int WEEK_IN_SECONDS = (int) TimeUnit.DAYS.toSeconds(7); + // we are limiting our randomness to ORBOT_CIRCUIT_TIMEOUT_SECONDS for now + final int WEEK_IN_SECONDS = 0; + + ImportOperation.KeyImportAccumulator accumulator + = new ImportOperation.KeyImportAccumulator(keyList.size(), null); + + // so that the first key can be updated without waiting. This is so that there isn't a + // large gap between a "Start Orbot" notification and the next key update + boolean first = true; + + for (ParcelableKeyRing keyRing : keyList) { + int waitTime; + int staggeredTime = new Random().nextInt(1 + 2 * (WEEK_IN_SECONDS / keyList.size())); + if (staggeredTime >= ORBOT_CIRCUIT_TIMEOUT_SECONDS) { + waitTime = staggeredTime; + } else { + waitTime = ORBOT_CIRCUIT_TIMEOUT_SECONDS + + new Random().nextInt(1 + ORBOT_CIRCUIT_TIMEOUT_SECONDS); + } + + if (first) { + waitTime = 0; + first = false; + } + + Timber.d("Updating key with a wait time of %d seconds", waitTime); + try { + Thread.sleep(waitTime * 1000); + } catch (InterruptedException e) { + Timber.e(e, "Exception during sleep between key updates"); + // skip this one + continue; + } + ArrayList keyWrapper = new ArrayList<>(); + keyWrapper.add(keyRing); + if (checkCancelled()) { + return new ImportKeyResult(ImportKeyResult.RESULT_CANCELLED, + new OperationResult.OperationLog()); + } + ImportKeyResult result = + new ImportOperation(mContext, mKeyWritableRepository, null, mCancelled) + .execute( + ImportKeyringParcel.createImportKeyringParcel( + keyWrapper, + preferences.getPreferredKeyserver() + ), + cryptoInputParcel + ); + if (result.isPending()) { + return result; + } + accumulator.accumulateKeyImport(result); + } + return accumulator.getConsolidatedResult(); + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/KeySyncParcel.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/KeySyncParcel.java new file mode 100644 index 000000000..007d233b5 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/KeySyncParcel.java @@ -0,0 +1,20 @@ +package org.sufficientlysecure.keychain.operations; + + +import android.os.Parcelable; + +import com.google.auto.value.AutoValue; + + +@AutoValue +public abstract class KeySyncParcel implements Parcelable { + public abstract boolean getRefreshAll(); + + public static KeySyncParcel createRefreshAll() { + return new AutoValue_KeySyncParcel(true); + } + + public static KeySyncParcel createRefreshOutdated() { + return new AutoValue_KeySyncParcel(false); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/KeybaseVerificationOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/KeybaseVerificationOperation.java index efddc8269..90c569f71 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/KeybaseVerificationOperation.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/KeybaseVerificationOperation.java @@ -18,16 +18,29 @@ package org.sufficientlysecure.keychain.operations; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.net.Proxy; +import java.util.ArrayList; +import java.util.List; + import android.content.Context; import android.support.annotation.NonNull; import com.textuality.keybase.lib.KeybaseQuery; import com.textuality.keybase.lib.Proof; import com.textuality.keybase.lib.prover.Prover; - -import org.json.JSONObject; +import de.measite.minidns.Client; +import de.measite.minidns.DNSMessage; +import de.measite.minidns.Question; +import de.measite.minidns.Record; +import de.measite.minidns.record.Data; +import de.measite.minidns.record.TXT; import org.bouncycastle.openpgp.PGPUtil; +import org.json.JSONObject; import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.network.OkHttpKeybaseClient; +import org.sufficientlysecure.keychain.network.orbot.OrbotHelper; import org.sufficientlysecure.keychain.operations.results.DecryptVerifyResult; import org.sufficientlysecure.keychain.operations.results.KeybaseVerificationResult; import org.sufficientlysecure.keychain.operations.results.OperationResult; @@ -35,28 +48,12 @@ import org.sufficientlysecure.keychain.operations.results.OperationResult.LogTyp import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyInputParcel; import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyOperation; import org.sufficientlysecure.keychain.pgp.Progressable; -import org.sufficientlysecure.keychain.provider.KeyRepository; -import org.sufficientlysecure.keychain.provider.KeyWritableRepository; +import org.sufficientlysecure.keychain.daos.KeyWritableRepository; import org.sufficientlysecure.keychain.service.KeybaseVerificationParcel; import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; -import org.sufficientlysecure.keychain.network.OkHttpKeybaseClient; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; import org.sufficientlysecure.keychain.util.Preferences; -import org.sufficientlysecure.keychain.network.orbot.OrbotHelper; - -import java.io.ByteArrayInputStream; -import java.io.InputStream; -import java.net.Proxy; -import java.util.ArrayList; -import java.util.List; - -import de.measite.minidns.Client; -import de.measite.minidns.DNSMessage; -import de.measite.minidns.Question; -import de.measite.minidns.Record; -import de.measite.minidns.record.Data; -import de.measite.minidns.record.TXT; public class KeybaseVerificationOperation extends BaseOperation { @@ -162,7 +159,7 @@ public class KeybaseVerificationOperation extends BaseOperation { public PromoteKeyOperation(Context context, KeyWritableRepository databaseInteractor, - Progressable progressable, AtomicBoolean cancelled) { + Progressable progressable, CancellationSignal cancelled) { super(context, databaseInteractor, progressable, cancelled); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/RevokeOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/RevokeOperation.java index 23b3380a2..3df9fc4c5 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/RevokeOperation.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/RevokeOperation.java @@ -19,17 +19,14 @@ package org.sufficientlysecure.keychain.operations; import android.content.Context; -import android.net.Uri; import android.support.annotation.NonNull; +import org.sufficientlysecure.keychain.model.SubKey.UnifiedKeyInfo; import org.sufficientlysecure.keychain.operations.results.EditKeyResult; import org.sufficientlysecure.keychain.operations.results.OperationResult; import org.sufficientlysecure.keychain.operations.results.RevokeResult; import org.sufficientlysecure.keychain.pgp.Progressable; -import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; -import org.sufficientlysecure.keychain.provider.CachedPublicKeyRing; -import org.sufficientlysecure.keychain.provider.KeyWritableRepository; -import org.sufficientlysecure.keychain.provider.KeychainContract; +import org.sufficientlysecure.keychain.daos.KeyWritableRepository; import org.sufficientlysecure.keychain.service.RevokeKeyringParcel; import org.sufficientlysecure.keychain.service.SaveKeyringParcel; import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; @@ -58,19 +55,21 @@ public class RevokeOperation extends BaseReadWriteOperation KeyFormattingUtils.beautifyKeyId(masterKeyId)); try { - - Uri secretUri = KeychainContract.KeyRings.buildUnifiedKeyRingUri(masterKeyId); - CachedPublicKeyRing keyRing = mKeyRepository.getCachedPublicKeyRing(secretUri); + UnifiedKeyInfo keyInfo = mKeyRepository.getUnifiedKeyInfo(masterKeyId); + if (keyInfo == null) { + log.add(OperationResult.LogType.MSG_REVOKE_ERROR_KEY_FAIL, 1); + return new RevokeResult(RevokeResult.RESULT_ERROR, log, masterKeyId); + } // check if this is a master secret key we can work with - switch (keyRing.getSecretKeyType(masterKeyId)) { + switch (mKeyRepository.getSecretKeyType(masterKeyId)) { case GNU_DUMMY: log.add(OperationResult.LogType.MSG_EK_ERROR_DUMMY, 1); return new RevokeResult(RevokeResult.RESULT_ERROR, log, masterKeyId); } SaveKeyringParcel.Builder saveKeyringParcel = - SaveKeyringParcel.buildChangeKeyringParcel(masterKeyId, keyRing.getFingerprint()); + SaveKeyringParcel.buildChangeKeyringParcel(masterKeyId, keyInfo.fingerprint()); // all revoke operations are made atomic as of now saveKeyringParcel.setUpdateOptions(revokeKeyringParcel.isShouldUpload(), true, @@ -96,7 +95,7 @@ public class RevokeOperation extends BaseReadWriteOperation return new RevokeResult(RevokeResult.RESULT_ERROR, log, masterKeyId); } - } catch (PgpKeyNotFoundException | KeyWritableRepository.NotFoundException e) { + } catch (KeyWritableRepository.NotFoundException e) { Timber.e(e, "could not find key to revoke"); log.add(OperationResult.LogType.MSG_REVOKE_ERROR_KEY_FAIL, 1); return new RevokeResult(RevokeResult.RESULT_ERROR, log, masterKeyId); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/SignEncryptOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/SignEncryptOperation.java index ef7387e46..306003b6e 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/SignEncryptOperation.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/SignEncryptOperation.java @@ -20,22 +20,21 @@ package org.sufficientlysecure.keychain.operations; import java.util.ArrayDeque; import java.util.ArrayList; -import java.util.concurrent.atomic.AtomicBoolean; import android.content.Context; import android.net.Uri; import android.support.annotation.NonNull; +import android.support.v4.os.CancellationSignal; import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType; import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; import org.sufficientlysecure.keychain.operations.results.PgpSignEncryptResult; import org.sufficientlysecure.keychain.operations.results.SignEncryptResult; -import org.sufficientlysecure.keychain.pgp.PgpSignEncryptData; import org.sufficientlysecure.keychain.pgp.PgpSignEncryptInputParcel; import org.sufficientlysecure.keychain.pgp.PgpSignEncryptOperation; import org.sufficientlysecure.keychain.pgp.Progressable; import org.sufficientlysecure.keychain.pgp.SignEncryptParcel; -import org.sufficientlysecure.keychain.provider.KeyRepository; +import org.sufficientlysecure.keychain.daos.KeyRepository; import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; import org.sufficientlysecure.keychain.service.input.RequiredInputParcel.RequiredInputType; @@ -53,7 +52,7 @@ import org.sufficientlysecure.keychain.util.ProgressScaler; public class SignEncryptOperation extends BaseOperation { public SignEncryptOperation(Context context, KeyRepository keyRepository, - Progressable progressable, AtomicBoolean cancelled) { + Progressable progressable, CancellationSignal cancelled) { super(context, keyRepository, progressable, cancelled); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/UploadOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/UploadOperation.java index f9a428008..221d7af64 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/UploadOperation.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/UploadOperation.java @@ -18,15 +18,24 @@ package org.sufficientlysecure.keychain.operations; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.net.Proxy; + import android.content.Context; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.v4.os.CancellationSignal; import org.bouncycastle.bcpg.ArmoredOutputStream; import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.daos.KeyMetadataDao; +import org.sufficientlysecure.keychain.daos.KeyRepository; +import org.sufficientlysecure.keychain.daos.KeyWritableRepository; +import org.sufficientlysecure.keychain.keyimport.HkpKeyserverAddress; import org.sufficientlysecure.keychain.keyimport.HkpKeyserverClient; import org.sufficientlysecure.keychain.keyimport.KeyserverClient.AddKeyException; -import org.sufficientlysecure.keychain.keyimport.HkpKeyserverAddress; +import org.sufficientlysecure.keychain.network.orbot.OrbotHelper; import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType; import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; import org.sufficientlysecure.keychain.operations.results.UploadResult; @@ -35,31 +44,26 @@ import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKeyRing; import org.sufficientlysecure.keychain.pgp.Progressable; import org.sufficientlysecure.keychain.pgp.UncachedKeyRing; import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; -import org.sufficientlysecure.keychain.provider.KeyRepository; -import org.sufficientlysecure.keychain.provider.KeyWritableRepository; import org.sufficientlysecure.keychain.service.UploadKeyringParcel; import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; import org.sufficientlysecure.keychain.util.ParcelableProxy; import org.sufficientlysecure.keychain.util.Preferences; -import org.sufficientlysecure.keychain.network.orbot.OrbotHelper; import timber.log.Timber; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.net.Proxy; -import java.util.concurrent.atomic.AtomicBoolean; - /** * An operation class which implements the upload of a single key to a key server. */ public class UploadOperation extends BaseOperation { + private KeyMetadataDao keyMetadataDao; public UploadOperation(Context context, KeyRepository keyRepository, - Progressable progressable, AtomicBoolean cancelled) { + Progressable progressable, CancellationSignal cancelled) { super(context, keyRepository, progressable, cancelled); + + keyMetadataDao = KeyMetadataDao.create(mContext); } @NonNull @@ -150,11 +154,18 @@ public class UploadOperation extends BaseOperation { keyring.encode(aos); aos.close(); + if (checkCancelled()) { + log.add(LogType.MSG_OPERATION_CANCELLED, 0); + return new UploadResult(UploadResult.RESULT_CANCELLED, log); + } + String armoredKey = bos.toString("UTF-8"); keyserverInteractor.add(armoredKey, proxy); updateProgress(R.string.progress_uploading, 1, 1); + keyMetadataDao.renewKeyLastUpdatedTime(keyring.getMasterKeyId(), true); + log.add(LogType.MSG_UPLOAD_SUCCESS, 1); return new UploadResult(UploadResult.RESULT_OK, log); } catch (IOException e) { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/OperationResult.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/OperationResult.java index f3385d9fc..f6426bd8d 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/OperationResult.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/OperationResult.java @@ -419,7 +419,6 @@ public abstract class OperationResult implements Parcelable { // import secret MSG_IS(LogLevel.START, R.string.msg_is), MSG_IS_BAD_TYPE_PUBLIC (LogLevel.WARN, R.string.msg_is_bad_type_public), - MSG_IS_DB_EXCEPTION (LogLevel.DEBUG, R.string.msg_is_db_exception), MSG_IS_ERROR_IO_EXC(LogLevel.DEBUG, R.string.msg_is_error_io_exc), MSG_IS_MERGE_PUBLIC (LogLevel.DEBUG, R.string.msg_is_merge_public), MSG_IS_MERGE_SECRET (LogLevel.DEBUG, R.string.msg_is_merge_secret), diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedKeyRing.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedKeyRing.java index c84ff0d8f..91a4897dd 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedKeyRing.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedKeyRing.java @@ -43,9 +43,9 @@ import org.sufficientlysecure.keychain.util.IterableIterator; */ public abstract class CanonicalizedKeyRing extends KeyRing { - private final int mVerified; + private final VerificationStatus mVerified; - CanonicalizedKeyRing(int verified) { + CanonicalizedKeyRing(VerificationStatus verified) { mVerified = verified; } @@ -53,7 +53,7 @@ public abstract class CanonicalizedKeyRing extends KeyRing { return getRing().getPublicKey().getKeyID(); } - public int getVerified() { + public VerificationStatus getVerified() { return mVerified; } @@ -61,15 +61,11 @@ public abstract class CanonicalizedKeyRing extends KeyRing { return getRing().getPublicKey().getFingerprint(); } - public byte[] getRawPrimaryUserId() throws PgpKeyNotFoundException { + public byte[] getRawPrimaryUserId() { return getPublicKey().getRawPrimaryUserId(); } - public String getPrimaryUserId() throws PgpKeyNotFoundException { - return getPublicKey().getPrimaryUserId(); - } - - public String getPrimaryUserIdWithFallback() throws PgpKeyNotFoundException { + public String getPrimaryUserIdWithFallback() { return getPublicKey().getPrimaryUserIdWithFallback(); } @@ -107,10 +103,6 @@ public abstract class CanonicalizedKeyRing extends KeyRing { return creationDate.after(now) || (expirationDate != null && expirationDate.before(now)); } - public boolean canCertify() throws PgpKeyNotFoundException { - return getRing().getPublicKey().isEncryptionKey(); - } - public Set getEncryptIds() { HashSet result = new HashSet<>(); for (CanonicalizedPublicKey key : publicKeyIterator()) { @@ -130,15 +122,6 @@ public abstract class CanonicalizedKeyRing extends KeyRing { throw new PgpKeyNotFoundException("No valid encryption key found!"); } - public boolean hasEncrypt() throws PgpKeyNotFoundException { - try { - getEncryptId(); - return true; - } catch (PgpKeyNotFoundException e) { - return false; - } - } - public long getSigningId() throws PgpKeyNotFoundException { for(CanonicalizedPublicKey key : publicKeyIterator()) { if (key.canSign() && key.isValid()) { @@ -194,4 +177,8 @@ public abstract class CanonicalizedKeyRing extends KeyRing { return false; } + public enum VerificationStatus { + UNVERIFIED, VERIFIED_SELF, VERIFIED_SECRET + } + } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedPublicKeyRing.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedPublicKeyRing.java index 5e7ae1ec0..a9b24b4ac 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedPublicKeyRing.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedPublicKeyRing.java @@ -41,12 +41,12 @@ public class CanonicalizedPublicKeyRing extends CanonicalizedKeyRing { private PGPPublicKeyRing mRing; - CanonicalizedPublicKeyRing(PGPPublicKeyRing ring, int verified) { + CanonicalizedPublicKeyRing(PGPPublicKeyRing ring, VerificationStatus verified) { super(verified); mRing = ring; } - public CanonicalizedPublicKeyRing(byte[] blob, int verified) { + public CanonicalizedPublicKeyRing(byte[] blob, VerificationStatus verified) { super(verified); if(mRing == null) { // get first object in block @@ -100,7 +100,7 @@ public class CanonicalizedPublicKeyRing extends CanonicalizedKeyRing { * - the user id that matches the userIdToKeep parameter, or the primary user id if none matches * each with their most recent binding certificates */ - public CanonicalizedPublicKeyRing minimize(@Nullable String userIdToKeep) throws IOException, PgpKeyNotFoundException { + public CanonicalizedPublicKeyRing minimize(@Nullable String userIdToKeep) throws IOException { CanonicalizedPublicKey masterKey = getPublicKey(); PGPPublicKey masterPubKey = masterKey.getPublicKey(); boolean userIdStrippedOk = false; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedSecretKey.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedSecretKey.java index b37b572f4..697e5da7d 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedSecretKey.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedSecretKey.java @@ -47,8 +47,7 @@ import org.bouncycastle.openpgp.operator.jcajce.NfcSyncPGPContentSignerBuilder; import org.bouncycastle.openpgp.operator.jcajce.SessionKeySecretKeyDecryptorBuilder; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; -import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; -import org.sufficientlysecure.keychain.provider.KeyWritableRepository; +import org.sufficientlysecure.keychain.daos.KeyWritableRepository; import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import org.sufficientlysecure.keychain.util.Passphrase; import timber.log.Timber; @@ -326,7 +325,7 @@ public class CanonicalizedSecretKey extends CanonicalizedPublicKey { spGen.setSignatureCreationTime(false, creationTimestamp); signatureGenerator.setHashedSubpackets(spGen.generate()); return signatureGenerator; - } catch (PgpKeyNotFoundException | PGPException e) { + } catch (PGPException e) { // TODO: simply throw PGPException! throw new PgpGeneralException("Error initializing signature!", e); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedSecretKeyRing.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedSecretKeyRing.java index b59ee917a..c9ccdec30 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedSecretKeyRing.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedSecretKeyRing.java @@ -35,12 +35,12 @@ public class CanonicalizedSecretKeyRing extends CanonicalizedKeyRing { private PGPSecretKeyRing mRing; - CanonicalizedSecretKeyRing(PGPSecretKeyRing ring, int verified) { + CanonicalizedSecretKeyRing(PGPSecretKeyRing ring, VerificationStatus verified) { super(verified); mRing = ring; } - public CanonicalizedSecretKeyRing(byte[] blob, int verified) + public CanonicalizedSecretKeyRing(byte[] blob, VerificationStatus verified) { super(verified); JcaPGPObjectFactory factory = new JcaPGPObjectFactory(blob); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/KeyRing.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/KeyRing.java index 7909cfdc2..a2493dcab 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/KeyRing.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/KeyRing.java @@ -17,16 +17,12 @@ package org.sufficientlysecure.keychain.pgp; -import android.text.TextUtils; import org.openintents.openpgp.util.OpenPgpUtils; import org.openintents.openpgp.util.OpenPgpUtils.UserId; +import org.sufficientlysecure.keychain.pgp.CanonicalizedKeyRing.VerificationStatus; import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; -import java.io.Serializable; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - /** * An abstract KeyRing. *

@@ -36,29 +32,18 @@ import java.util.regex.Pattern; * here. * * @see CanonicalizedKeyRing - * @see org.sufficientlysecure.keychain.provider.CachedPublicKeyRing */ public abstract class KeyRing { abstract public long getMasterKeyId() throws PgpKeyNotFoundException; - abstract public String getPrimaryUserId() throws PgpKeyNotFoundException; - abstract public String getPrimaryUserIdWithFallback() throws PgpKeyNotFoundException; - public UserId getSplitPrimaryUserIdWithFallback() throws PgpKeyNotFoundException { - return splitUserId(getPrimaryUserIdWithFallback()); - } - abstract public boolean isRevoked() throws PgpKeyNotFoundException; - abstract public boolean canCertify() throws PgpKeyNotFoundException; - abstract public long getEncryptId() throws PgpKeyNotFoundException; - abstract public boolean hasEncrypt() throws PgpKeyNotFoundException; - - abstract public int getVerified() throws PgpKeyNotFoundException; + abstract public VerificationStatus getVerified() throws PgpKeyNotFoundException; /** * Splits userId string into naming part, email part, and comment part diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/OpenPgpSignatureResultBuilder.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/OpenPgpSignatureResultBuilder.java index f7034800b..033879f51 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/OpenPgpSignatureResultBuilder.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/OpenPgpSignatureResultBuilder.java @@ -20,14 +20,14 @@ package org.sufficientlysecure.keychain.pgp; import java.util.ArrayList; import java.util.Date; +import java.util.List; import org.openintents.openpgp.OpenPgpSignatureResult; import org.openintents.openpgp.OpenPgpSignatureResult.SenderStatusResult; import org.openintents.openpgp.util.OpenPgpUtils; import org.openintents.openpgp.util.OpenPgpUtils.UserId; -import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; -import org.sufficientlysecure.keychain.provider.KeyRepository; -import org.sufficientlysecure.keychain.provider.KeyRepository.NotFoundException; +import org.sufficientlysecure.keychain.pgp.CanonicalizedKeyRing.VerificationStatus; +import org.sufficientlysecure.keychain.daos.KeyRepository; import timber.log.Timber; @@ -41,8 +41,8 @@ public class OpenPgpSignatureResultBuilder { // OpenPgpSignatureResult private String mPrimaryUserId; - private ArrayList mUserIds = new ArrayList<>(); - private ArrayList mConfirmedUserIds; + private List mUserIds = new ArrayList<>(); + private List mConfirmedUserIds; private long mKeyId; private SenderStatusResult mSenderStatusResult; @@ -101,7 +101,7 @@ public class OpenPgpSignatureResultBuilder { this.mIsKeyExpired = keyExpired; } - public void setUserIds(ArrayList userIds, ArrayList confirmedUserIds) { + public void setUserIds(List userIds, List confirmedUserIds) { this.mUserIds = userIds; this.mConfirmedUserIds = confirmedUserIds; } @@ -118,20 +118,11 @@ public class OpenPgpSignatureResultBuilder { // from RING setKeyId(signingRing.getMasterKeyId()); - try { - setPrimaryUserId(signingRing.getPrimaryUserIdWithFallback()); - } catch (PgpKeyNotFoundException e) { - Timber.d("No primary user id in keyring with master key id " + signingRing.getMasterKeyId()); - } - setSignatureKeyCertified(signingRing.getVerified() > 0); + setPrimaryUserId(signingRing.getPrimaryUserIdWithFallback()); + setSignatureKeyCertified(signingRing.getVerified() == VerificationStatus.VERIFIED_SECRET); - ArrayList allUserIds = signingRing.getUnorderedUserIds(); - ArrayList confirmedUserIds; - try { - confirmedUserIds = mKeyRepository.getConfirmedUserIds(signingRing.getMasterKeyId()); - } catch (NotFoundException e) { - throw new IllegalStateException("Key didn't exist anymore for user id query!", e); - } + List allUserIds = signingRing.getUnorderedUserIds(); + List confirmedUserIds = mKeyRepository.getConfirmedUserIds(signingRing.getMasterKeyId()); setUserIds(allUserIds, confirmedUserIds); mSenderStatusResult = processSenderStatusResult(allUserIds, confirmedUserIds); @@ -142,7 +133,7 @@ public class OpenPgpSignatureResultBuilder { } private SenderStatusResult processSenderStatusResult( - ArrayList allUserIds, ArrayList confirmedUserIds) { + List allUserIds, List confirmedUserIds) { if (mSenderAddress == null) { return SenderStatusResult.UNKNOWN; } @@ -156,7 +147,7 @@ public class OpenPgpSignatureResultBuilder { } } - private static boolean userIdListContainsAddress(String senderAddress, ArrayList confirmedUserIds) { + private static boolean userIdListContainsAddress(String senderAddress, List confirmedUserIds) { for (String rawUserId : confirmedUserIds) { UserId userId = OpenPgpUtils.splitUserId(rawUserId); if (senderAddress.equalsIgnoreCase(userId.email)) { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerifyOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerifyOperation.java index 6a5881e79..9fdc6362d 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerifyOperation.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerifyOperation.java @@ -71,11 +71,8 @@ import org.sufficientlysecure.keychain.pgp.SecurityProblem.EncryptionAlgorithmPr import org.sufficientlysecure.keychain.pgp.SecurityProblem.KeySecurityProblem; import org.sufficientlysecure.keychain.pgp.SecurityProblem.MissingMdc; import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; -import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; -import org.sufficientlysecure.keychain.provider.CachedPublicKeyRing; -import org.sufficientlysecure.keychain.provider.KeyRepository; -import org.sufficientlysecure.keychain.provider.KeyWritableRepository; -import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; +import org.sufficientlysecure.keychain.daos.KeyRepository; +import org.sufficientlysecure.keychain.daos.KeyWritableRepository; import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; import org.sufficientlysecure.keychain.service.input.RequiredInputParcel.RequireAnyDecryptPassphraseBuilder; @@ -635,13 +632,13 @@ public class PgpDecryptVerifyOperation extends BaseOperation mProgress; - private AtomicBoolean mCancelled; + private CancellationSignal mCancelled; public PgpKeyOperation(Progressable progress) { super(); @@ -118,13 +119,13 @@ public class PgpKeyOperation { } } - public PgpKeyOperation(Progressable progress, AtomicBoolean cancelled) { + public PgpKeyOperation(Progressable progress, CancellationSignal cancelled) { this(progress); mCancelled = cancelled; } private boolean checkCancelled() { - return mCancelled != null && mCancelled.get(); + return mCancelled != null && mCancelled.isCanceled(); } private void subProgressPush(int from, int to) { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSignEncryptOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSignEncryptOperation.java index bf24bebfb..9d071fc4b 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSignEncryptOperation.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSignEncryptOperation.java @@ -33,11 +33,11 @@ import java.security.SignatureException; import java.util.Collection; import java.util.Date; import java.util.Set; -import java.util.concurrent.atomic.AtomicBoolean; import android.content.Context; import android.net.Uri; import android.support.annotation.NonNull; +import android.support.v4.os.CancellationSignal; import org.bouncycastle.bcpg.ArmoredOutputStream; import org.bouncycastle.bcpg.BCPGOutputStream; @@ -63,10 +63,9 @@ import org.sufficientlysecure.keychain.pgp.PgpSecurityConstants.OpenKeychainComp import org.sufficientlysecure.keychain.pgp.PgpSecurityConstants.OpenKeychainHashAlgorithmTags; import org.sufficientlysecure.keychain.pgp.PgpSecurityConstants.OpenKeychainSymmetricKeyAlgorithmTags; import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; -import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; -import org.sufficientlysecure.keychain.provider.KeyRepository; -import org.sufficientlysecure.keychain.provider.KeyWritableRepository; -import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; +import org.sufficientlysecure.keychain.daos.KeyRepository; +import org.sufficientlysecure.keychain.daos.KeyRepository.NotFoundException; +import org.sufficientlysecure.keychain.daos.KeyWritableRepository; import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; @@ -105,7 +104,7 @@ public class PgpSignEncryptOperation extends BaseOperation encryptSubKeyIds = keyRing.getEncryptIds(); for (Long subKeyId : encryptSubKeyIds) { CanonicalizedPublicKey key = keyRing.getPublicKey(subKeyId); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSignatureChecker.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSignatureChecker.java index 6d1313451..1da7077be 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSignatureChecker.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSignatureChecker.java @@ -38,9 +38,8 @@ import org.sufficientlysecure.keychain.operations.results.OperationResult.Operat import org.sufficientlysecure.keychain.pgp.DecryptVerifySecurityProblem.DecryptVerifySecurityProblemBuilder; import org.sufficientlysecure.keychain.pgp.SecurityProblem.InsecureSigningAlgorithm; import org.sufficientlysecure.keychain.pgp.SecurityProblem.KeySecurityProblem; -import org.sufficientlysecure.keychain.provider.KeyRepository; -import org.sufficientlysecure.keychain.provider.KeyWritableRepository; -import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; +import org.sufficientlysecure.keychain.daos.KeyRepository; +import org.sufficientlysecure.keychain.daos.KeyWritableRepository; import timber.log.Timber; @@ -160,9 +159,11 @@ class PgpSignatureChecker { for (int i = 0; i < sigList.size(); ++i) { try { long sigKeyId = sigList.get(i).getKeyID(); - CanonicalizedPublicKeyRing signingRing = mKeyRepository.getCanonicalizedPublicKeyRing( - KeyRings.buildUnifiedKeyRingsFindBySubkeyUri(sigKeyId) - ); + Long masterKeyId = mKeyRepository.getMasterKeyIdBySubkeyId(sigKeyId); + if (masterKeyId == null) { + continue; + } + CanonicalizedPublicKeyRing signingRing = mKeyRepository.getCanonicalizedPublicKeyRing(masterKeyId); CanonicalizedPublicKey keyCandidate = signingRing.getPublicKey(sigKeyId); if ( ! keyCandidate.canSign()) { continue; @@ -183,9 +184,11 @@ class PgpSignatureChecker { for (int i = 0; i < sigList.size(); ++i) { try { long sigKeyId = sigList.get(i).getKeyID(); - CanonicalizedPublicKeyRing signingRing = mKeyRepository.getCanonicalizedPublicKeyRing( - KeyRings.buildUnifiedKeyRingsFindBySubkeyUri(sigKeyId) - ); + Long masterKeyId = mKeyRepository.getMasterKeyIdBySubkeyId(sigKeyId); + if (masterKeyId == null) { + continue; + } + CanonicalizedPublicKeyRing signingRing = mKeyRepository.getCanonicalizedPublicKeyRing(masterKeyId); CanonicalizedPublicKey keyCandidate = signingRing.getPublicKey(sigKeyId); if ( ! keyCandidate.canSign()) { continue; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedKeyRing.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedKeyRing.java index 93986c5a1..2aa8bd05c 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedKeyRing.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedKeyRing.java @@ -64,6 +64,7 @@ import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType; import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; +import org.sufficientlysecure.keychain.pgp.CanonicalizedKeyRing.VerificationStatus; import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; import org.sufficientlysecure.keychain.util.IterableIterator; @@ -1115,8 +1116,8 @@ public class UncachedKeyRing { log.add(LogType.MSG_KC_SUCCESS, indent); } - return isSecret() ? new CanonicalizedSecretKeyRing((PGPSecretKeyRing) ring, 1) - : new CanonicalizedPublicKeyRing((PGPPublicKeyRing) ring, 0); + return isSecret() ? new CanonicalizedSecretKeyRing((PGPSecretKeyRing) ring, VerificationStatus.VERIFIED_SECRET) + : new CanonicalizedPublicKeyRing((PGPPublicKeyRing) ring, VerificationStatus.UNVERIFIED); } /** This operation merges information from a different keyring, returning a combined diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/ApiDataAccessObject.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/ApiDataAccessObject.java deleted file mode 100644 index ed33c1863..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/ApiDataAccessObject.java +++ /dev/null @@ -1,198 +0,0 @@ -/* - * Copyright (C) 2017 Schürmann & Breitmoser GbR - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sufficientlysecure.keychain.provider; - - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.Set; - -import android.content.ContentResolver; -import android.content.ContentValues; -import android.content.Context; -import android.content.OperationApplicationException; -import android.database.Cursor; -import android.net.Uri; -import android.os.RemoteException; - -import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAllowedKeys; -import org.sufficientlysecure.keychain.provider.KeychainContract.ApiApps; -import org.sufficientlysecure.keychain.remote.AppSettings; - - -public class ApiDataAccessObject { - - private final SimpleContentResolverInterface mQueryInterface; - - public ApiDataAccessObject(Context context) { - final ContentResolver contentResolver = context.getContentResolver(); - mQueryInterface = new SimpleContentResolverInterface() { - @Override - public Cursor query(Uri contentUri, String[] projection, String selection, String[] selectionArgs, - String sortOrder) { - return contentResolver.query(contentUri, projection, selection, selectionArgs, sortOrder); - } - - @Override - public Uri insert(Uri contentUri, ContentValues values) { - return contentResolver.insert(contentUri, values); - } - - @Override - public int update(Uri contentUri, ContentValues values, String where, String[] selectionArgs) { - return contentResolver.update(contentUri, values, where, selectionArgs); - } - - @Override - public int delete(Uri contentUri, String where, String[] selectionArgs) { - return contentResolver.delete(contentUri, where, selectionArgs); - } - }; - } - - public ApiDataAccessObject(SimpleContentResolverInterface queryInterface) { - mQueryInterface = queryInterface; - } - - public ArrayList getRegisteredApiApps() { - Cursor cursor = mQueryInterface.query(ApiApps.CONTENT_URI, null, null, null, null); - - ArrayList packageNames = new ArrayList<>(); - try { - if (cursor != null) { - int packageNameCol = cursor.getColumnIndex(ApiApps.PACKAGE_NAME); - if (cursor.moveToFirst()) { - do { - packageNames.add(cursor.getString(packageNameCol)); - } while (cursor.moveToNext()); - } - } - } finally { - if (cursor != null) { - cursor.close(); - } - } - - return packageNames; - } - - private ContentValues contentValueForApiApps(AppSettings appSettings) { - ContentValues values = new ContentValues(); - values.put(ApiApps.PACKAGE_NAME, appSettings.getPackageName()); - values.put(ApiApps.PACKAGE_CERTIFICATE, appSettings.getPackageCertificate()); - return values; - } - - public void insertApiApp(AppSettings appSettings) { - mQueryInterface.insert(ApiApps.CONTENT_URI, contentValueForApiApps(appSettings)); - } - - public void deleteApiApp(String packageName) { - mQueryInterface.delete(ApiApps.buildByPackageNameUri(packageName), null, null); - } - - /** - * Must be an uri pointing to an account - */ - public AppSettings getApiAppSettings(Uri uri) { - AppSettings settings = null; - - Cursor cursor = mQueryInterface.query(uri, null, null, null, null); - try { - if (cursor != null && cursor.moveToFirst()) { - settings = new AppSettings(); - settings.setPackageName(cursor.getString( - cursor.getColumnIndex(ApiApps.PACKAGE_NAME))); - settings.setPackageCertificate(cursor.getBlob( - cursor.getColumnIndex(ApiApps.PACKAGE_CERTIFICATE))); - } - } finally { - if (cursor != null) { - cursor.close(); - } - } - - return settings; - } - - public HashSet getAllowedKeyIdsForApp(Uri uri) { - HashSet keyIds = new HashSet<>(); - - Cursor cursor = mQueryInterface.query(uri, null, null, null, null); - try { - if (cursor != null) { - int keyIdColumn = cursor.getColumnIndex(ApiAllowedKeys.KEY_ID); - while (cursor.moveToNext()) { - keyIds.add(cursor.getLong(keyIdColumn)); - } - } - } finally { - if (cursor != null) { - cursor.close(); - } - } - - return keyIds; - } - - public void saveAllowedKeyIdsForApp(Uri uri, Set allowedKeyIds) - throws RemoteException, OperationApplicationException { - // wipe whole table of allowed keys for this account - mQueryInterface.delete(uri, null, null); - - // re-insert allowed key ids - for (Long keyId : allowedKeyIds) { - ContentValues values = new ContentValues(); - values.put(ApiAllowedKeys.KEY_ID, keyId); - mQueryInterface.insert(uri, values); - } - } - - public void addAllowedKeyIdForApp(Uri uri, long allowedKeyId) { - ContentValues values = new ContentValues(); - values.put(ApiAllowedKeys.KEY_ID, allowedKeyId); - mQueryInterface.insert(uri, values); - } - - public void addAllowedKeyIdForApp(String packageName, long allowedKeyId) { - Uri uri = ApiAllowedKeys.buildBaseUri(packageName); - addAllowedKeyIdForApp(uri, allowedKeyId); - } - - public byte[] getApiAppCertificate(String packageName) { - Uri queryUri = ApiApps.buildByPackageNameUri(packageName); - - String[] projection = new String[]{ApiApps.PACKAGE_CERTIFICATE}; - - Cursor cursor = mQueryInterface.query(queryUri, projection, null, null, null); - try { - byte[] signature = null; - if (cursor != null && cursor.moveToFirst()) { - int signatureCol = 0; - - signature = cursor.getBlob(signatureCol); - } - return signature; - } finally { - if (cursor != null) { - cursor.close(); - } - } - } - -} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/AutocryptPeerDataAccessObject.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/AutocryptPeerDataAccessObject.java deleted file mode 100644 index d7d4707dd..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/AutocryptPeerDataAccessObject.java +++ /dev/null @@ -1,328 +0,0 @@ -/* - * Copyright (C) 2017 Schürmann & Breitmoser GbR - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sufficientlysecure.keychain.provider; - - -import java.util.ArrayList; -import java.util.Date; -import java.util.List; - -import android.content.ContentResolver; -import android.content.ContentValues; -import android.content.Context; -import android.database.Cursor; -import android.net.Uri; -import android.support.annotation.Nullable; -import android.text.format.DateUtils; - -import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAutocryptPeer; - - -public class AutocryptPeerDataAccessObject { - private static final long AUTOCRYPT_DISCOURAGE_THRESHOLD_MILLIS = 35 * DateUtils.DAY_IN_MILLIS; - - private static final String[] PROJECTION_LAST_SEEN = { ApiAutocryptPeer.LAST_SEEN }; - private static final String[] PROJECTION_LAST_SEEN_KEY = { ApiAutocryptPeer.LAST_SEEN_KEY }; - private static final String[] PROJECTION_GOSSIP_LAST_SEEN_KEY = { ApiAutocryptPeer.GOSSIP_LAST_SEEN_KEY }; - private static final String[] PROJECTION_MASTER_KEY_ID = { ApiAutocryptPeer.MASTER_KEY_ID }; - - private static final String[] PROJECTION_AUTOCRYPT_QUERY = { - ApiAutocryptPeer.IDENTIFIER, - ApiAutocryptPeer.LAST_SEEN, - ApiAutocryptPeer.MASTER_KEY_ID, - ApiAutocryptPeer.LAST_SEEN_KEY, - ApiAutocryptPeer.IS_MUTUAL, - ApiAutocryptPeer.KEY_IS_REVOKED, - ApiAutocryptPeer.KEY_IS_EXPIRED, - ApiAutocryptPeer.KEY_IS_VERIFIED, - ApiAutocryptPeer.GOSSIP_MASTER_KEY_ID, - ApiAutocryptPeer.GOSSIP_LAST_SEEN_KEY, - ApiAutocryptPeer.GOSSIP_KEY_IS_REVOKED, - ApiAutocryptPeer.GOSSIP_KEY_IS_EXPIRED, - ApiAutocryptPeer.GOSSIP_KEY_IS_VERIFIED, - }; - private static final int INDEX_IDENTIFIER = 0; - private static final int INDEX_LAST_SEEN = 1; - private static final int INDEX_MASTER_KEY_ID = 2; - private static final int INDEX_LAST_SEEN_KEY = 3; - private static final int INDEX_STATE = 4; - private static final int INDEX_KEY_IS_REVOKED = 5; - private static final int INDEX_KEY_IS_EXPIRED = 6; - private static final int INDEX_KEY_IS_VERIFIED = 7; - private static final int INDEX_GOSSIP_MASTER_KEY_ID = 8; - private static final int INDEX_GOSSIP_LAST_SEEN_KEY = 9; - private static final int INDEX_GOSSIP_KEY_IS_REVOKED = 10; - private static final int INDEX_GOSSIP_KEY_IS_EXPIRED = 11; - private static final int INDEX_GOSSIP_KEY_IS_VERIFIED = 12; - - private final SimpleContentResolverInterface queryInterface; - private final String packageName; - private final DatabaseNotifyManager databaseNotifyManager; - - public AutocryptPeerDataAccessObject(Context context, String packageName) { - this.packageName = packageName; - this.databaseNotifyManager = DatabaseNotifyManager.create(context); - - final ContentResolver contentResolver = context.getContentResolver(); - queryInterface = new SimpleContentResolverInterface() { - @Override - public Cursor query(Uri contentUri, String[] projection, String selection, String[] selectionArgs, - String sortOrder) { - return contentResolver.query(contentUri, projection, selection, selectionArgs, sortOrder); - } - - @Override - public Uri insert(Uri contentUri, ContentValues values) { - return contentResolver.insert(contentUri, values); - } - - @Override - public int update(Uri contentUri, ContentValues values, String where, String[] selectionArgs) { - return contentResolver.update(contentUri, values, where, selectionArgs); - } - - @Override - public int delete(Uri contentUri, String where, String[] selectionArgs) { - return contentResolver.delete(contentUri, where, selectionArgs); - } - }; - } - - public AutocryptPeerDataAccessObject(SimpleContentResolverInterface queryInterface, String packageName, - DatabaseNotifyManager databaseNotifyManager) { - this.queryInterface = queryInterface; - this.packageName = packageName; - this.databaseNotifyManager = databaseNotifyManager; - } - - public Long getMasterKeyIdForAutocryptPeer(String autocryptId) { - Cursor cursor = queryInterface.query( - ApiAutocryptPeer.buildByPackageNameAndAutocryptId(packageName, autocryptId), - PROJECTION_MASTER_KEY_ID, null, null, null); - - try { - if (cursor != null && cursor.moveToFirst()) { - return cursor.getLong(0); - } - } finally { - if (cursor != null) { - cursor.close(); - } - } - - return null; - } - - public Date getLastSeen(String autocryptId) { - Cursor cursor = queryInterface.query(ApiAutocryptPeer.buildByPackageNameAndAutocryptId(packageName, autocryptId), - PROJECTION_LAST_SEEN, null, null, null); - - try { - if (cursor != null && cursor.moveToFirst()) { - long lastUpdated = cursor.getLong(0); - return new Date(lastUpdated); - } - } finally { - if (cursor != null) { - cursor.close(); - } - } - return null; - } - - public Date getLastSeenKey(String autocryptId) { - Cursor cursor = queryInterface.query(ApiAutocryptPeer.buildByPackageNameAndAutocryptId(packageName, autocryptId), - PROJECTION_LAST_SEEN_KEY, null, null, null); - - try { - if (cursor != null && cursor.moveToFirst()) { - long lastUpdated = cursor.getLong(0); - return new Date(lastUpdated); - } - } finally { - if (cursor != null) { - cursor.close(); - } - } - return null; - } - - public Date getLastSeenGossip(String autocryptId) { - Cursor cursor = queryInterface.query(ApiAutocryptPeer.buildByPackageNameAndAutocryptId(packageName, autocryptId), - PROJECTION_GOSSIP_LAST_SEEN_KEY, null, null, null); - - try { - if (cursor != null && cursor.moveToFirst()) { - long lastUpdated = cursor.getLong(0); - return new Date(lastUpdated); - } - } finally { - if (cursor != null) { - cursor.close(); - } - } - return null; - } - - public void updateLastSeen(String autocryptId, Date date) { - ContentValues cv = new ContentValues(); - cv.put(ApiAutocryptPeer.LAST_SEEN, date.getTime()); - queryInterface - .update(ApiAutocryptPeer.buildByPackageNameAndAutocryptId(packageName, autocryptId), cv, null, null); - } - - public void updateKey(String autocryptId, Date effectiveDate, long masterKeyId, boolean isMutual) { - ContentValues cv = new ContentValues(); - cv.put(ApiAutocryptPeer.MASTER_KEY_ID, masterKeyId); - cv.put(ApiAutocryptPeer.LAST_SEEN_KEY, effectiveDate.getTime()); - cv.put(ApiAutocryptPeer.IS_MUTUAL, isMutual ? 1 : 0); - queryInterface - .update(ApiAutocryptPeer.buildByPackageNameAndAutocryptId(packageName, autocryptId), cv, null, null); - databaseNotifyManager.notifyAutocryptUpdate(autocryptId, masterKeyId); - } - - public void updateKeyGossipFromAutocrypt(String autocryptId, Date effectiveDate, long masterKeyId) { - updateKeyGossip(autocryptId, effectiveDate, masterKeyId, ApiAutocryptPeer.GOSSIP_ORIGIN_AUTOCRYPT); - } - - public void updateKeyGossipFromSignature(String autocryptId, Date effectiveDate, long masterKeyId) { - updateKeyGossip(autocryptId, effectiveDate, masterKeyId, ApiAutocryptPeer.GOSSIP_ORIGIN_SIGNATURE); - } - - public void updateKeyGossipFromDedup(String autocryptId, Date effectiveDate, long masterKeyId) { - updateKeyGossip(autocryptId, effectiveDate, masterKeyId, ApiAutocryptPeer.GOSSIP_ORIGIN_DEDUP); - } - - private void updateKeyGossip(String autocryptId, Date effectiveDate, long masterKeyId, int origin) { - ContentValues cv = new ContentValues(); - cv.put(ApiAutocryptPeer.GOSSIP_MASTER_KEY_ID, masterKeyId); - cv.put(ApiAutocryptPeer.GOSSIP_LAST_SEEN_KEY, effectiveDate.getTime()); - cv.put(ApiAutocryptPeer.GOSSIP_ORIGIN, origin); - queryInterface - .update(ApiAutocryptPeer.buildByPackageNameAndAutocryptId(packageName, autocryptId), cv, null, null); - databaseNotifyManager.notifyAutocryptUpdate(autocryptId, masterKeyId); - } - - public void delete(String autocryptId) { - Long masterKeyId = getMasterKeyIdForAutocryptPeer(autocryptId); - queryInterface.delete(ApiAutocryptPeer.buildByPackageNameAndAutocryptId(packageName, autocryptId), null, null); - databaseNotifyManager.notifyAutocryptDelete(autocryptId, masterKeyId); - } - - public List determineAutocryptRecommendations(String... autocryptIds) { - List result = new ArrayList<>(autocryptIds.length); - - Cursor cursor = queryAutocryptPeerData(autocryptIds); - try { - while (cursor.moveToNext()) { - AutocryptRecommendationResult peerResult = determineAutocryptRecommendation(cursor); - result.add(peerResult); - } - } finally { - cursor.close(); - } - - return result; - } - - /** Determines Autocrypt "ui-recommendation", according to spec. - * See https://autocrypt.org/level1.html#recommendations-for-single-recipient-messages - */ - private AutocryptRecommendationResult determineAutocryptRecommendation(Cursor cursor) { - String peerId = cursor.getString(INDEX_IDENTIFIER); - - AutocryptRecommendationResult keyRecommendation = determineAutocryptKeyRecommendation(peerId, cursor); - if (keyRecommendation != null) return keyRecommendation; - - AutocryptRecommendationResult gossipRecommendation = determineAutocryptGossipRecommendation(peerId, cursor); - if (gossipRecommendation != null) return gossipRecommendation; - - return new AutocryptRecommendationResult(peerId, AutocryptState.DISABLE, null, false); - } - - @Nullable - private AutocryptRecommendationResult determineAutocryptKeyRecommendation(String peerId, Cursor cursor) { - boolean hasKey = !cursor.isNull(INDEX_MASTER_KEY_ID); - boolean isRevoked = cursor.getInt(INDEX_KEY_IS_REVOKED) != 0; - boolean isExpired = cursor.getInt(INDEX_KEY_IS_EXPIRED) != 0; - if (!hasKey || isRevoked || isExpired) { - return null; - } - - long masterKeyId = cursor.getLong(INDEX_MASTER_KEY_ID); - long lastSeen = cursor.getLong(INDEX_LAST_SEEN); - long lastSeenKey = cursor.getLong(INDEX_LAST_SEEN_KEY); - boolean isVerified = cursor.getInt(INDEX_KEY_IS_VERIFIED) != 0; - if (lastSeenKey < (lastSeen - AUTOCRYPT_DISCOURAGE_THRESHOLD_MILLIS)) { - return new AutocryptRecommendationResult(peerId, AutocryptState.DISCOURAGED_OLD, masterKeyId, isVerified); - } - - boolean isMutual = cursor.getInt(INDEX_STATE) != 0; - if (isMutual) { - return new AutocryptRecommendationResult(peerId, AutocryptState.MUTUAL, masterKeyId, isVerified); - } else { - return new AutocryptRecommendationResult(peerId, AutocryptState.AVAILABLE, masterKeyId, isVerified); - } - } - - @Nullable - private AutocryptRecommendationResult determineAutocryptGossipRecommendation(String peerId, Cursor cursor) { - boolean gossipHasKey = !cursor.isNull(INDEX_GOSSIP_MASTER_KEY_ID); - boolean gossipIsRevoked = cursor.getInt(INDEX_GOSSIP_KEY_IS_REVOKED) != 0; - boolean gossipIsExpired = cursor.getInt(INDEX_GOSSIP_KEY_IS_EXPIRED) != 0; - boolean isVerified = cursor.getInt(INDEX_GOSSIP_KEY_IS_VERIFIED) != 0; - - if (!gossipHasKey || gossipIsRevoked || gossipIsExpired) { - return null; - } - - long masterKeyId = cursor.getLong(INDEX_GOSSIP_MASTER_KEY_ID); - return new AutocryptRecommendationResult(peerId, AutocryptState.DISCOURAGED_GOSSIP, masterKeyId, isVerified); - } - - private Cursor queryAutocryptPeerData(String[] autocryptIds) { - StringBuilder selection = new StringBuilder(ApiAutocryptPeer.IDENTIFIER + " IN (?"); - for (int i = 1; i < autocryptIds.length; i++) { - selection.append(",?"); - } - selection.append(")"); - - return queryInterface.query(ApiAutocryptPeer.buildByPackageName(packageName), - PROJECTION_AUTOCRYPT_QUERY, selection.toString(), autocryptIds, null); - } - - public static class AutocryptRecommendationResult { - public final String peerId; - public final Long masterKeyId; - public final AutocryptState autocryptState; - public final boolean isVerified; - - AutocryptRecommendationResult(String peerId, AutocryptState autocryptState, Long masterKeyId, - boolean isVerified) { - this.peerId = peerId; - this.autocryptState = autocryptState; - this.masterKeyId = masterKeyId; - this.isVerified = isVerified; - } - - } - - public enum AutocryptState { - DISABLE, DISCOURAGED_OLD, DISCOURAGED_GOSSIP, AVAILABLE, MUTUAL - } -} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/CachedPublicKeyRing.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/CachedPublicKeyRing.java deleted file mode 100644 index 077c72285..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/CachedPublicKeyRing.java +++ /dev/null @@ -1,287 +0,0 @@ -/* - * Copyright (C) 2017 Schürmann & Breitmoser GbR - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sufficientlysecure.keychain.provider; - - -import android.net.Uri; - -import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey.SecretKeyType; -import org.sufficientlysecure.keychain.pgp.KeyRing; -import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; -import org.sufficientlysecure.keychain.provider.KeyRepository.NotFoundException; -import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; -import org.sufficientlysecure.keychain.provider.KeychainContract.Keys; -import timber.log.Timber; - - -/** This implementation of KeyRing provides a cached view of PublicKeyRing - * objects based on database queries exclusively. - * - * This class should be used where only few points of data but no actual - * cryptographic operations are required about a PublicKeyRing which is already - * in the database. This happens commonly in UI code, where parsing of a PGP - * key for examination would be a very expensive operation. - * - * Each getter method is implemented using a more or less expensive database - * query, while object construction is (almost) free. A common pattern is - * mProviderHelper.getCachedKeyRing(uri).getterMethod() - * - * TODO Ensure that the values returned here always match the ones returned by - * the parsed KeyRing! - * - */ -public class CachedPublicKeyRing extends KeyRing { - - final KeyRepository mKeyRepository; - final Uri mUri; - - public CachedPublicKeyRing(KeyRepository keyRepository, Uri uri) { - mKeyRepository = keyRepository; - mUri = uri; - } - - @Override - public long getMasterKeyId() throws PgpKeyNotFoundException { - try { - Object data = mKeyRepository.getGenericData(mUri, - KeychainContract.KeyRings.MASTER_KEY_ID, KeyRepository.FIELD_TYPE_INTEGER); - return (Long) data; - } catch (KeyWritableRepository.NotFoundException e) { - throw new PgpKeyNotFoundException(e); - } - } - - /** - * Find the master key id related to a given query. The id will either be extracted from the - * query, which should work for all specific /key_rings/ queries, or will be queried if it can't. - */ - public long extractOrGetMasterKeyId() throws PgpKeyNotFoundException { - // try extracting from the uri first - String firstSegment = mUri.getPathSegments().get(1); - if (!"find".equals(firstSegment)) try { - return Long.parseLong(firstSegment); - } catch (NumberFormatException e) { - // didn't work? oh well. - Timber.d("Couldn't get masterKeyId from URI, querying..."); - } - return getMasterKeyId(); - } - - public byte[] getFingerprint() throws PgpKeyNotFoundException { - try { - Object data = mKeyRepository.getGenericData(mUri, - KeychainContract.KeyRings.FINGERPRINT, KeyRepository.FIELD_TYPE_BLOB); - return (byte[]) data; - } catch (KeyWritableRepository.NotFoundException e) { - throw new PgpKeyNotFoundException(e); - } - } - - public long getCreationTime() throws PgpKeyNotFoundException { - try { - Object data = mKeyRepository.getGenericData(mUri, - KeychainContract.KeyRings.CREATION, KeyRepository.FIELD_TYPE_INTEGER); - return (long) data; - } catch (KeyWritableRepository.NotFoundException e) { - throw new PgpKeyNotFoundException(e); - } - } - - @Override - public String getPrimaryUserId() throws PgpKeyNotFoundException { - try { - Object data = mKeyRepository.getGenericData(mUri, - KeychainContract.KeyRings.USER_ID, - KeyRepository.FIELD_TYPE_STRING); - return (String) data; - } catch(KeyWritableRepository.NotFoundException e) { - throw new PgpKeyNotFoundException(e); - } - } - - public String getPrimaryUserIdWithFallback() throws PgpKeyNotFoundException { - return getPrimaryUserId(); - } - - public String getName() throws PgpKeyNotFoundException { - try { - Object data = mKeyRepository.getGenericData(mUri, - KeyRings.NAME, - KeyRepository.FIELD_TYPE_STRING); - return (String) data; - } catch(KeyWritableRepository.NotFoundException e) { - throw new PgpKeyNotFoundException(e); - } - } - - public String getEmail() throws PgpKeyNotFoundException { - try { - Object data = mKeyRepository.getGenericData(mUri, - KeyRings.EMAIL, - KeyRepository.FIELD_TYPE_STRING); - return (String) data; - } catch(KeyWritableRepository.NotFoundException e) { - throw new PgpKeyNotFoundException(e); - } - } - - - public String getComment() throws PgpKeyNotFoundException { - try { - Object data = mKeyRepository.getGenericData(mUri, - KeyRings.COMMENT, - KeyRepository.FIELD_TYPE_STRING); - return (String) data; - } catch(KeyWritableRepository.NotFoundException e) { - throw new PgpKeyNotFoundException(e); - } - } - - @Override - public boolean isRevoked() throws PgpKeyNotFoundException { - try { - Object data = mKeyRepository.getGenericData(mUri, - KeychainContract.KeyRings.IS_REVOKED, - KeyRepository.FIELD_TYPE_INTEGER); - return (Long) data > 0; - } catch(KeyWritableRepository.NotFoundException e) { - throw new PgpKeyNotFoundException(e); - } - } - - @Override - public boolean canCertify() throws PgpKeyNotFoundException { - try { - Object data = mKeyRepository.getGenericData(mUri, - KeychainContract.KeyRings.HAS_CERTIFY_SECRET, - KeyRepository.FIELD_TYPE_NULL); - return !((Boolean) data); - } catch(KeyWritableRepository.NotFoundException e) { - throw new PgpKeyNotFoundException(e); - } - } - - @Override - public long getEncryptId() throws PgpKeyNotFoundException { - try { - Object data = mKeyRepository.getGenericData(mUri, - KeyRings.HAS_ENCRYPT, - KeyRepository.FIELD_TYPE_INTEGER); - return (Long) data; - } catch(KeyWritableRepository.NotFoundException e) { - throw new PgpKeyNotFoundException(e); - } - } - - @Override - public boolean hasEncrypt() throws PgpKeyNotFoundException { - return getEncryptId() != 0; - } - - /** Returns the key id which should be used for signing. - * - * This method returns keys which are actually available (ie. secret available, and not stripped, - * revoked, or expired), hence only works on keyrings where a secret key is available! - * - */ - public long getSecretSignId() throws PgpKeyNotFoundException { - try { - Object data = mKeyRepository.getGenericData(mUri, - KeyRings.HAS_SIGN_SECRET, - KeyRepository.FIELD_TYPE_INTEGER); - return (Long) data; - } catch(KeyWritableRepository.NotFoundException e) { - throw new PgpKeyNotFoundException(e); - } - } - - /** Returns the key id which should be used for authentication. - * - * This method returns keys which are actually available (ie. secret available, and not stripped, - * revoked, or expired), hence only works on keyrings where a secret key is available! - * - */ - public long getSecretAuthenticationId() throws PgpKeyNotFoundException { - try { - Object data = mKeyRepository.getGenericData(mUri, - KeyRings.HAS_AUTHENTICATE_SECRET, - KeyRepository.FIELD_TYPE_INTEGER); - return (Long) data; - } catch(KeyWritableRepository.NotFoundException e) { - throw new PgpKeyNotFoundException(e); - } - } - - public boolean hasSecretAuthentication() throws PgpKeyNotFoundException { - return getSecretAuthenticationId() != 0; - } - - public long getAuthenticationId() throws PgpKeyNotFoundException { - try { - Object data = mKeyRepository.getGenericData(mUri, - KeyRings.HAS_AUTHENTICATE, - KeyRepository.FIELD_TYPE_INTEGER); - return (Long) data; - } catch(KeyWritableRepository.NotFoundException e) { - throw new PgpKeyNotFoundException(e); - } - } - - public boolean hasAuthentication() throws PgpKeyNotFoundException { - return getAuthenticationId() != 0; - } - - @Override - public int getVerified() throws PgpKeyNotFoundException { - try { - Object data = mKeyRepository.getGenericData(mUri, - KeychainContract.KeyRings.VERIFIED, - KeyRepository.FIELD_TYPE_INTEGER); - return ((Long) data).intValue(); - } catch(KeyWritableRepository.NotFoundException e) { - throw new PgpKeyNotFoundException(e); - } - } - - public boolean hasAnySecret() throws PgpKeyNotFoundException { - try { - Object data = mKeyRepository.getGenericData(mUri, - KeychainContract.KeyRings.HAS_ANY_SECRET, - KeyRepository.FIELD_TYPE_INTEGER); - return (Long) data > 0; - } catch(KeyWritableRepository.NotFoundException e) { - throw new PgpKeyNotFoundException(e); - } - } - - public SecretKeyType getSecretKeyType(long keyId) throws NotFoundException { - Object data = mKeyRepository.getGenericData(Keys.buildKeysUri(mUri), - KeyRings.HAS_SECRET, - KeyRepository.FIELD_TYPE_INTEGER, - KeyRings.KEY_ID + " = " + Long.toString(keyId)); - return SecretKeyType.fromNum(((Long) data).intValue()); - } - - public byte[] getEncoded() throws PgpKeyNotFoundException { - try { - return mKeyRepository.loadPublicKeyRingData(getMasterKeyId()); - } catch(KeyWritableRepository.NotFoundException e) { - throw new PgpKeyNotFoundException(e); - } - } -} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/DatabaseNotifyManager.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/DatabaseNotifyManager.java deleted file mode 100644 index b283dd576..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/DatabaseNotifyManager.java +++ /dev/null @@ -1,42 +0,0 @@ -package org.sufficientlysecure.keychain.provider; - - -import android.content.ContentResolver; -import android.content.Context; -import android.net.Uri; - -import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; - - -public class DatabaseNotifyManager { - private ContentResolver contentResolver; - - public static DatabaseNotifyManager create(Context context) { - ContentResolver contentResolver = context.getContentResolver(); - return new DatabaseNotifyManager(contentResolver); - } - - private DatabaseNotifyManager(ContentResolver contentResolver) { - this.contentResolver = contentResolver; - } - - public void notifyKeyChange(long masterKeyId) { - Uri uri = KeyRings.buildGenericKeyRingUri(masterKeyId); - contentResolver.notifyChange(uri, null); - } - - public void notifyAutocryptDelete(String autocryptId, Long masterKeyId) { - Uri uri = KeyRings.buildGenericKeyRingUri(masterKeyId); - contentResolver.notifyChange(uri, null); - } - - public void notifyAutocryptUpdate(String autocryptId, long masterKeyId) { - Uri uri = KeyRings.buildGenericKeyRingUri(masterKeyId); - contentResolver.notifyChange(uri, null); - } - - public void notifyKeyserverStatusChange(long masterKeyId) { - Uri uri = KeyRings.buildGenericKeyRingUri(masterKeyId); - contentResolver.notifyChange(uri, null); - } -} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeyRepository.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeyRepository.java deleted file mode 100644 index 2f1847a17..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeyRepository.java +++ /dev/null @@ -1,346 +0,0 @@ -/* - * Copyright (C) 2017 Schürmann & Breitmoser GbR - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sufficientlysecure.keychain.provider; - - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.util.ArrayList; -import java.util.HashMap; - -import android.content.ContentResolver; -import android.content.Context; -import android.database.Cursor; -import android.net.Uri; -import android.support.annotation.Nullable; - -import org.bouncycastle.bcpg.ArmoredOutputStream; -import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType; -import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; -import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKeyRing; -import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKeyRing; -import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; -import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; -import org.sufficientlysecure.keychain.provider.KeychainContract.Certs; -import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRingData; -import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; -import org.sufficientlysecure.keychain.provider.KeychainContract.UpdatedKeys; -import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets; -import timber.log.Timber; - - -public class KeyRepository { - // If we ever switch to api level 11, we can ditch this whole mess! - public static final int FIELD_TYPE_NULL = 1; - // this is called integer to stay coherent with the constants in Cursor (api level 11) - public static final int FIELD_TYPE_INTEGER = 2; - public static final int FIELD_TYPE_FLOAT = 3; - public static final int FIELD_TYPE_STRING = 4; - public static final int FIELD_TYPE_BLOB = 5; - - final ContentResolver contentResolver; - final LocalPublicKeyStorage mLocalPublicKeyStorage; - OperationLog mLog; - int mIndent; - - public static KeyRepository create(Context context) { - ContentResolver contentResolver = context.getContentResolver(); - LocalPublicKeyStorage localPublicKeyStorage = LocalPublicKeyStorage.getInstance(context); - - return new KeyRepository(contentResolver, localPublicKeyStorage); - } - - private KeyRepository(ContentResolver contentResolver, LocalPublicKeyStorage localPublicKeyStorage) { - this(contentResolver, localPublicKeyStorage, new OperationLog(), 0); - } - - KeyRepository(ContentResolver contentResolver, LocalPublicKeyStorage localPublicKeyStorage, - OperationLog log, int indent) { - this.contentResolver = contentResolver; - mLocalPublicKeyStorage = localPublicKeyStorage; - mIndent = indent; - mLog = log; - } - - public OperationLog getLog() { - return mLog; - } - - public void log(LogType type) { - if (mLog != null) { - mLog.add(type, mIndent); - } - } - - public void log(LogType type, Object... parameters) { - if (mLog != null) { - mLog.add(type, mIndent, parameters); - } - } - - public void clearLog() { - mLog = new OperationLog(); - } - - Object getGenericData(Uri uri, String column, int type) throws NotFoundException { - Object result = getGenericData(uri, new String[]{column}, new int[]{type}, null).get(column); - if (result == null) { - throw new NotFoundException(); - } - return result; - } - - Object getGenericDataOrNull(Uri uri, String column, int type) throws NotFoundException { - return getGenericData(uri, new String[]{column}, new int[]{type}, null).get(column); - } - - Object getGenericData(Uri uri, String column, int type, String selection) - throws NotFoundException { - return getGenericData(uri, new String[]{column}, new int[]{type}, selection).get(column); - } - - private HashMap getGenericData(Uri uri, String[] proj, int[] types) - throws NotFoundException { - return getGenericData(uri, proj, types, null); - } - - private HashMap getGenericData(Uri uri, String[] proj, int[] types, String selection) - throws NotFoundException { - Cursor cursor = contentResolver.query(uri, proj, selection, null, null); - - try { - HashMap result = new HashMap<>(proj.length); - if (cursor != null && cursor.moveToFirst()) { - int pos = 0; - for (String p : proj) { - switch (types[pos]) { - case FIELD_TYPE_NULL: - result.put(p, cursor.isNull(pos)); - break; - case FIELD_TYPE_INTEGER: - result.put(p, cursor.getLong(pos)); - break; - case FIELD_TYPE_FLOAT: - result.put(p, cursor.getFloat(pos)); - break; - case FIELD_TYPE_STRING: - result.put(p, cursor.getString(pos)); - break; - case FIELD_TYPE_BLOB: - result.put(p, cursor.getBlob(pos)); - break; - } - pos += 1; - } - } else { - // If no data was found, throw an appropriate exception - throw new NotFoundException(); - } - - return result; - } finally { - if (cursor != null) { - cursor.close(); - } - } - } - - public HashMap getUnifiedData(long masterKeyId, String[] proj, int[] types) - throws NotFoundException { - return getGenericData(KeyRings.buildUnifiedKeyRingUri(masterKeyId), proj, types); - } - - public long getMasterKeyId(long subKeyId) throws NotFoundException { - return (Long) getGenericData(KeyRings.buildUnifiedKeyRingsFindBySubkeyUri(subKeyId), - KeyRings.MASTER_KEY_ID, FIELD_TYPE_INTEGER); - } - - public CachedPublicKeyRing getCachedPublicKeyRing(Uri queryUri) throws PgpKeyNotFoundException { - long masterKeyId = new CachedPublicKeyRing(this, queryUri).extractOrGetMasterKeyId(); - return getCachedPublicKeyRing(masterKeyId); - } - - public CachedPublicKeyRing getCachedPublicKeyRing(long id) { - return new CachedPublicKeyRing(this, KeyRings.buildUnifiedKeyRingUri(id)); - } - - public CanonicalizedPublicKeyRing getCanonicalizedPublicKeyRing(long id) throws NotFoundException { - return getCanonicalizedPublicKeyRing(KeyRings.buildUnifiedKeyRingUri(id)); - } - - public CanonicalizedPublicKeyRing getCanonicalizedPublicKeyRing(Uri queryUri) throws NotFoundException { - Cursor cursor = contentResolver.query(queryUri, - new String[] { KeyRings.MASTER_KEY_ID, KeyRings.VERIFIED }, null, null, null); - try { - if (cursor != null && cursor.moveToFirst()) { - long masterKeyId = cursor.getLong(0); - int verified = cursor.getInt(1); - - byte[] publicKeyData = loadPublicKeyRingData(masterKeyId); - return new CanonicalizedPublicKeyRing(publicKeyData, verified); - } else { - throw new NotFoundException("Key not found!"); - } - } finally { - if (cursor != null) { - cursor.close(); - } - } - } - - public CanonicalizedSecretKeyRing getCanonicalizedSecretKeyRing(long id) throws NotFoundException { - return getCanonicalizedSecretKeyRing(KeyRings.buildUnifiedKeyRingUri(id)); - } - - public CanonicalizedSecretKeyRing getCanonicalizedSecretKeyRing(Uri queryUri) throws NotFoundException { - Cursor cursor = contentResolver.query(queryUri, - new String[] { KeyRings.MASTER_KEY_ID, KeyRings.VERIFIED, KeyRings.HAS_ANY_SECRET }, null, null, null); - try { - if (cursor != null && cursor.moveToFirst()) { - long masterKeyId = cursor.getLong(0); - int verified = cursor.getInt(1); - int hasAnySecret = cursor.getInt(2); - if (hasAnySecret == 0) { - throw new NotFoundException("No secret key available or unknown public key!"); - } - - byte[] secretKeyData = loadSecretKeyRingData(masterKeyId); - return new CanonicalizedSecretKeyRing(secretKeyData, verified); - } else { - throw new NotFoundException("Key not found!"); - } - } finally { - if (cursor != null) { - cursor.close(); - } - } - } - - public ArrayList getConfirmedUserIds(long masterKeyId) throws NotFoundException { - Cursor cursor = contentResolver.query(UserPackets.buildUserIdsUri(masterKeyId), - new String[]{UserPackets.USER_ID}, UserPackets.VERIFIED + " = " + Certs.VERIFIED_SECRET, null, null - ); - if (cursor == null) { - throw new NotFoundException("Key id for requested user ids not found"); - } - - try { - ArrayList userIds = new ArrayList<>(cursor.getCount()); - while (cursor.moveToNext()) { - String userId = cursor.getString(0); - userIds.add(userId); - } - - return userIds; - } finally { - cursor.close(); - } - } - - private byte[] getKeyRingAsArmoredData(byte[] data) throws IOException, PgpGeneralException { - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - ArmoredOutputStream aos = new ArmoredOutputStream(bos); - - aos.write(data); - aos.close(); - - return bos.toByteArray(); - } - - public String getPublicKeyRingAsArmoredString(long masterKeyId) - throws NotFoundException, IOException, PgpGeneralException { - byte[] data = loadPublicKeyRingData(masterKeyId); - byte[] armoredData = getKeyRingAsArmoredData(data); - return new String(armoredData); - } - - public byte[] getSecretKeyRingAsArmoredData(long masterKeyId) - throws NotFoundException, IOException, PgpGeneralException { - byte[] data = loadSecretKeyRingData(masterKeyId); - return getKeyRingAsArmoredData(data); - } - - public ContentResolver getContentResolver() { - return contentResolver; - } - - @Nullable - Long getLastUpdateTime(long masterKeyId) { - Cursor lastUpdatedCursor = contentResolver.query( - UpdatedKeys.CONTENT_URI, - new String[] { UpdatedKeys.LAST_UPDATED }, - UpdatedKeys.MASTER_KEY_ID + " = ?", - new String[] { "" + masterKeyId }, - null - ); - if (lastUpdatedCursor == null) { - return null; - } - - Long lastUpdateTime; - try { - if (!lastUpdatedCursor.moveToNext()) { - return null; - } - lastUpdateTime = lastUpdatedCursor.getLong(0); - } finally { - lastUpdatedCursor.close(); - } - return lastUpdateTime; - } - - public final byte[] loadPublicKeyRingData(long masterKeyId) throws NotFoundException { - byte[] data = (byte[]) getGenericDataOrNull(KeyRingData.buildPublicKeyRingUri(masterKeyId), - KeyRingData.KEY_RING_DATA, FIELD_TYPE_BLOB); - - if (data == null) { - try { - data = mLocalPublicKeyStorage.readPublicKey(masterKeyId); - } catch (IOException e) { - Timber.e(e, "Error reading public key from storage!"); - throw new NotFoundException(); - } - } - - if (data == null) { - throw new NotFoundException(); - } - - return data; - } - - public final byte[] loadSecretKeyRingData(long masterKeyId) throws NotFoundException { - byte[] data = (byte[]) getGenericDataOrNull(KeychainContract.KeyRingData.buildSecretKeyRingUri(masterKeyId), - KeyRingData.KEY_RING_DATA, FIELD_TYPE_BLOB); - - if (data == null) { - throw new NotFoundException(); - } - - return data; - } - - public static class NotFoundException extends Exception { - public NotFoundException() { - } - - public NotFoundException(String name) { - super(name); - } - } -} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainContract.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainContract.java index b97bb44cc..0602d0b70 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainContract.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainContract.java @@ -24,12 +24,12 @@ import org.sufficientlysecure.keychain.Constants; public class KeychainContract { - interface KeyRingsColumns { + public interface KeyRingsColumns { String MASTER_KEY_ID = "master_key_id"; // not a database id String KEY_RING_DATA = "key_ring_data"; // PGPPublicKeyRing / PGPSecretKeyRing blob } - interface KeysColumns { + public interface KeysColumns { String MASTER_KEY_ID = "master_key_id"; // not a database id String RANK = "rank"; @@ -51,18 +51,12 @@ public class KeychainContract { String EXPIRY = "expiry"; } - interface UpdatedKeysColumns { - String MASTER_KEY_ID = "master_key_id"; // not a database id - String LAST_UPDATED = "last_updated"; // time since epoch in seconds - String SEEN_ON_KEYSERVERS = "seen_on_keyservers"; - } - - interface KeySignaturesColumns { + public interface KeySignaturesColumns { String MASTER_KEY_ID = "master_key_id"; // not a database id String SIGNER_KEY_ID = "signer_key_id"; } - interface UserPacketsColumns { + public interface UserPacketsColumns { String MASTER_KEY_ID = "master_key_id"; // foreign key to key_rings._ID String TYPE = "type"; // not a database id String USER_ID = "user_id"; // not a database id @@ -75,7 +69,7 @@ public class KeychainContract { String IS_REVOKED = "is_revoked"; } - interface CertsColumns { + public interface CertsColumns { String MASTER_KEY_ID = "master_key_id"; String RANK = "rank"; String KEY_ID_CERTIFIER = "key_id_certifier"; @@ -85,34 +79,15 @@ public class KeychainContract { String DATA = "data"; } - interface ApiAppsColumns { - String PACKAGE_NAME = "package_name"; - String PACKAGE_CERTIFICATE = "package_signature"; - } - - interface ApiAppsAllowedKeysColumns { + public interface ApiAppsAllowedKeysColumns { String KEY_ID = "key_id"; // not a database id String PACKAGE_NAME = "package_name"; // foreign key to api_apps.package_name } - interface OverriddenWarnings { + public interface OverriddenWarnings { String IDENTIFIER = "identifier"; } - interface ApiAutocryptPeerColumns { - String PACKAGE_NAME = "package_name"; - String IDENTIFIER = "identifier"; - String LAST_SEEN = "last_seen"; - - String MASTER_KEY_ID = "master_key_id"; - String LAST_SEEN_KEY = "last_seen_key"; - String IS_MUTUAL = "is_mutual"; - - String GOSSIP_MASTER_KEY_ID = "gossip_master_key_id"; - String GOSSIP_LAST_SEEN_KEY = "gossip_last_seen_key"; - String GOSSIP_ORIGIN = "gossip_origin"; - } - public static final String CONTENT_AUTHORITY = Constants.PROVIDER_AUTHORITY; private static final Uri BASE_CONTENT_URI_INTERNAL = Uri @@ -120,181 +95,40 @@ public class KeychainContract { public static final String BASE_KEY_RINGS = "key_rings"; - public static final String BASE_UPDATED_KEYS = "updated_keys"; - public static final String BASE_KEY_SIGNATURES = "key_signatures"; - public static final String PATH_UNIFIED = "unified"; - - public static final String PATH_FIND = "find"; - public static final String PATH_BY_EMAIL = "email"; - public static final String PATH_BY_SUBKEY = "subkey"; - public static final String PATH_BY_USER_ID = "user_id"; - - public static final String PATH_FILTER = "filter"; - public static final String PATH_BY_SIGNER = "signer"; - public static final String PATH_PUBLIC = "public"; - public static final String PATH_SECRET = "secret"; public static final String PATH_USER_IDS = "user_ids"; - public static final String PATH_LINKED_IDS = "linked_ids"; public static final String PATH_KEYS = "keys"; public static final String PATH_CERTS = "certs"; - public static final String BASE_API_APPS = "api_apps"; - public static final String PATH_ALLOWED_KEYS = "allowed_keys"; - - public static final String PATH_BY_PACKAGE_NAME = "by_package_name"; - public static final String PATH_BY_KEY_ID = "by_key_id"; - - public static final String BASE_AUTOCRYPT_PEERS = "autocrypt_peers"; - public static class KeyRings implements BaseColumns, KeysColumns, UserPacketsColumns { - public static final String MASTER_KEY_ID = KeysColumns.MASTER_KEY_ID; - public static final String IS_REVOKED = KeysColumns.IS_REVOKED; - public static final String IS_SECURE = KeysColumns.IS_SECURE; - public static final String VERIFIED = CertsColumns.VERIFIED; - public static final String IS_EXPIRED = "is_expired"; - public static final String HAS_ANY_SECRET = "has_any_secret"; - public static final String HAS_ENCRYPT = "has_encrypt"; - public static final String HAS_SIGN_SECRET = "has_sign_secret"; - public static final String HAS_CERTIFY_SECRET = "has_certify_secret"; - public static final String HAS_AUTHENTICATE = "has_authenticate"; - public static final String HAS_AUTHENTICATE_SECRET = "has_authenticate_secret"; - public static final String HAS_DUPLICATE_USER_ID = "has_duplicate_user_id"; - public static final String API_KNOWN_TO_PACKAGE_NAMES = "known_to_apps"; - public static final Uri CONTENT_URI = BASE_CONTENT_URI_INTERNAL.buildUpon() .appendPath(BASE_KEY_RINGS).build(); - - public static final String CONTENT_TYPE - = "vnd.android.cursor.dir/vnd.org.sufficientlysecure.keychain.provider.key_rings"; - public static final String CONTENT_ITEM_TYPE - = "vnd.android.cursor.item/vnd.org.sufficientlysecure.keychain.provider.key_rings"; - - public static Uri buildUnifiedKeyRingsUri() { - return CONTENT_URI.buildUpon().appendPath(PATH_UNIFIED).build(); - } - - public static Uri buildGenericKeyRingUri(long masterKeyId) { - return CONTENT_URI.buildUpon().appendPath(Long.toString(masterKeyId)).build(); - } - - public static Uri buildGenericKeyRingUri(String masterKeyId) { - return CONTENT_URI.buildUpon().appendPath(masterKeyId).build(); - } - - public static Uri buildGenericKeyRingUri(Uri uri) { - return CONTENT_URI.buildUpon().appendPath(uri.getPathSegments().get(1)).build(); - } - - public static Uri buildUnifiedKeyRingUri(long masterKeyId) { - return CONTENT_URI.buildUpon().appendPath(Long.toString(masterKeyId)) - .appendPath(PATH_UNIFIED).build(); - } - - public static Uri buildUnifiedKeyRingUri(Uri uri) { - return CONTENT_URI.buildUpon().appendPath(uri.getPathSegments().get(1)) - .appendPath(PATH_UNIFIED).build(); - } - - public static Uri buildUnifiedKeyRingsFindByEmailUri(String email) { - return CONTENT_URI.buildUpon().appendPath(PATH_FIND) - .appendPath(PATH_BY_EMAIL).appendPath(email).build(); - } - - public static Uri buildUnifiedKeyRingsFindByUserIdUri(String query) { - return CONTENT_URI.buildUpon().appendPath(PATH_FIND) - .appendPath(PATH_BY_USER_ID).appendPath(query).build(); - } - - public static Uri buildUnifiedKeyRingsFindBySubkeyUri(long subkey) { - return CONTENT_URI.buildUpon().appendPath(PATH_FIND) - .appendPath(PATH_BY_SUBKEY).appendPath(Long.toString(subkey)).build(); - } - - public static Uri buildUnifiedKeyRingsFilterBySigner() { - return CONTENT_URI.buildUpon().appendPath(PATH_FILTER).appendPath(PATH_BY_SIGNER).build(); - } } public static class KeyRingData implements KeyRingsColumns, BaseColumns { public static final Uri CONTENT_URI = BASE_CONTENT_URI_INTERNAL.buildUpon() .appendPath(BASE_KEY_RINGS).build(); - public static final String CONTENT_TYPE - = "vnd.android.cursor.dir/vnd.org.sufficientlysecure.keychain.provider.key_ring_data"; - public static final String CONTENT_ITEM_TYPE - = "vnd.android.cursor.item/vnd.org.sufficientlysecure.keychain.provider.key_ring_data"; - - public static Uri buildPublicKeyRingUri() { - return CONTENT_URI.buildUpon().appendPath(PATH_PUBLIC).build(); - } - public static Uri buildPublicKeyRingUri(long masterKeyId) { return CONTENT_URI.buildUpon().appendPath(Long.toString(masterKeyId)).appendPath(PATH_PUBLIC).build(); } - - public static Uri buildPublicKeyRingUri(Uri uri) { - return CONTENT_URI.buildUpon().appendPath(uri.getPathSegments().get(1)).appendPath(PATH_PUBLIC).build(); - } - - public static Uri buildSecretKeyRingUri() { - return CONTENT_URI.buildUpon().appendPath(PATH_SECRET).build(); - } - - public static Uri buildSecretKeyRingUri(long masterKeyId) { - return CONTENT_URI.buildUpon().appendPath(Long.toString(masterKeyId)).appendPath(PATH_SECRET).build(); - } - - public static Uri buildSecretKeyRingUri(Uri uri) { - return CONTENT_URI.buildUpon().appendPath(uri.getPathSegments().get(1)).appendPath(PATH_SECRET).build(); - } - } public static class Keys implements KeysColumns, BaseColumns { public static final Uri CONTENT_URI = BASE_CONTENT_URI_INTERNAL.buildUpon() .appendPath(BASE_KEY_RINGS).build(); - /** - * Use if multiple items get returned - */ - public static final String CONTENT_TYPE - = "vnd.android.cursor.dir/vnd.org.sufficientlysecure.keychain.provider.keychain.keys"; - - /** - * Use if a single item is returned - */ - public static final String CONTENT_ITEM_TYPE - = "vnd.android.cursor.item/vnd.org.sufficientlysecure.keychain.provider.keychain.keys"; - public static Uri buildKeysUri(long masterKeyId) { return CONTENT_URI.buildUpon().appendPath(Long.toString(masterKeyId)).appendPath(PATH_KEYS).build(); } - public static Uri buildKeysUri(Uri uri) { - return CONTENT_URI.buildUpon().appendPath(uri.getPathSegments().get(1)).appendPath(PATH_KEYS).build(); - } - - } - - public static class UpdatedKeys implements UpdatedKeysColumns, BaseColumns { - public static final Uri CONTENT_URI = BASE_CONTENT_URI_INTERNAL.buildUpon() - .appendPath(BASE_UPDATED_KEYS).build(); - - public static final String CONTENT_TYPE - = "vnd.android.cursor.dir/vnd.org.sufficientlysecure.keychain.provider.updated_keys"; - public static final String CONTENT_ITEM_TYPE - = "vnd.android.cursor.item/vnd.org.sufficientlysecure.keychain.provider.updated_keys"; } public static class KeySignatures implements KeySignaturesColumns, BaseColumns { public static final Uri CONTENT_URI = BASE_CONTENT_URI_INTERNAL.buildUpon() .appendPath(BASE_KEY_SIGNATURES).build(); - - public static final String CONTENT_TYPE - = "vnd.android.cursor.dir/vnd.org.sufficientlysecure.keychain.provider.key_signatures"; } public static class UserPackets implements UserPacketsColumns, BaseColumns { @@ -302,116 +136,12 @@ public class KeychainContract { public static final Uri CONTENT_URI = BASE_CONTENT_URI_INTERNAL.buildUpon() .appendPath(BASE_KEY_RINGS).build(); - /** - * Use if multiple items get returned - */ - public static final String CONTENT_TYPE - = "vnd.android.cursor.dir/vnd.org.sufficientlysecure.keychain.provider.user_ids"; - - /** - * Use if a single item is returned - */ - public static final String CONTENT_ITEM_TYPE - = "vnd.android.cursor.item/vnd.org.sufficientlysecure.keychain.provider.user_ids"; - - public static Uri buildUserIdsUri() { - return CONTENT_URI.buildUpon().appendPath(PATH_USER_IDS).build(); - } - public static Uri buildUserIdsUri(long masterKeyId) { return CONTENT_URI.buildUpon().appendPath(Long.toString(masterKeyId)).appendPath(PATH_USER_IDS).build(); } - - public static Uri buildUserIdsUri(Uri uri) { - return CONTENT_URI.buildUpon().appendPath(uri.getPathSegments().get(1)).appendPath(PATH_USER_IDS).build(); - } - - public static Uri buildLinkedIdsUri(long masterKeyId) { - return CONTENT_URI.buildUpon().appendPath(Long.toString(masterKeyId)).appendPath(PATH_LINKED_IDS).build(); - } - - public static Uri buildLinkedIdsUri(Uri uri) { - return CONTENT_URI.buildUpon().appendPath(uri.getPathSegments().get(1)).appendPath(PATH_LINKED_IDS).build(); - } - - } - - public static class ApiApps implements ApiAppsColumns, BaseColumns { - public static final Uri CONTENT_URI = BASE_CONTENT_URI_INTERNAL.buildUpon() - .appendPath(BASE_API_APPS).build(); - - /** - * Use if multiple items get returned - */ - public static final String CONTENT_TYPE - = "vnd.android.cursor.dir/vnd.org.sufficientlysecure.keychain.provider.api_apps"; - - /** - * Use if a single item is returned - */ - public static final String CONTENT_ITEM_TYPE - = "vnd.android.cursor.item/vnd.org.sufficientlysecure.keychain.provider.api_apps"; - - public static Uri buildByPackageNameUri(String packageName) { - return CONTENT_URI.buildUpon().appendEncodedPath(packageName).build(); - } - } - - public static class ApiAllowedKeys implements ApiAppsAllowedKeysColumns, BaseColumns { - public static final Uri CONTENT_URI = BASE_CONTENT_URI_INTERNAL.buildUpon() - .appendPath(BASE_API_APPS).build(); - - /** - * Use if multiple items get returned - */ - public static final String CONTENT_TYPE - = "vnd.android.cursor.dir/vnd.org.sufficientlysecure.keychain.provider.api_apps.allowed_keys"; - - public static Uri buildBaseUri(String packageName) { - return CONTENT_URI.buildUpon().appendEncodedPath(packageName).appendPath(PATH_ALLOWED_KEYS) - .build(); - } - } - - public static class ApiAutocryptPeer implements ApiAutocryptPeerColumns, BaseColumns { - public static final Uri CONTENT_URI = BASE_CONTENT_URI_INTERNAL.buildUpon() - .appendPath(BASE_AUTOCRYPT_PEERS).build(); - public static final String KEY_IS_REVOKED = "key_is_revoked"; - public static final String KEY_IS_EXPIRED = "key_is_expired"; - public static final String KEY_IS_VERIFIED = "key_is_verified"; - public static final String GOSSIP_KEY_IS_REVOKED = "gossip_key_is_revoked"; - public static final String GOSSIP_KEY_IS_EXPIRED = "gossip_key_is_expired"; - public static final String GOSSIP_KEY_IS_VERIFIED = "gossip_key_is_verified"; - - public static final int GOSSIP_ORIGIN_AUTOCRYPT = 0; - public static final int GOSSIP_ORIGIN_SIGNATURE = 10; - public static final int GOSSIP_ORIGIN_DEDUP = 20; - - public static Uri buildByKeyUri(Uri uri) { - return CONTENT_URI.buildUpon().appendPath(PATH_BY_KEY_ID).appendPath(uri.getPathSegments().get(1)).build(); - } - - public static Uri buildByPackageName(String packageName) { - return CONTENT_URI.buildUpon().appendPath(PATH_BY_PACKAGE_NAME).appendPath(packageName).build(); - } - - public static Uri buildByPackageNameAndAutocryptId(String packageName, String autocryptPeer) { - return CONTENT_URI.buildUpon().appendPath(PATH_BY_PACKAGE_NAME).appendPath(packageName).appendPath(autocryptPeer).build(); - } - - public static Uri buildByMasterKeyId(long masterKeyId) { - return CONTENT_URI.buildUpon().appendPath(PATH_BY_KEY_ID).appendPath(Long.toString(masterKeyId)).build(); - } } public static class Certs implements CertsColumns, BaseColumns { - public static final String USER_ID = UserPacketsColumns.USER_ID; - public static final String NAME = UserPacketsColumns.NAME; - public static final String EMAIL = UserPacketsColumns.EMAIL; - public static final String COMMENT = UserPacketsColumns.COMMENT; - public static final String SIGNER_UID = "signer_user_id"; - - public static final int UNVERIFIED = 0; public static final int VERIFIED_SECRET = 1; public static final int VERIFIED_SELF = 2; @@ -421,24 +151,6 @@ public class KeychainContract { public static Uri buildCertsUri(long masterKeyId) { return CONTENT_URI.buildUpon().appendPath(Long.toString(masterKeyId)).appendPath(PATH_CERTS).build(); } - - public static Uri buildCertsSpecificUri(long masterKeyId, long rank, long certifier) { - return CONTENT_URI.buildUpon().appendPath(Long.toString(masterKeyId)) - .appendPath(PATH_CERTS).appendPath(Long.toString(rank)) - .appendPath(Long.toString(certifier)).build(); - } - - public static Uri buildCertsUri(Uri uri) { - return CONTENT_URI.buildUpon().appendPath(uri.getPathSegments().get(1)) - .appendPath(PATH_CERTS).build(); - } - - public static Uri buildLinkedIdCertsUri(Uri uri, int rank) { - return CONTENT_URI.buildUpon().appendPath(uri.getPathSegments().get(1)) - .appendPath(PATH_LINKED_IDS).appendPath(Integer.toString(rank)) - .appendPath(PATH_CERTS).build(); - } - } private KeychainContract() { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainExternalContract.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainExternalContract.java index e19332d92..4802b667e 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainExternalContract.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainExternalContract.java @@ -22,7 +22,6 @@ import android.net.Uri; import android.provider.BaseColumns; import org.sufficientlysecure.keychain.Constants; -import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAutocryptPeer; public class KeychainExternalContract { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java index d581de0f5..6efac8a72 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java @@ -18,76 +18,33 @@ package org.sufficientlysecure.keychain.provider; -import java.util.Arrays; -import java.util.Date; -import java.util.HashMap; -import java.util.List; - +import android.arch.persistence.db.SupportSQLiteDatabase; import android.content.ContentProvider; -import android.content.ContentResolver; import android.content.ContentValues; import android.content.UriMatcher; import android.database.Cursor; -import android.database.DatabaseUtils; import android.database.sqlite.SQLiteConstraintException; import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteQueryBuilder; import android.net.Uri; import android.support.annotation.NonNull; import android.text.TextUtils; -import org.sufficientlysecure.keychain.Constants; -import org.sufficientlysecure.keychain.pgp.WrappedUserAttribute; -import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAllowedKeys; -import org.sufficientlysecure.keychain.provider.KeychainContract.ApiApps; -import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAutocryptPeer; +import org.sufficientlysecure.keychain.KeychainDatabase; +import org.sufficientlysecure.keychain.daos.DatabaseNotifyManager; import org.sufficientlysecure.keychain.provider.KeychainContract.Certs; -import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRingData; -import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; import org.sufficientlysecure.keychain.provider.KeychainContract.KeySignatures; import org.sufficientlysecure.keychain.provider.KeychainContract.Keys; -import org.sufficientlysecure.keychain.provider.KeychainContract.UpdatedKeys; import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets; import org.sufficientlysecure.keychain.provider.KeychainContract.UserPacketsColumns; -import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables; -import org.sufficientlysecure.keychain.util.DatabaseUtil; +import org.sufficientlysecure.keychain.KeychainDatabase.Tables; import timber.log.Timber; -import static android.database.DatabaseUtils.dumpCursorToString; - -public class KeychainProvider extends ContentProvider implements SimpleContentResolverInterface { - - private static final int KEY_RINGS_UNIFIED = 101; - private static final int KEY_RINGS_PUBLIC = 102; - private static final int KEY_RINGS_SECRET = 103; - private static final int KEY_RINGS_USER_IDS = 104; - - private static final int KEY_RING_UNIFIED = 200; +public class KeychainProvider extends ContentProvider { private static final int KEY_RING_KEYS = 201; private static final int KEY_RING_USER_IDS = 202; private static final int KEY_RING_PUBLIC = 203; - private static final int KEY_RING_SECRET = 204; private static final int KEY_RING_CERTS = 205; - private static final int KEY_RING_CERTS_SPECIFIC = 206; - private static final int KEY_RING_LINKED_IDS = 207; - private static final int KEY_RING_LINKED_ID_CERTS = 208; - - private static final int API_APPS = 301; - private static final int API_APPS_BY_PACKAGE_NAME = 302; - private static final int API_ALLOWED_KEYS = 305; - - private static final int KEY_RINGS_FIND_BY_EMAIL = 400; - private static final int KEY_RINGS_FIND_BY_SUBKEY = 401; - private static final int KEY_RINGS_FIND_BY_USER_ID = 402; - private static final int KEY_RINGS_FILTER_BY_SIGNER = 403; - - private static final int UPDATED_KEYS = 500; - private static final int UPDATED_KEYS_SPECIFIC = 501; - - private static final int AUTOCRYPT_PEERS_BY_MASTER_KEY_ID = 601; - private static final int AUTOCRYPT_PEERS_BY_PACKAGE_NAME = 602; - private static final int AUTOCRYPT_PEERS_BY_PACKAGE_NAME_AND_TRUST_ID = 603; private static final int KEY_SIGNATURES = 700; @@ -102,132 +59,28 @@ public class KeychainProvider extends ContentProvider implements SimpleContentRe String authority = KeychainContract.CONTENT_AUTHORITY; - /* - * list key_rings - * - *

-         * key_rings/unified
-         * key_rings/public
-         * key_rings/secret
-         * key_rings/user_ids
-         * 
- */ - matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS - + "/" + KeychainContract.PATH_UNIFIED, - KEY_RINGS_UNIFIED); - matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS - + "/" + KeychainContract.PATH_PUBLIC, - KEY_RINGS_PUBLIC); - matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS - + "/" + KeychainContract.PATH_SECRET, - KEY_RINGS_SECRET); - matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS - + "/" + KeychainContract.PATH_USER_IDS, - KEY_RINGS_USER_IDS); - - /* - * find by criteria other than master key id - * - * key_rings/find/email/_ - * key_rings/find/subkey/_ - * - */ - matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/" - + KeychainContract.PATH_FIND + "/" + KeychainContract.PATH_BY_EMAIL + "/*", - KEY_RINGS_FIND_BY_EMAIL); - matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/" - + KeychainContract.PATH_FIND + "/" + KeychainContract.PATH_BY_SUBKEY + "/*", - KEY_RINGS_FIND_BY_SUBKEY); - matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/" - + KeychainContract.PATH_FIND + "/" + KeychainContract.PATH_BY_USER_ID + "/*", - KEY_RINGS_FIND_BY_USER_ID); - matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/" - + KeychainContract.PATH_FILTER + "/" + KeychainContract.PATH_BY_SIGNER, - KEY_RINGS_FILTER_BY_SIGNER); - /* * list key_ring specifics * *
-         * key_rings/_/unified
          * key_rings/_/keys
          * key_rings/_/user_ids
-         * key_rings/_/linked_ids
-         * key_rings/_/linked_ids/_
-         * key_rings/_/linked_ids/_/certs
          * key_rings/_/public
-         * key_rings/_/secret
          * key_rings/_/certs
-         * key_rings/_/certs/_/_
          * 
*/ - matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/*/" - + KeychainContract.PATH_UNIFIED, - KEY_RING_UNIFIED); matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/*/" + KeychainContract.PATH_KEYS, KEY_RING_KEYS); matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/*/" + KeychainContract.PATH_USER_IDS, KEY_RING_USER_IDS); - matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/*/" - + KeychainContract.PATH_LINKED_IDS, - KEY_RING_LINKED_IDS); - matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/*/" - + KeychainContract.PATH_LINKED_IDS + "/*/" - + KeychainContract.PATH_CERTS, - KEY_RING_LINKED_ID_CERTS); matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/*/" + KeychainContract.PATH_PUBLIC, KEY_RING_PUBLIC); - matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/*/" - + KeychainContract.PATH_SECRET, - KEY_RING_SECRET); matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/*/" + KeychainContract.PATH_CERTS, KEY_RING_CERTS); - matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/*/" - + KeychainContract.PATH_CERTS + "/*/*", - KEY_RING_CERTS_SPECIFIC); - - /* - * API apps - * - *
-         * api_apps
-         * api_apps/_ (package name)
-         *
-         * api_apps/_/allowed_keys
-         * 
- */ - matcher.addURI(authority, KeychainContract.BASE_API_APPS, API_APPS); - matcher.addURI(authority, KeychainContract.BASE_API_APPS + "/*", API_APPS_BY_PACKAGE_NAME); - - matcher.addURI(authority, KeychainContract.BASE_API_APPS + "/*/" - + KeychainContract.PATH_ALLOWED_KEYS, API_ALLOWED_KEYS); - - /* - * Trust Identity access - * - *
-         * trust_ids/by_key_id/_
-         *
-         * 
- */ - matcher.addURI(authority, KeychainContract.BASE_AUTOCRYPT_PEERS + "/" + - KeychainContract.PATH_BY_KEY_ID + "/*", AUTOCRYPT_PEERS_BY_MASTER_KEY_ID); - matcher.addURI(authority, KeychainContract.BASE_AUTOCRYPT_PEERS + "/" + - KeychainContract.PATH_BY_PACKAGE_NAME + "/*", AUTOCRYPT_PEERS_BY_PACKAGE_NAME); - matcher.addURI(authority, KeychainContract.BASE_AUTOCRYPT_PEERS + "/" + - KeychainContract.PATH_BY_PACKAGE_NAME + "/*/*", AUTOCRYPT_PEERS_BY_PACKAGE_NAME_AND_TRUST_ID); - - - /* - * to access table containing last updated dates of keys - */ - matcher.addURI(authority, KeychainContract.BASE_UPDATED_KEYS, UPDATED_KEYS); - matcher.addURI(authority, KeychainContract.BASE_UPDATED_KEYS + "/*", UPDATED_KEYS_SPECIFIC); - matcher.addURI(authority, KeychainContract.BASE_KEY_SIGNATURES, KEY_SIGNATURES); @@ -237,9 +90,6 @@ public class KeychainProvider extends ContentProvider implements SimpleContentRe private KeychainDatabase mKeychainDatabase; - /** - * {@inheritDoc} - */ @Override public boolean onCreate() { mUriMatcher = buildUriMatcher(); @@ -247,623 +97,31 @@ public class KeychainProvider extends ContentProvider implements SimpleContentRe } public KeychainDatabase getDb() { - if(mKeychainDatabase == null) - mKeychainDatabase = new KeychainDatabase(getContext()); + if(mKeychainDatabase == null) { + mKeychainDatabase = KeychainDatabase.getInstance(getContext()); + } return mKeychainDatabase; } - /** - * {@inheritDoc} - */ @Override public String getType(@NonNull Uri uri) { - final int match = mUriMatcher.match(uri); - switch (match) { - case KEY_RING_PUBLIC: - return KeyRings.CONTENT_ITEM_TYPE; + throw new UnsupportedOperationException(); + } - case KEY_RING_KEYS: - return Keys.CONTENT_TYPE; - - case KEY_RING_USER_IDS: - return UserPackets.CONTENT_TYPE; - - case KEY_RING_SECRET: - return KeyRings.CONTENT_ITEM_TYPE; - - case UPDATED_KEYS: - return UpdatedKeys.CONTENT_TYPE; - - case UPDATED_KEYS_SPECIFIC: - return UpdatedKeys.CONTENT_ITEM_TYPE; - - case KEY_SIGNATURES: - return KeySignatures.CONTENT_TYPE; - - case API_APPS: - return ApiApps.CONTENT_TYPE; - - case API_APPS_BY_PACKAGE_NAME: - return ApiApps.CONTENT_ITEM_TYPE; - - case API_ALLOWED_KEYS: - return ApiAllowedKeys.CONTENT_TYPE; - - default: - throw new UnsupportedOperationException("Unknown uri: " + uri); - } + @Override + public Cursor query( + @NonNull Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { + throw new UnsupportedOperationException(); } /** * {@inheritDoc} */ @Override - public Cursor query(@NonNull Uri uri, String[] projection, String selection, String[] selectionArgs, - String sortOrder) { - Timber.v("query(uri=" + uri + ", proj=" + Arrays.toString(projection) + ")"); - - SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); - - int match = mUriMatcher.match(uri); - - // all query() parameters, for good measure - String groupBy = null, having = null; - - switch (match) { - case KEY_RING_UNIFIED: - case KEY_RINGS_UNIFIED: - case KEY_RINGS_FIND_BY_EMAIL: - case KEY_RINGS_FIND_BY_SUBKEY: - case KEY_RINGS_FIND_BY_USER_ID: - case KEY_RINGS_FILTER_BY_SIGNER: { - HashMap projectionMap = new HashMap<>(); - projectionMap.put(KeyRings._ID, Tables.KEYS + ".oid AS _id"); - projectionMap.put(KeyRings.MASTER_KEY_ID, Tables.KEYS + "." + Keys.MASTER_KEY_ID); - projectionMap.put(KeyRings.KEY_ID, Tables.KEYS + "." + Keys.KEY_ID); - projectionMap.put(KeyRings.KEY_SIZE, Tables.KEYS + "." + Keys.KEY_SIZE); - projectionMap.put(KeyRings.KEY_CURVE_OID, Tables.KEYS + "." + Keys.KEY_CURVE_OID); - projectionMap.put(KeyRings.IS_REVOKED, Tables.KEYS + "." + Keys.IS_REVOKED); - projectionMap.put(KeyRings.IS_SECURE, Tables.KEYS + "." + Keys.IS_SECURE); - projectionMap.put(KeyRings.CAN_CERTIFY, Tables.KEYS + "." + Keys.CAN_CERTIFY); - projectionMap.put(KeyRings.CAN_ENCRYPT, Tables.KEYS + "." + Keys.CAN_ENCRYPT); - projectionMap.put(KeyRings.CAN_SIGN, Tables.KEYS + "." + Keys.CAN_SIGN); - projectionMap.put(KeyRings.CAN_AUTHENTICATE, Tables.KEYS + "." + Keys.CAN_AUTHENTICATE); - projectionMap.put(KeyRings.CREATION, Tables.KEYS + "." + Keys.CREATION); - projectionMap.put(KeyRings.EXPIRY, Tables.KEYS + "." + Keys.EXPIRY); - projectionMap.put(KeyRings.ALGORITHM, Tables.KEYS + "." + Keys.ALGORITHM); - projectionMap.put(KeyRings.FINGERPRINT, Tables.KEYS + "." + Keys.FINGERPRINT); - projectionMap.put(KeyRings.USER_ID, Tables.USER_PACKETS + "." + UserPackets.USER_ID); - projectionMap.put(KeyRings.NAME, Tables.USER_PACKETS + "." + UserPackets.NAME); - projectionMap.put(KeyRings.EMAIL, Tables.USER_PACKETS + "." + UserPackets.EMAIL); - projectionMap.put(KeyRings.COMMENT, Tables.USER_PACKETS + "." + UserPackets.COMMENT); - projectionMap.put(KeyRings.HAS_DUPLICATE_USER_ID, - "(EXISTS (SELECT * FROM " + Tables.USER_PACKETS + " AS dups" - + " WHERE dups." + UserPackets.MASTER_KEY_ID - + " != " + Tables.KEYS + "." + Keys.MASTER_KEY_ID - + " AND dups." + UserPackets.RANK + " = 0" - + " AND dups." + UserPackets.NAME - + " = " + Tables.USER_PACKETS + "." + UserPackets.NAME + " COLLATE NOCASE" - + " AND dups." + UserPackets.EMAIL - + " = " + Tables.USER_PACKETS + "." + UserPackets.EMAIL + " COLLATE NOCASE" - + ")) AS " + KeyRings.HAS_DUPLICATE_USER_ID); - projectionMap.put(KeyRings.VERIFIED, Tables.CERTS + "." + Certs.VERIFIED); - projectionMap.put(KeyRings.HAS_SECRET, Tables.KEYS + "." + KeyRings.HAS_SECRET); - projectionMap.put(KeyRings.HAS_ANY_SECRET, - "(EXISTS (SELECT * FROM " + Tables.KEY_RINGS_SECRET + " WHERE " - + Tables.KEYS + "." + Keys.MASTER_KEY_ID + " = " - + Tables.KEY_RINGS_SECRET + "." + KeyRingData.MASTER_KEY_ID - + ")) AS " + KeyRings.HAS_ANY_SECRET); - projectionMap.put(KeyRings.HAS_ENCRYPT, - "kE." + Keys.KEY_ID + " AS " + KeyRings.HAS_ENCRYPT); - projectionMap.put(KeyRings.HAS_SIGN_SECRET, - "kS." + Keys.KEY_ID + " AS " + KeyRings.HAS_SIGN_SECRET); - projectionMap.put(KeyRings.HAS_AUTHENTICATE, - "kA." + Keys.KEY_ID + " AS " + KeyRings.HAS_AUTHENTICATE); - projectionMap.put(KeyRings.HAS_AUTHENTICATE_SECRET, - "kA." + Keys.KEY_ID + " AS " + KeyRings.HAS_AUTHENTICATE_SECRET); - projectionMap.put(KeyRings.HAS_CERTIFY_SECRET, - "kC." + Keys.KEY_ID + " AS " + KeyRings.HAS_CERTIFY_SECRET); - projectionMap.put(KeyRings.IS_EXPIRED, - "(" + Tables.KEYS + "." + Keys.EXPIRY + " IS NOT NULL AND " + Tables.KEYS + "." + Keys.EXPIRY - + " < " + new Date().getTime() / 1000 + ") AS " + KeyRings.IS_EXPIRED); - projectionMap.put(KeyRings.API_KNOWN_TO_PACKAGE_NAMES, - "GROUP_CONCAT(DISTINCT aTI." + ApiAutocryptPeer.PACKAGE_NAME + ") AS " - + KeyRings.API_KNOWN_TO_PACKAGE_NAMES); - qb.setProjectionMap(projectionMap); - - if (projection == null) { - throw new IllegalArgumentException("Please provide a projection!"); - } - - // Need this as list so we can search in it - List plist = Arrays.asList(projection); - - qb.setTables( - Tables.KEYS - + " INNER JOIN " + Tables.USER_PACKETS + " ON (" - + Tables.KEYS + "." + Keys.MASTER_KEY_ID - + " = " - + Tables.USER_PACKETS + "." + UserPackets.MASTER_KEY_ID - // we KNOW that the rank zero user packet is a user id! - + " AND " + Tables.USER_PACKETS + "." + UserPackets.RANK + " = 0" - + ") LEFT JOIN " + Tables.CERTS + " ON (" - + Tables.KEYS + "." + Keys.MASTER_KEY_ID - + " = " - + Tables.CERTS + "." + KeyRings.MASTER_KEY_ID - + " AND " + Tables.CERTS + "." + Certs.VERIFIED - + " = " + Certs.VERIFIED_SECRET - + ")" - // fairly expensive joins following, only do when requested - + (plist.contains(KeyRings.HAS_ENCRYPT) ? - " LEFT JOIN " + Tables.KEYS + " AS kE ON (" - +"kE." + Keys.MASTER_KEY_ID - + " = " + Tables.KEYS + "." + Keys.MASTER_KEY_ID - + " AND kE." + Keys.IS_REVOKED + " = 0" - + " AND kE." + Keys.IS_SECURE + " = 1" - + " AND kE." + Keys.CAN_ENCRYPT + " = 1" - + " AND ( kE." + Keys.EXPIRY + " IS NULL OR kE." + Keys.EXPIRY - + " >= " + new Date().getTime() / 1000 + " )" - + ")" : "") - + (plist.contains(KeyRings.HAS_SIGN_SECRET) ? - " LEFT JOIN " + Tables.KEYS + " AS kS ON (" - +"kS." + Keys.MASTER_KEY_ID - + " = " + Tables.KEYS + "." + Keys.MASTER_KEY_ID - + " AND kS." + Keys.IS_REVOKED + " = 0" - + " AND kS." + Keys.IS_SECURE + " = 1" - + " AND kS." + Keys.CAN_SIGN + " = 1" - + " AND kS." + Keys.HAS_SECRET + " > 1" - + " AND ( kS." + Keys.EXPIRY + " IS NULL OR kS." + Keys.EXPIRY - + " >= " + new Date().getTime() / 1000 + " )" - + ")" : "") - + (plist.contains(KeyRings.HAS_AUTHENTICATE) ? - " LEFT JOIN " + Tables.KEYS + " AS kA ON (" - +"kA." + Keys.MASTER_KEY_ID - + " = " + Tables.KEYS + "." + Keys.MASTER_KEY_ID - + " AND kA." + Keys.IS_REVOKED + " = 0" - + " AND kA." + Keys.IS_SECURE + " = 1" - + " AND kA." + Keys.CAN_AUTHENTICATE + " = 1" - + " AND ( kA." + Keys.EXPIRY + " IS NULL OR kA." + Keys.EXPIRY - + " >= " + new Date().getTime() / 1000 + " )" - + ")" : "") - + (plist.contains(KeyRings.HAS_AUTHENTICATE_SECRET) ? - " LEFT JOIN " + Tables.KEYS + " AS kA ON (" - +"kA." + Keys.MASTER_KEY_ID - + " = " + Tables.KEYS + "." + Keys.MASTER_KEY_ID - + " AND kA." + Keys.IS_REVOKED + " = 0" - + " AND kA." + Keys.IS_SECURE + " = 1" - + " AND kA." + Keys.CAN_AUTHENTICATE + " = 1" - + " AND kA." + Keys.HAS_SECRET + " > 1" - + " AND ( kA." + Keys.EXPIRY + " IS NULL OR kA." + Keys.EXPIRY - + " >= " + new Date().getTime() / 1000 + " )" - + ")" : "") - + (plist.contains(KeyRings.HAS_CERTIFY_SECRET) ? - " LEFT JOIN " + Tables.KEYS + " AS kC ON (" - +"kC." + Keys.MASTER_KEY_ID - + " = " + Tables.KEYS + "." + Keys.MASTER_KEY_ID - + " AND kC." + Keys.IS_REVOKED + " = 0" - + " AND kC." + Keys.IS_SECURE + " = 1" - + " AND kC." + Keys.CAN_CERTIFY + " = 1" - + " AND kC." + Keys.HAS_SECRET + " > 1" - + " AND ( kC." + Keys.EXPIRY + " IS NULL OR kC." + Keys.EXPIRY - + " >= " + new Date().getTime() / 1000 + " )" - + ")" : "") - + (plist.contains(KeyRings.API_KNOWN_TO_PACKAGE_NAMES) ? - " LEFT JOIN " + Tables.API_AUTOCRYPT_PEERS + " AS aTI ON (" - +"aTI." + Keys.MASTER_KEY_ID - + " = " + Tables.KEYS + "." + Keys.MASTER_KEY_ID - + ")" : "") - ); - qb.appendWhere(Tables.KEYS + "." + Keys.RANK + " = 0"); - // in case there are multiple verifying certificates - groupBy = Tables.KEYS + "." + Keys.MASTER_KEY_ID; - - switch(match) { - case KEY_RING_UNIFIED: { - qb.appendWhere(" AND " + Tables.KEYS + "." + Keys.MASTER_KEY_ID + " = "); - qb.appendWhereEscapeString(uri.getPathSegments().get(1)); - break; - } - case KEY_RINGS_FIND_BY_SUBKEY: { - try { - String subkey = Long.valueOf(uri.getLastPathSegment()).toString(); - qb.appendWhere(" AND EXISTS (" - + " SELECT 1 FROM " + Tables.KEYS + " AS tmp" - + " WHERE tmp." + UserPackets.MASTER_KEY_ID - + " = " + Tables.KEYS + "." + Keys.MASTER_KEY_ID - + " AND tmp." + Keys.KEY_ID + " = " + subkey + "" - + ")"); - } catch(NumberFormatException e) { - Timber.e(e, "Malformed find by subkey query!"); - qb.appendWhere(" AND 0"); - } - break; - } - case KEY_RINGS_FILTER_BY_SIGNER: { - StringBuilder signerKeyIds = new StringBuilder(); - signerKeyIds.append(selectionArgs[0]); - for (int i = 1; i < selectionArgs.length; i++) { - signerKeyIds.append(',').append(selectionArgs[i]); - } - - qb.appendWhere(" AND EXISTS (SELECT 1 FROM " + Tables.KEY_SIGNATURES + " WHERE " + - Tables.KEY_SIGNATURES + "." + KeySignatures.MASTER_KEY_ID + " = " + Tables.KEYS + "." + Keys.MASTER_KEY_ID + - " AND " + - Tables.KEY_SIGNATURES + "." + KeySignatures.SIGNER_KEY_ID + " IN (" + signerKeyIds + ")" + - ")"); - - selection = null; - selectionArgs = null; - break; - } - case KEY_RINGS_FIND_BY_EMAIL: - case KEY_RINGS_FIND_BY_USER_ID: { - String chunks[] = uri.getLastPathSegment().split(" *, *"); - boolean gotCondition = false; - String emailWhere = ""; - // JAVA ♥ - for (int i = 0; i < chunks.length; ++i) { - if (chunks[i].length() == 0) { - continue; - } - if (i != 0) { - emailWhere += " OR "; - } - if (match == KEY_RINGS_FIND_BY_EMAIL) { - emailWhere += "tmp." + UserPackets.EMAIL + " LIKE " - + DatabaseUtils.sqlEscapeString(chunks[i]); - } else { - emailWhere += "tmp." + UserPackets.USER_ID + " LIKE " - + DatabaseUtils.sqlEscapeString("%" + chunks[i] + "%"); - } - gotCondition = true; - } - if(gotCondition) { - qb.appendWhere(" AND EXISTS (" - + " SELECT 1 FROM " + Tables.USER_PACKETS + " AS tmp" - + " WHERE tmp." + UserPackets.MASTER_KEY_ID - + " = " + Tables.KEYS + "." + Keys.MASTER_KEY_ID - + " AND (" + emailWhere + ")" - + ")"); - } else { - // TODO better way to do this? - Timber.e("Malformed find by email query!"); - qb.appendWhere(" AND 0"); - } - break; - } - } - - if (TextUtils.isEmpty(sortOrder)) { - sortOrder = Tables.USER_PACKETS + "." + UserPackets.USER_ID + " ASC"; - } - - // uri to watch is all /key_rings/ - uri = KeyRings.CONTENT_URI; - - break; - } - - case KEY_RING_KEYS: { - HashMap projectionMap = new HashMap<>(); - projectionMap.put(Keys._ID, Tables.KEYS + ".oid AS _id"); - projectionMap.put(Keys.MASTER_KEY_ID, Tables.KEYS + "." + Keys.MASTER_KEY_ID); - projectionMap.put(Keys.RANK, Tables.KEYS + "." + Keys.RANK); - projectionMap.put(Keys.KEY_ID, Keys.KEY_ID); - projectionMap.put(Keys.KEY_SIZE, Keys.KEY_SIZE); - projectionMap.put(Keys.KEY_CURVE_OID, Keys.KEY_CURVE_OID); - projectionMap.put(Keys.IS_REVOKED, Tables.KEYS + "." + Keys.IS_REVOKED); - projectionMap.put(Keys.IS_SECURE, Tables.KEYS + "." + Keys.IS_SECURE); - projectionMap.put(Keys.CAN_CERTIFY, Keys.CAN_CERTIFY); - projectionMap.put(Keys.CAN_ENCRYPT, Keys.CAN_ENCRYPT); - projectionMap.put(Keys.CAN_SIGN, Keys.CAN_SIGN); - projectionMap.put(Keys.CAN_AUTHENTICATE, Keys.CAN_AUTHENTICATE); - projectionMap.put(Keys.HAS_SECRET, Keys.HAS_SECRET); - projectionMap.put(Keys.CREATION, Keys.CREATION); - projectionMap.put(Keys.EXPIRY, Keys.EXPIRY); - projectionMap.put(Keys.ALGORITHM, Keys.ALGORITHM); - projectionMap.put(Keys.FINGERPRINT, Keys.FINGERPRINT); - qb.setProjectionMap(projectionMap); - - qb.setTables(Tables.KEYS); - qb.appendWhere(Keys.MASTER_KEY_ID + " = "); - qb.appendWhereEscapeString(uri.getPathSegments().get(1)); - - break; - } - - case KEY_RINGS_USER_IDS: - case KEY_RING_USER_IDS: - case KEY_RING_LINKED_IDS: { - HashMap projectionMap = new HashMap<>(); - projectionMap.put(UserPackets._ID, Tables.USER_PACKETS + ".oid AS _id"); - projectionMap.put(UserPackets.MASTER_KEY_ID, Tables.USER_PACKETS + "." + UserPackets.MASTER_KEY_ID); - projectionMap.put(UserPackets.TYPE, Tables.USER_PACKETS + "." + UserPackets.TYPE); - projectionMap.put(UserPackets.USER_ID, Tables.USER_PACKETS + "." + UserPackets.USER_ID); - projectionMap.put(UserPackets.NAME, Tables.USER_PACKETS + "." + UserPackets.NAME); - projectionMap.put(UserPackets.EMAIL, Tables.USER_PACKETS + "." + UserPackets.EMAIL); - projectionMap.put(UserPackets.COMMENT, Tables.USER_PACKETS + "." + UserPackets.COMMENT); - projectionMap.put(UserPackets.ATTRIBUTE_DATA, Tables.USER_PACKETS + "." + UserPackets.ATTRIBUTE_DATA); - projectionMap.put(UserPackets.RANK, Tables.USER_PACKETS + "." + UserPackets.RANK); - projectionMap.put(UserPackets.IS_PRIMARY, Tables.USER_PACKETS + "." + UserPackets.IS_PRIMARY); - projectionMap.put(UserPackets.IS_REVOKED, Tables.USER_PACKETS + "." + UserPackets.IS_REVOKED); - // we take the minimum (>0) here, where "1" is "verified by known secret key" - projectionMap.put(UserPackets.VERIFIED, "MIN(" + Certs.VERIFIED + ") AS " + UserPackets.VERIFIED); - qb.setProjectionMap(projectionMap); - - qb.setTables(Tables.USER_PACKETS - + " LEFT JOIN " + Tables.CERTS + " ON (" - + Tables.USER_PACKETS + "." + UserPackets.MASTER_KEY_ID + " = " - + Tables.CERTS + "." + Certs.MASTER_KEY_ID - + " AND " + Tables.USER_PACKETS + "." + UserPackets.RANK + " = " - + Tables.CERTS + "." + Certs.RANK - + " AND " + Tables.CERTS + "." + Certs.VERIFIED + " > 0" - + ")"); - groupBy = Tables.USER_PACKETS + "." + UserPackets.MASTER_KEY_ID - + ", " + Tables.USER_PACKETS + "." + UserPackets.RANK; - - if (match == KEY_RING_LINKED_IDS) { - qb.appendWhere(Tables.USER_PACKETS + "." + UserPackets.TYPE + " = " - + WrappedUserAttribute.UAT_URI_ATTRIBUTE); - } else { - qb.appendWhere(Tables.USER_PACKETS + "." + UserPackets.TYPE + " IS NULL"); - } - - // If we are searching for a particular keyring's ids, add where - if (match == KEY_RING_USER_IDS || match == KEY_RING_LINKED_IDS) { - qb.appendWhere(" AND "); - qb.appendWhere(Tables.USER_PACKETS + "." + UserPackets.MASTER_KEY_ID + " = "); - qb.appendWhereEscapeString(uri.getPathSegments().get(1)); - } - - if (TextUtils.isEmpty(sortOrder)) { - sortOrder = Tables.USER_PACKETS + "." + UserPackets.MASTER_KEY_ID + " ASC" - + "," + Tables.USER_PACKETS + "." + UserPackets.RANK + " ASC"; - } - - break; - } - - case KEY_RINGS_PUBLIC: - case KEY_RING_PUBLIC: { - HashMap projectionMap = new HashMap<>(); - projectionMap.put(KeyRingData._ID, Tables.KEY_RINGS_PUBLIC + ".oid AS _id"); - projectionMap.put(KeyRingData.MASTER_KEY_ID, KeyRingData.MASTER_KEY_ID); - projectionMap.put(KeyRingData.KEY_RING_DATA, KeyRingData.KEY_RING_DATA); - qb.setProjectionMap(projectionMap); - - qb.setTables(Tables.KEY_RINGS_PUBLIC); - - if(match == KEY_RING_PUBLIC) { - qb.appendWhere(KeyRings.MASTER_KEY_ID + " = "); - qb.appendWhereEscapeString(uri.getPathSegments().get(1)); - } - - break; - } - - case KEY_RINGS_SECRET: - case KEY_RING_SECRET: { - HashMap projectionMap = new HashMap<>(); - projectionMap.put(KeyRingData._ID, Tables.KEY_RINGS_SECRET + ".oid AS _id"); - projectionMap.put(KeyRingData.MASTER_KEY_ID, KeyRingData.MASTER_KEY_ID); - projectionMap.put(KeyRingData.KEY_RING_DATA, KeyRingData.KEY_RING_DATA); - qb.setProjectionMap(projectionMap); - - qb.setTables(Tables.KEY_RINGS_SECRET); - - if(match == KEY_RING_SECRET) { - qb.appendWhere(KeyRings.MASTER_KEY_ID + " = "); - qb.appendWhereEscapeString(uri.getPathSegments().get(1)); - } - - break; - } - - case KEY_RING_CERTS: - case KEY_RING_CERTS_SPECIFIC: - case KEY_RING_LINKED_ID_CERTS: { - HashMap projectionMap = new HashMap<>(); - projectionMap.put(Certs._ID, Tables.CERTS + ".oid AS " + Certs._ID); - projectionMap.put(Certs.MASTER_KEY_ID, Tables.CERTS + "." + Certs.MASTER_KEY_ID); - projectionMap.put(Certs.RANK, Tables.CERTS + "." + Certs.RANK); - projectionMap.put(Certs.VERIFIED, Tables.CERTS + "." + Certs.VERIFIED); - projectionMap.put(Certs.TYPE, Tables.CERTS + "." + Certs.TYPE); - projectionMap.put(Certs.CREATION, Tables.CERTS + "." + Certs.CREATION); - projectionMap.put(Certs.KEY_ID_CERTIFIER, Tables.CERTS + "." + Certs.KEY_ID_CERTIFIER); - projectionMap.put(Certs.DATA, Tables.CERTS + "." + Certs.DATA); - projectionMap.put(Certs.USER_ID, Tables.USER_PACKETS + "." + UserPackets.USER_ID); - projectionMap.put(Certs.SIGNER_UID, "signer." + UserPackets.USER_ID + " AS " + Certs.SIGNER_UID); - qb.setProjectionMap(projectionMap); - - qb.setTables(Tables.CERTS - + " JOIN " + Tables.USER_PACKETS + " ON (" - + Tables.CERTS + "." + Certs.MASTER_KEY_ID + " = " - + Tables.USER_PACKETS + "." + UserPackets.MASTER_KEY_ID - + " AND " - + Tables.CERTS + "." + Certs.RANK + " = " - + Tables.USER_PACKETS + "." + UserPackets.RANK - + ") LEFT JOIN " + Tables.USER_PACKETS + " AS signer ON (" - + Tables.CERTS + "." + Certs.KEY_ID_CERTIFIER + " = " - + "signer." + UserPackets.MASTER_KEY_ID - + " AND " - + "signer." + Keys.RANK + " = 0" - + ")"); - - groupBy = Tables.CERTS + "." + Certs.RANK + ", " - + Tables.CERTS + "." + Certs.KEY_ID_CERTIFIER; - - qb.appendWhere(Tables.CERTS + "." + Certs.MASTER_KEY_ID + " = "); - qb.appendWhereEscapeString(uri.getPathSegments().get(1)); - if(match == KEY_RING_CERTS_SPECIFIC) { - qb.appendWhere(" AND " + Tables.CERTS + "." + Certs.RANK + " = "); - qb.appendWhereEscapeString(uri.getPathSegments().get(3)); - qb.appendWhere(" AND " + Tables.CERTS + "." + Certs.KEY_ID_CERTIFIER+ " = "); - qb.appendWhereEscapeString(uri.getPathSegments().get(4)); - } - - if (match == KEY_RING_LINKED_ID_CERTS) { - qb.appendWhere(" AND " + Tables.USER_PACKETS + "." - + UserPackets.TYPE + " IS NOT NULL"); - - qb.appendWhere(" AND " + Tables.USER_PACKETS + "." - + UserPackets.RANK + " = "); - qb.appendWhereEscapeString(uri.getPathSegments().get(3)); - } else { - qb.appendWhere(" AND " + Tables.USER_PACKETS + "." + UserPackets.TYPE + " IS NULL"); - } - - break; - } - - case AUTOCRYPT_PEERS_BY_MASTER_KEY_ID: - case AUTOCRYPT_PEERS_BY_PACKAGE_NAME: - case AUTOCRYPT_PEERS_BY_PACKAGE_NAME_AND_TRUST_ID: { - long unixSeconds = new Date().getTime() / 1000; - - HashMap projectionMap = new HashMap<>(); - projectionMap.put(ApiAutocryptPeer._ID, Tables.API_AUTOCRYPT_PEERS + ".oid AS " + ApiAutocryptPeer._ID); - projectionMap.put(ApiAutocryptPeer.IDENTIFIER, ApiAutocryptPeer.IDENTIFIER); - projectionMap.put(ApiAutocryptPeer.PACKAGE_NAME, ApiAutocryptPeer.PACKAGE_NAME); - projectionMap.put(ApiAutocryptPeer.LAST_SEEN, ApiAutocryptPeer.LAST_SEEN); - projectionMap.put(ApiAutocryptPeer.MASTER_KEY_ID, Tables.API_AUTOCRYPT_PEERS + "." + ApiAutocryptPeer.MASTER_KEY_ID); - projectionMap.put(ApiAutocryptPeer.IS_MUTUAL, ApiAutocryptPeer.IS_MUTUAL); - projectionMap.put(ApiAutocryptPeer.LAST_SEEN_KEY, ApiAutocryptPeer.LAST_SEEN_KEY); - projectionMap.put(ApiAutocryptPeer.GOSSIP_MASTER_KEY_ID, ApiAutocryptPeer.GOSSIP_MASTER_KEY_ID); - projectionMap.put(ApiAutocryptPeer.GOSSIP_LAST_SEEN_KEY, ApiAutocryptPeer.GOSSIP_LAST_SEEN_KEY); - projectionMap.put(ApiAutocryptPeer.GOSSIP_ORIGIN, ApiAutocryptPeer.GOSSIP_ORIGIN); - projectionMap.put(ApiAutocryptPeer.KEY_IS_REVOKED, "ac_key." + Keys.IS_REVOKED + " AS " + ApiAutocryptPeer.KEY_IS_REVOKED); - projectionMap.put(ApiAutocryptPeer.KEY_IS_EXPIRED, "(CASE" + - " WHEN ac_key." + Keys.EXPIRY + " IS NULL THEN 0" + - " WHEN ac_key." + Keys.EXPIRY + " > " + unixSeconds + " THEN 0" + - " ELSE 1 END) AS " + ApiAutocryptPeer.KEY_IS_EXPIRED); - projectionMap.put(ApiAutocryptPeer.KEY_IS_VERIFIED, "EXISTS (SELECT * FROM " + Tables.CERTS + - " WHERE " + Tables.CERTS + "." + Certs.MASTER_KEY_ID + " = " + - Tables.API_AUTOCRYPT_PEERS + "." + ApiAutocryptPeer.MASTER_KEY_ID + - " AND " + Certs.VERIFIED + " = " + Certs.VERIFIED_SECRET + - ") AS " + ApiAutocryptPeer.KEY_IS_VERIFIED); - projectionMap.put(ApiAutocryptPeer.GOSSIP_KEY_IS_REVOKED, - "gossip_key." + Keys.IS_REVOKED + " AS " + ApiAutocryptPeer.GOSSIP_KEY_IS_REVOKED); - projectionMap.put(ApiAutocryptPeer.GOSSIP_KEY_IS_EXPIRED, "(CASE" + - " WHEN gossip_key." + Keys.EXPIRY + " IS NULL THEN 0" + - " WHEN gossip_key." + Keys.EXPIRY + " > " + unixSeconds + " THEN 0" + - " ELSE 1 END) AS " + ApiAutocryptPeer.GOSSIP_KEY_IS_EXPIRED); - projectionMap.put(ApiAutocryptPeer.GOSSIP_KEY_IS_VERIFIED, "EXISTS (SELECT * FROM " + Tables.CERTS + - " WHERE " + Tables.CERTS + "." + Certs.MASTER_KEY_ID + " = " + - Tables.API_AUTOCRYPT_PEERS + "." + ApiAutocryptPeer.GOSSIP_MASTER_KEY_ID + - " AND " + Certs.VERIFIED + " = " + Certs.VERIFIED_SECRET + - ") AS " + ApiAutocryptPeer.GOSSIP_KEY_IS_VERIFIED); - qb.setProjectionMap(projectionMap); - - qb.setTables(Tables.API_AUTOCRYPT_PEERS + - " LEFT JOIN " + Tables.KEYS + " AS ac_key" + - " ON (ac_key." + Keys.MASTER_KEY_ID + " = " + Tables.API_AUTOCRYPT_PEERS + "." + ApiAutocryptPeer.MASTER_KEY_ID + - " AND ac_key." + Keys.RANK + " = 0)" + - " LEFT JOIN " + Tables.KEYS + " AS gossip_key" + - " ON (gossip_key." + Keys.MASTER_KEY_ID + " = " + ApiAutocryptPeer.GOSSIP_MASTER_KEY_ID + - " AND gossip_key." + Keys.RANK + " = 0)" - ); - - if (match == AUTOCRYPT_PEERS_BY_MASTER_KEY_ID) { - long masterKeyId = Long.parseLong(uri.getLastPathSegment()); - - qb.appendWhere(Tables.API_AUTOCRYPT_PEERS + "." + ApiAutocryptPeer.MASTER_KEY_ID + " = "); - qb.appendWhere(Long.toString(masterKeyId)); - } else if (match == AUTOCRYPT_PEERS_BY_PACKAGE_NAME) { - String packageName = uri.getPathSegments().get(2); - - qb.appendWhere(Tables.API_AUTOCRYPT_PEERS + "." + ApiAutocryptPeer.PACKAGE_NAME + " = "); - qb.appendWhereEscapeString(packageName); - } else { // AUTOCRYPT_PEERS_BY_PACKAGE_NAME_AND_TRUST_ID - String packageName = uri.getPathSegments().get(2); - String autocryptPeer = uri.getPathSegments().get(3); - - selection = Tables.API_AUTOCRYPT_PEERS + "." + ApiAutocryptPeer.PACKAGE_NAME + " = ? AND " + - Tables.API_AUTOCRYPT_PEERS + "." + ApiAutocryptPeer.IDENTIFIER + " = ?"; - selectionArgs = new String[] { packageName, autocryptPeer }; - } - - break; - } - - case UPDATED_KEYS: - case UPDATED_KEYS_SPECIFIC: { - HashMap projectionMap = new HashMap<>(); - qb.setTables(Tables.UPDATED_KEYS); - projectionMap.put(UpdatedKeys.MASTER_KEY_ID, Tables.UPDATED_KEYS + "." + UpdatedKeys.MASTER_KEY_ID); - projectionMap.put(UpdatedKeys.LAST_UPDATED, Tables.UPDATED_KEYS + "." + UpdatedKeys.LAST_UPDATED); - projectionMap.put(UpdatedKeys.SEEN_ON_KEYSERVERS, - Tables.UPDATED_KEYS + "." + UpdatedKeys.SEEN_ON_KEYSERVERS); - qb.setProjectionMap(projectionMap); - if (match == UPDATED_KEYS_SPECIFIC) { - qb.appendWhere(UpdatedKeys.MASTER_KEY_ID + " = "); - qb.appendWhereEscapeString(uri.getPathSegments().get(1)); - } - break; - } - - case API_APPS: { - qb.setTables(Tables.API_APPS); - - break; - } - case API_APPS_BY_PACKAGE_NAME: { - qb.setTables(Tables.API_APPS); - qb.appendWhere(ApiApps.PACKAGE_NAME + " = "); - qb.appendWhereEscapeString(uri.getLastPathSegment()); - - break; - } - case API_ALLOWED_KEYS: { - qb.setTables(Tables.API_ALLOWED_KEYS); - qb.appendWhere(Tables.API_ALLOWED_KEYS + "." + ApiAllowedKeys.PACKAGE_NAME + " = "); - qb.appendWhereEscapeString(uri.getPathSegments().get(1)); - - break; - } - default: { - throw new IllegalArgumentException("Unknown URI " + uri + " (" + match + ")"); - } - - } - - // If no sort order is specified use the default - String orderBy; - if (TextUtils.isEmpty(sortOrder)) { - orderBy = null; - } else { - orderBy = sortOrder; - } - - SQLiteDatabase db = getDb().getReadableDatabase(); - - Cursor cursor = qb.query(db, projection, selection, selectionArgs, groupBy, null, orderBy); - if (cursor != null) { - // Tell the cursor what uri to watch, so it knows when its source data changes - cursor.setNotificationUri(getContext().getContentResolver(), uri); - } - - Timber.d("Query: " + qb.buildQuery(projection, selection, null, null, orderBy, null)); - - if (Constants.DEBUG && Constants.DEBUG_LOG_DB_QUERIES) { - Timber.d("Cursor: " + dumpCursorToString(cursor)); - } - - if (Constants.DEBUG && Constants.DEBUG_EXPLAIN_QUERIES) { - String rawQuery = qb.buildQuery(projection, selection, groupBy, having, orderBy, null); - DatabaseUtil.explainQuery(db, rawQuery); - } - - return cursor; - } - - /** - * {@inheritDoc} - */ - @Override - public Uri insert(Uri uri, ContentValues values) { + public Uri insert(@NonNull Uri uri, ContentValues values) { Timber.d("insert(uri=" + uri + ", values=" + values.toString() + ")"); - final SQLiteDatabase db = getDb().getWritableDatabase(); + final SupportSQLiteDatabase db = getDb().getWritableDatabase(); Uri rowUri = null; Long keyId = null; @@ -872,17 +130,12 @@ public class KeychainProvider extends ContentProvider implements SimpleContentRe switch (match) { case KEY_RING_PUBLIC: { - db.insertOrThrow(Tables.KEY_RINGS_PUBLIC, null, values); - keyId = values.getAsLong(KeyRings.MASTER_KEY_ID); - break; - } - case KEY_RING_SECRET: { - db.insertOrThrow(Tables.KEY_RINGS_SECRET, null, values); - keyId = values.getAsLong(KeyRings.MASTER_KEY_ID); + db.insert(Tables.KEY_RINGS_PUBLIC, SQLiteDatabase.CONFLICT_FAIL, values); + keyId = values.getAsLong(Keys.MASTER_KEY_ID); break; } case KEY_RING_KEYS: { - db.insertOrThrow(Tables.KEYS, null, values); + db.insert(Tables.KEYS, SQLiteDatabase.CONFLICT_FAIL, values); keyId = values.getAsLong(Keys.MASTER_KEY_ID); break; } @@ -897,53 +150,29 @@ public class KeychainProvider extends ContentProvider implements SimpleContentRe if (((Number) values.get(UserPacketsColumns.RANK)).intValue() == 0 && values.get(UserPacketsColumns.USER_ID) == null) { throw new AssertionError("Rank 0 user packet must be a user id!"); } - db.insertOrThrow(Tables.USER_PACKETS, null, values); + db.insert(Tables.USER_PACKETS, SQLiteDatabase.CONFLICT_FAIL, values); keyId = values.getAsLong(UserPackets.MASTER_KEY_ID); break; } case KEY_RING_CERTS: { // we replace here, keeping only the latest signature // TODO this would be better handled in savePublicKeyRing directly! - db.replaceOrThrow(Tables.CERTS, null, values); + db.insert(Tables.CERTS, SQLiteDatabase.CONFLICT_FAIL, values); keyId = values.getAsLong(Certs.MASTER_KEY_ID); break; } - case UPDATED_KEYS: { - keyId = values.getAsLong(UpdatedKeys.MASTER_KEY_ID); - try { - db.insertOrThrow(Tables.UPDATED_KEYS, null, values); - } catch (SQLiteConstraintException e) { - db.update(Tables.UPDATED_KEYS, values, - UpdatedKeys.MASTER_KEY_ID + " = ?", new String[] { Long.toString(keyId) }); - } - rowUri = UpdatedKeys.CONTENT_URI; - break; - } case KEY_SIGNATURES: { - db.insert(Tables.KEY_SIGNATURES, null, values); + db.insert(Tables.KEY_SIGNATURES, SQLiteDatabase.CONFLICT_FAIL, values); rowUri = KeySignatures.CONTENT_URI; break; } - case API_APPS: { - db.insert(Tables.API_APPS, null, values); - break; - } - case API_ALLOWED_KEYS: { - // set foreign key automatically based on given uri - // e.g., api_apps/com.example.app/allowed_keys/ - String packageName = uri.getPathSegments().get(1); - values.put(ApiAllowedKeys.PACKAGE_NAME, packageName); - - db.insert(Tables.API_ALLOWED_KEYS, null, values); - break; - } default: { throw new UnsupportedOperationException("Unknown uri: " + uri); } } if (keyId != null) { - uri = KeyRings.buildGenericKeyRingUri(keyId); + uri = DatabaseNotifyManager.getNotifyUriMasterKeyId(keyId); rowUri = uri; } @@ -954,90 +183,16 @@ public class KeychainProvider extends ContentProvider implements SimpleContentRe return rowUri; } - /** - * {@inheritDoc} - */ @Override - public int delete(Uri uri, String additionalSelection, String[] selectionArgs) { - Timber.v("delete(uri=" + uri + ")"); - - final SQLiteDatabase db = getDb().getWritableDatabase(); - - int count; - final int match = mUriMatcher.match(uri); - - ContentResolver contentResolver = getContext().getContentResolver(); - switch (match) { - // dangerous - case KEY_RINGS_UNIFIED: { - count = db.delete(Tables.KEY_RINGS_PUBLIC, null, null); - break; - } - case KEY_RING_PUBLIC: { - @SuppressWarnings("ConstantConditions") // ensured by uriMatcher above - String selection = KeyRings.MASTER_KEY_ID + " = " + uri.getPathSegments().get(1); - if (!TextUtils.isEmpty(additionalSelection)) { - selection += " AND (" + additionalSelection + ")"; - } - // corresponding keys and userIds are deleted by ON DELETE CASCADE - count = db.delete(Tables.KEY_RINGS_PUBLIC, selection, selectionArgs); - break; - } - case KEY_RING_SECRET: { - @SuppressWarnings("ConstantConditions") // ensured by uriMatcher above - String selection = KeyRings.MASTER_KEY_ID + " = " + uri.getPathSegments().get(1); - if (!TextUtils.isEmpty(additionalSelection)) { - selection += " AND (" + additionalSelection + ")"; - } - count = db.delete(Tables.KEY_RINGS_SECRET, selection, selectionArgs); - break; - } - - case AUTOCRYPT_PEERS_BY_PACKAGE_NAME_AND_TRUST_ID: { - String packageName = uri.getPathSegments().get(2); - String autocryptPeer = uri.getPathSegments().get(3); - - String selection = ApiAutocryptPeer.PACKAGE_NAME + " = ? AND " + ApiAutocryptPeer.IDENTIFIER + " = ?"; - selectionArgs = new String[] { packageName, autocryptPeer }; - - count = db.delete(Tables.API_AUTOCRYPT_PEERS, selection, selectionArgs); - break; - } - - case AUTOCRYPT_PEERS_BY_MASTER_KEY_ID: - String selection = ApiAutocryptPeer.MASTER_KEY_ID + " = " + uri.getLastPathSegment(); - if (!TextUtils.isEmpty(additionalSelection)) { - selection += " AND (" + additionalSelection + ")"; - } - count = db.delete(Tables.API_AUTOCRYPT_PEERS, selection, selectionArgs); - break; - - case API_APPS_BY_PACKAGE_NAME: { - count = db.delete(Tables.API_APPS, buildDefaultApiAppsSelection(uri, additionalSelection), - selectionArgs); - break; - } - case API_ALLOWED_KEYS: { - count = db.delete(Tables.API_ALLOWED_KEYS, buildDefaultApiAllowedKeysSelection(uri, additionalSelection), - selectionArgs); - break; - } - default: { - throw new UnsupportedOperationException("Unknown uri: " + uri); - } - } - - return count; + public int delete(@NonNull Uri uri, String additionalSelection, String[] selectionArgs) { + throw new UnsupportedOperationException(); } - /** - * {@inheritDoc} - */ @Override - public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + public int update(@NonNull Uri uri, ContentValues values, String selection, String[] selectionArgs) { Timber.v("update(uri=" + uri + ", values=" + values.toString() + ")"); - final SQLiteDatabase db = getDb().getWritableDatabase(); + final SupportSQLiteDatabase db = getDb().getWritableDatabase(); int count = 0; try { @@ -1054,40 +209,7 @@ public class KeychainProvider extends ContentProvider implements SimpleContentRe if (!TextUtils.isEmpty(selection)) { actualSelection += " AND (" + selection + ")"; } - count = db.update(Tables.KEYS, values, actualSelection, selectionArgs); - break; - } - case API_APPS_BY_PACKAGE_NAME: { - count = db.update(Tables.API_APPS, values, - buildDefaultApiAppsSelection(uri, selection), selectionArgs); - break; - } - case UPDATED_KEYS: { - if (values.size() != 2 || - !values.containsKey(UpdatedKeys.SEEN_ON_KEYSERVERS) || - !values.containsKey(UpdatedKeys.LAST_UPDATED) || - values.get(UpdatedKeys.LAST_UPDATED) != null || - values.get(UpdatedKeys.SEEN_ON_KEYSERVERS) != null || - selection != null || selectionArgs != null) { - throw new UnsupportedOperationException("can only reset all keys"); - } - - db.update(Tables.UPDATED_KEYS, values, null, null); - break; - } - case AUTOCRYPT_PEERS_BY_PACKAGE_NAME_AND_TRUST_ID: { - String packageName = uri.getPathSegments().get(2); - String identifier = uri.getLastPathSegment(); - values.put(ApiAutocryptPeer.PACKAGE_NAME, packageName); - values.put(ApiAutocryptPeer.IDENTIFIER, identifier); - - int updated = db.update(Tables.API_AUTOCRYPT_PEERS, values, - ApiAutocryptPeer.PACKAGE_NAME + "=? AND " + ApiAutocryptPeer.IDENTIFIER + "=?", - new String[] { packageName, identifier }); - if (updated == 0) { - db.insertOrThrow(Tables.API_AUTOCRYPT_PEERS, null, values); - } - + count = db.update(Tables.KEYS, SQLiteDatabase.CONFLICT_FAIL, values, actualSelection, selectionArgs); break; } default: { @@ -1100,35 +222,4 @@ public class KeychainProvider extends ContentProvider implements SimpleContentRe return count; } - - /** - * Build default selection statement for API apps. If no extra selection is specified only build - * where clause with rowId - * - * @param uri - * @param selection - * @return - */ - private String buildDefaultApiAppsSelection(Uri uri, String selection) { - String packageName = DatabaseUtils.sqlEscapeString(uri.getLastPathSegment()); - - String andSelection = ""; - if (!TextUtils.isEmpty(selection)) { - andSelection = " AND (" + selection + ")"; - } - - return ApiApps.PACKAGE_NAME + "=" + packageName + andSelection; - } - - private String buildDefaultApiAllowedKeysSelection(Uri uri, String selection) { - String packageName = DatabaseUtils.sqlEscapeString(uri.getPathSegments().get(1)); - - String andSelection = ""; - if (!TextUtils.isEmpty(selection)) { - andSelection = " AND (" + selection + ")"; - } - - return ApiAllowedKeys.PACKAGE_NAME + "=" + packageName + andSelection; - } - } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/LastUpdateInteractor.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/LastUpdateInteractor.java deleted file mode 100644 index 89520cb1a..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/LastUpdateInteractor.java +++ /dev/null @@ -1,78 +0,0 @@ -package org.sufficientlysecure.keychain.provider; - - -import java.util.GregorianCalendar; - -import android.content.ContentResolver; -import android.content.ContentValues; -import android.content.Context; -import android.database.Cursor; -import android.net.Uri; -import android.support.annotation.Nullable; - -import org.sufficientlysecure.keychain.provider.KeychainContract.UpdatedKeys; - - -public class LastUpdateInteractor { - private final ContentResolver contentResolver; - private final DatabaseNotifyManager databaseNotifyManager; - - - public static LastUpdateInteractor create(Context context) { - return new LastUpdateInteractor(context.getContentResolver(), DatabaseNotifyManager.create(context)); - } - - private LastUpdateInteractor(ContentResolver contentResolver, DatabaseNotifyManager databaseNotifyManager) { - this.contentResolver = contentResolver; - this.databaseNotifyManager = databaseNotifyManager; - } - - @Nullable - public Boolean getSeenOnKeyservers(long masterKeyId) { - Cursor cursor = contentResolver.query( - UpdatedKeys.CONTENT_URI, - new String[] { UpdatedKeys.SEEN_ON_KEYSERVERS }, - UpdatedKeys.MASTER_KEY_ID + " = ?", - new String[] { "" + masterKeyId }, - null - ); - if (cursor == null) { - return null; - } - - Boolean seenOnKeyservers; - try { - if (!cursor.moveToNext()) { - return null; - } - seenOnKeyservers = cursor.isNull(0) ? null : cursor.getInt(0) != 0; - } finally { - cursor.close(); - } - return seenOnKeyservers; - } - - public void resetAllLastUpdatedTimes() { - ContentValues values = new ContentValues(); - values.putNull(UpdatedKeys.LAST_UPDATED); - values.putNull(UpdatedKeys.SEEN_ON_KEYSERVERS); - contentResolver.update(UpdatedKeys.CONTENT_URI, values, null, null); - } - - public Uri renewKeyLastUpdatedTime(long masterKeyId, boolean seenOnKeyservers) { - boolean isFirstKeyserverStatusCheck = getSeenOnKeyservers(masterKeyId) == null; - - ContentValues values = new ContentValues(); - values.put(UpdatedKeys.MASTER_KEY_ID, masterKeyId); - values.put(UpdatedKeys.LAST_UPDATED, GregorianCalendar.getInstance().getTimeInMillis() / 1000); - if (seenOnKeyservers || isFirstKeyserverStatusCheck) { - values.put(UpdatedKeys.SEEN_ON_KEYSERVERS, seenOnKeyservers); - } - - // this will actually update/replace, doing the right thing™ for seenOnKeyservers value - // see `KeychainProvider.insert()` - Uri insert = contentResolver.insert(UpdatedKeys.CONTENT_URI, values); - databaseNotifyManager.notifyKeyserverStatusChange(masterKeyId); - return insert; - } -} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/OverriddenWarningsRepository.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/OverriddenWarningsRepository.java deleted file mode 100644 index 4405363fe..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/OverriddenWarningsRepository.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright (C) 2017 Schürmann & Breitmoser GbR - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sufficientlysecure.keychain.provider; - - -import android.content.ContentValues; -import android.content.Context; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; - -import org.sufficientlysecure.keychain.provider.KeychainContract.OverriddenWarnings; -import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables; - - -public class OverriddenWarningsRepository { - private final Context context; - private KeychainDatabase keychainDatabase; - - public static OverriddenWarningsRepository createOverriddenWarningsRepository(Context context) { - return new OverriddenWarningsRepository(context); - } - - private OverriddenWarningsRepository(Context context) { - this.context = context; - } - - private KeychainDatabase getDb() { - if (keychainDatabase == null) { - keychainDatabase = new KeychainDatabase(context); - } - return keychainDatabase; - } - - public boolean isWarningOverridden(String identifier) { - SQLiteDatabase db = getDb().getReadableDatabase(); - Cursor cursor = db.query( - Tables.OVERRIDDEN_WARNINGS, - new String[] { "COUNT(*)" }, - OverriddenWarnings.IDENTIFIER + " = ?", - new String[] { identifier }, - null, null, null); - - try { - cursor.moveToFirst(); - return cursor.getInt(0) > 0; - } finally { - cursor.close(); - db.close(); - } - } - - public void putOverride(String identifier) { - SQLiteDatabase db = getDb().getWritableDatabase(); - ContentValues cv = new ContentValues(); - cv.put(OverriddenWarnings.IDENTIFIER, identifier); - db.replace(Tables.OVERRIDDEN_WARNINGS, null, cv); - db.close(); - } - - public void deleteOverride(String identifier) { - SQLiteDatabase db = getDb().getWritableDatabase(); - db.delete(Tables.OVERRIDDEN_WARNINGS, OverriddenWarnings.IDENTIFIER + " = ?", new String[] { identifier }); - db.close(); - } -} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/SimpleContentResolverInterface.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/SimpleContentResolverInterface.java deleted file mode 100644 index 483dd7419..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/SimpleContentResolverInterface.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (C) 2017 Schürmann & Breitmoser GbR - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sufficientlysecure.keychain.provider; - - -import android.content.ContentValues; -import android.database.Cursor; -import android.net.Uri; - -/** This interface contains the principal methods for database access - * from {#android.content.ContentResolver}. It is used to allow substitution - * of a ContentResolver in DAOs. - * - * @see ApiDataAccessObject - */ -public interface SimpleContentResolverInterface { - Cursor query(Uri contentUri, String[] projection, String selection, String[] selectionArgs, String sortOrder); - - Uri insert(Uri contentUri, ContentValues values); - - int update(Uri contentUri, ContentValues values, String where, String[] selectionArgs); - - int delete(Uri contentUri, String where, String[] selectionArgs); -} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/TemporaryFileProvider.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/TemporaryFileProvider.java index 8a9ee7738..18ca44a0d 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/TemporaryFileProvider.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/TemporaryFileProvider.java @@ -18,8 +18,17 @@ package org.sufficientlysecure.keychain.provider; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + import android.content.ClipDescription; import android.content.ContentProvider; +import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; @@ -29,18 +38,15 @@ import android.database.sqlite.SQLiteOpenHelper; import android.net.Uri; import android.os.ParcelFileDescriptor; import android.provider.MediaStore; +import android.support.annotation.NonNull; +import androidx.work.OneTimeWorkRequest; +import androidx.work.WorkManager; +import androidx.work.Worker; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.util.DatabaseUtil; import timber.log.Timber; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.util.UUID; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - /** * TemporaryStorageProvider stores decrypted files inside the app's cache directory previously to * sharing them with other applications. @@ -81,16 +87,20 @@ public class TemporaryFileProvider extends ContentProvider { private static Pattern UUID_PATTERN = Pattern.compile("[a-fA-F0-9-]+"); public static Uri createFile(Context context, String targetName, String mimeType) { + ContentResolver contentResolver = context.getContentResolver(); + ContentValues contentValues = new ContentValues(); contentValues.put(TemporaryFileColumns.COLUMN_NAME, targetName); contentValues.put(TemporaryFileColumns.COLUMN_TYPE, mimeType); - return context.getContentResolver().insert(CONTENT_URI, contentValues); + contentValues.put(TemporaryFileColumns.COLUMN_TIME, System.currentTimeMillis()); + Uri resultUri = contentResolver.insert(CONTENT_URI, contentValues); + + scheduleCleanupAfterTtl(); + return resultUri; } public static Uri createFile(Context context, String targetName) { - ContentValues contentValues = new ContentValues(); - contentValues.put(TemporaryFileColumns.COLUMN_NAME, targetName); - return context.getContentResolver().insert(CONTENT_URI, contentValues); + return createFile(context, targetName, null); } public static Uri createFile(Context context) { @@ -110,15 +120,6 @@ public class TemporaryFileProvider extends ContentProvider { return context.getContentResolver().update(uri, values, null, null); } - public static int cleanUp(Context context) { - Timber.d("Cleaning up temporary files…"); - return context.getContentResolver().delete( - CONTENT_URI, - TemporaryFileColumns.COLUMN_TIME + "< ?", - new String[]{Long.toString(System.currentTimeMillis() - Constants.TEMPFILE_TTL)} - ); - } - private class TemporaryStorageDatabase extends SQLiteOpenHelper { public TemporaryStorageDatabase(Context context) { @@ -247,9 +248,6 @@ public class TemporaryFileProvider extends ContentProvider { @Override public Uri insert(Uri uri, ContentValues values) { - if (!values.containsKey(TemporaryFileColumns.COLUMN_TIME)) { - values.put(TemporaryFileColumns.COLUMN_TIME, System.currentTimeMillis()); - } String uuid = UUID.randomUUID().toString(); values.put(TemporaryFileColumns.COLUMN_UUID, uuid); int insert = (int) db.getWritableDatabase().insert(TABLE_FILES, null, values); @@ -310,4 +308,34 @@ public class TemporaryFileProvider extends ContentProvider { return openFileHelper(uri, mode); } + public static void scheduleCleanupAfterTtl() { + OneTimeWorkRequest cleanupWork = new OneTimeWorkRequest.Builder(CleanupWorker.class) + .setInitialDelay(Constants.TEMPFILE_TTL, TimeUnit.MILLISECONDS).build(); + WorkManager.getInstance().enqueue(cleanupWork); + } + + public static void scheduleCleanupImmediately() { + OneTimeWorkRequest cleanupWork = new OneTimeWorkRequest.Builder(CleanupWorker.class).build(); + WorkManager workManager = WorkManager.getInstance(); + if (workManager != null) { // it's possible this is null, if this is called in onCreate of secondary processes + workManager.enqueue(cleanupWork); + } + } + + public static class CleanupWorker extends Worker { + @NonNull + @Override + public WorkerResult doWork() { + Timber.d("Cleaning up temporary files…"); + + ContentResolver contentResolver = getApplicationContext().getContentResolver(); + contentResolver.delete( + CONTENT_URI, + TemporaryFileColumns.COLUMN_TIME + " <= ?", + new String[]{Long.toString(System.currentTimeMillis() - Constants.TEMPFILE_TTL)} + ); + + return WorkerResult.SUCCESS; + } + } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ApiPendingIntentFactory.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ApiPendingIntentFactory.java index 18a467299..085299aaf 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ApiPendingIntentFactory.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ApiPendingIntentFactory.java @@ -17,6 +17,7 @@ package org.sufficientlysecure.keychain.remote; + import java.util.ArrayList; import android.app.PendingIntent; @@ -25,7 +26,6 @@ import android.content.Intent; import android.os.Build; import org.sufficientlysecure.keychain.pgp.DecryptVerifySecurityProblem; -import org.sufficientlysecure.keychain.provider.KeychainContract; import org.sufficientlysecure.keychain.remote.ui.RemoteBackupActivity; import org.sufficientlysecure.keychain.remote.ui.RemoteDisplayTransferCodeActivity; import org.sufficientlysecure.keychain.remote.ui.RemoteErrorActivity; @@ -92,8 +92,9 @@ public class ApiPendingIntentFactory { return createInternal(data, intent); } - PendingIntent createSelectPublicKeyPendingIntent(Intent data, long[] keyIdsArray, ArrayList missingEmails, - ArrayList duplicateEmails, boolean noUserIdsCheck) { + public PendingIntent createSelectPublicKeyPendingIntent(Intent data, long[] keyIdsArray, + ArrayList missingEmails, + ArrayList duplicateEmails, boolean noUserIdsCheck) { Intent intent = new Intent(mContext, RemoteSelectPubKeyActivity.class); intent.putExtra(RemoteSelectPubKeyActivity.EXTRA_SELECTED_MASTER_KEY_IDS, keyIdsArray); intent.putExtra(RemoteSelectPubKeyActivity.EXTRA_NO_USER_IDS_CHECK, noUserIdsCheck); @@ -103,7 +104,7 @@ public class ApiPendingIntentFactory { return createInternal(data, intent); } - PendingIntent createDeduplicatePendingIntent(String packageName, Intent data, ArrayList duplicateEmails) { + public PendingIntent createDeduplicatePendingIntent(String packageName, Intent data, ArrayList duplicateEmails) { Intent intent = new Intent(mContext, RemoteDeduplicateActivity.class); intent.putExtra(RemoteDeduplicateActivity.EXTRA_PACKAGE_NAME, packageName); @@ -121,7 +122,7 @@ public class ApiPendingIntentFactory { return createInternal(data, intent); } - PendingIntent createRequestKeyPermissionPendingIntent(Intent data, String packageName, long... masterKeyIds) { + public PendingIntent createRequestKeyPermissionPendingIntent(Intent data, String packageName, long... masterKeyIds) { Intent intent = new Intent(mContext, RequestKeyPermissionActivity.class); intent.putExtra(RequestKeyPermissionActivity.EXTRA_PACKAGE_NAME, packageName); intent.putExtra(RequestKeyPermissionActivity.EXTRA_REQUESTED_KEY_IDS, masterKeyIds); @@ -130,24 +131,23 @@ public class ApiPendingIntentFactory { } PendingIntent createShowKeyPendingIntent(Intent data, long masterKeyId) { - Intent intent = new Intent(mContext, ViewKeyActivity.class); - intent.setData(KeychainContract.KeyRings.buildGenericKeyRingUri(masterKeyId)); + Intent intent = ViewKeyActivity.getViewKeyActivityIntent(mContext, masterKeyId); return createInternal(data, intent); } - PendingIntent createSelectSignKeyIdLegacyPendingIntent(Intent data, String packageName, String preferredUserId) { + public PendingIntent createSelectSignKeyIdLegacyPendingIntent(Intent data, String packageName, + String preferredUserId) { Intent intent = new Intent(mContext, SelectSignKeyIdActivity.class); - intent.setData(KeychainContract.ApiApps.buildByPackageNameUri(packageName)); + intent.putExtra(SelectSignKeyIdActivity.EXTRA_PACKAGE_NAME, packageName); intent.putExtra(SelectSignKeyIdActivity.EXTRA_USER_ID, preferredUserId); return createInternal(data, intent); } - PendingIntent createSelectSignKeyIdPendingIntent(Intent data, String packageName, + public PendingIntent createSelectSignKeyIdPendingIntent(Intent data, String packageName, byte[] packageSignature, String preferredUserId, boolean showAutocryptHint) { Intent intent = new Intent(mContext, RemoteSelectIdKeyActivity.class); - intent.setData(KeychainContract.ApiApps.buildByPackageNameUri(packageName)); intent.putExtra(RemoteSelectIdKeyActivity.EXTRA_PACKAGE_NAME, packageName); intent.putExtra(RemoteSelectIdKeyActivity.EXTRA_PACKAGE_SIGNATURE, packageSignature); intent.putExtra(RemoteSelectIdKeyActivity.EXTRA_USER_ID, preferredUserId); @@ -156,9 +156,8 @@ public class ApiPendingIntentFactory { return createInternal(data, intent); } - PendingIntent createSelectAuthenticationKeyIdPendingIntent(Intent data, String packageName) { + public PendingIntent createSelectAuthenticationKeyIdPendingIntent(Intent data, String packageName) { Intent intent = new Intent(mContext, RemoteSelectAuthenticationKeyActivity.class); - intent.setData(KeychainContract.ApiApps.buildByPackageNameUri(packageName)); intent.putExtra(RemoteSelectAuthenticationKeyActivity.EXTRA_PACKAGE_NAME, packageName); return createInternal(data, intent); @@ -220,7 +219,7 @@ public class ApiPendingIntentFactory { } } - PendingIntent createRegisterPendingIntent(Intent data, String packageName, byte[] packageCertificate) { + public PendingIntent createRegisterPendingIntent(Intent data, String packageName, byte[] packageCertificate) { Intent intent = new Intent(mContext, RemoteRegisterActivity.class); intent.putExtra(RemoteRegisterActivity.EXTRA_PACKAGE_NAME, packageName); intent.putExtra(RemoteRegisterActivity.EXTRA_PACKAGE_SIGNATURE, packageCertificate); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ApiPermissionHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ApiPermissionHelper.java index fa6d04bbd..7069cc354 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ApiPermissionHelper.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ApiPermissionHelper.java @@ -35,7 +35,7 @@ import android.os.Binder; import org.openintents.openpgp.OpenPgpError; import org.openintents.openpgp.util.OpenPgpApi; import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.provider.ApiDataAccessObject; +import org.sufficientlysecure.keychain.daos.ApiAppDao; import timber.log.Timber; @@ -45,13 +45,13 @@ import timber.log.Timber; public class ApiPermissionHelper { private final Context mContext; - private final ApiDataAccessObject mApiDao; + private final ApiAppDao mApiAppDao; private PackageManager mPackageManager; - public ApiPermissionHelper(Context context, ApiDataAccessObject apiDao) { + public ApiPermissionHelper(Context context, ApiAppDao apiAppDao) { mContext = context; mPackageManager = context.getPackageManager(); - mApiDao = apiDao; + mApiAppDao = apiAppDao; } public static class WrongPackageCertificateException extends Exception { @@ -206,7 +206,7 @@ public class ApiPermissionHelper { public boolean isPackageAllowed(String packageName) throws WrongPackageCertificateException { Timber.d("isPackageAllowed packageName: " + packageName); - byte[] storedPackageCert = mApiDao.getApiAppCertificate(packageName); + byte[] storedPackageCert = mApiAppDao.getApiAppCertificate(packageName); boolean isKnownPackage = storedPackageCert != null; if (!isKnownPackage) { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/AppSettings.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/AppSettings.java deleted file mode 100644 index a5dcc24bd..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/AppSettings.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (C) 2017 Schürmann & Breitmoser GbR - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sufficientlysecure.keychain.remote; - -public class AppSettings { - private String mPackageName; - private byte[] mPackageCertificate; - - public AppSettings() { - - } - - public AppSettings(String packageName, byte[] packageSignature) { - super(); - this.mPackageName = packageName; - this.mPackageCertificate = packageSignature; - } - - public String getPackageName() { - return mPackageName; - } - - public void setPackageName(String packageName) { - this.mPackageName = packageName; - } - - public byte[] getPackageCertificate() { - return mPackageCertificate; - } - - public void setPackageCertificate(byte[] packageCertificate) { - this.mPackageCertificate = packageCertificate; - } - -} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/AutocryptInteractor.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/AutocryptInteractor.java index c981f5df5..6dfa1e143 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/AutocryptInteractor.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/AutocryptInteractor.java @@ -2,52 +2,64 @@ package org.sufficientlysecure.keychain.remote; import java.io.IOException; +import java.util.ArrayList; import java.util.Date; +import java.util.List; import android.content.Context; import android.support.annotation.Nullable; +import android.text.format.DateUtils; import org.openintents.openpgp.AutocryptPeerUpdate; import org.openintents.openpgp.AutocryptPeerUpdate.PreferEncrypt; import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.model.AutocryptPeer; +import org.sufficientlysecure.keychain.model.AutocryptPeer.AutocryptKeyStatus; +import org.sufficientlysecure.keychain.model.AutocryptPeer.GossipOrigin; import org.sufficientlysecure.keychain.operations.results.SaveKeyringResult; import org.sufficientlysecure.keychain.pgp.UncachedKeyRing; import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; -import org.sufficientlysecure.keychain.provider.AutocryptPeerDataAccessObject; -import org.sufficientlysecure.keychain.provider.KeyWritableRepository; +import org.sufficientlysecure.keychain.daos.AutocryptPeerDao; +import org.sufficientlysecure.keychain.daos.KeyWritableRepository; import timber.log.Timber; -class AutocryptInteractor { +public class AutocryptInteractor { + private static final long AUTOCRYPT_DISCOURAGE_THRESHOLD_MILLIS = 35 * DateUtils.DAY_IN_MILLIS; - private AutocryptPeerDataAccessObject autocryptPeerDao; + private AutocryptPeerDao autocryptPeerDao; private KeyWritableRepository keyWritableRepository; - public static AutocryptInteractor getInstance(Context context, AutocryptPeerDataAccessObject autocryptPeerentityDao) { + private final String packageName; + + public static AutocryptInteractor getInstance(Context context, String packageName) { + AutocryptPeerDao autocryptPeerDao = AutocryptPeerDao.getInstance(context); KeyWritableRepository keyWritableRepository = KeyWritableRepository.create(context); - return new AutocryptInteractor(autocryptPeerentityDao, keyWritableRepository); + return new AutocryptInteractor(autocryptPeerDao, keyWritableRepository, packageName); } - private AutocryptInteractor(AutocryptPeerDataAccessObject autocryptPeerDao, - KeyWritableRepository keyWritableRepository) { + private AutocryptInteractor(AutocryptPeerDao autocryptPeerDao, + KeyWritableRepository keyWritableRepository, String packageName) { this.autocryptPeerDao = autocryptPeerDao; this.keyWritableRepository = keyWritableRepository; + this.packageName = packageName; } void updateAutocryptPeerState(String autocryptPeerId, AutocryptPeerUpdate autocryptPeerUpdate) { + AutocryptPeer currentAutocryptPeer = autocryptPeerDao.getAutocryptPeer(packageName, autocryptPeerId); Date effectiveDate = autocryptPeerUpdate.getEffectiveDate(); // 1. If the message’s effective date is older than the peers[from-addr].autocrypt_timestamp value, then no changes are required, and the update process terminates. - Date lastSeenAutocrypt = autocryptPeerDao.getLastSeenKey(autocryptPeerId); - if (lastSeenAutocrypt != null && effectiveDate.compareTo(lastSeenAutocrypt) <= 0) { + Date lastSeenKey = currentAutocryptPeer != null ? currentAutocryptPeer.last_seen_key() : null; + if (lastSeenKey != null && effectiveDate.compareTo(lastSeenKey) <= 0) { return; } // 2. If the message’s effective date is more recent than peers[from-addr].last_seen then set peers[from-addr].last_seen to the message’s effective date. - Date lastSeen = autocryptPeerDao.getLastSeen(autocryptPeerId); + Date lastSeen = currentAutocryptPeer != null ? currentAutocryptPeer.last_seen() : null; if (lastSeen == null || effectiveDate.after(lastSeen)) { - autocryptPeerDao.updateLastSeen(autocryptPeerId, effectiveDate); + autocryptPeerDao.insertOrUpdateLastSeen(packageName, autocryptPeerId, effectiveDate); } // 3. If the Autocrypt header is unavailable, no further changes are required and the update process terminates. @@ -66,17 +78,18 @@ class AutocryptInteractor { // 6. Set peers[from-addr].prefer_encrypt to the corresponding prefer-encrypt value of the Autocrypt header. boolean isMutual = autocryptPeerUpdate.getPreferEncrypt() == PreferEncrypt.MUTUAL; - autocryptPeerDao.updateKey(autocryptPeerId, effectiveDate, newMasterKeyId, isMutual); + autocryptPeerDao.updateKey(packageName, autocryptPeerId, effectiveDate, newMasterKeyId, isMutual); } void updateAutocryptPeerGossipState(String autocryptPeerId, AutocryptPeerUpdate autocryptPeerUpdate) { + AutocryptPeer currentAutocryptPeer = autocryptPeerDao.getAutocryptPeer(packageName, autocryptPeerId); Date effectiveDate = autocryptPeerUpdate.getEffectiveDate(); // 1. If gossip-addr does not match any recipient in the mail’s To or Cc header, the update process terminates (i.e., header is ignored). // -> This should be taken care of in the mail client that sends us this data! // 2. If peers[gossip-addr].gossip_timestamp is more recent than the message’s effective date, then the update process terminates. - Date lastSeenGossip = autocryptPeerDao.getLastSeenGossip(autocryptPeerId); + Date lastSeenGossip = currentAutocryptPeer != null ? currentAutocryptPeer.gossip_last_seen_key() : null; if (lastSeenGossip != null && lastSeenGossip.after(effectiveDate)) { return; } @@ -94,7 +107,8 @@ class AutocryptInteractor { // 4. Set peers[gossip-addr].gossip_key to the value of the keydata attribute. Long newMasterKeyId = saveKeyringResult.savedMasterKeyId; - autocryptPeerDao.updateKeyGossipFromAutocrypt(autocryptPeerId, effectiveDate, newMasterKeyId); + autocryptPeerDao.updateKeyGossip(packageName, autocryptPeerId, effectiveDate, newMasterKeyId, + GossipOrigin.GOSSIP_HEADER); } @Nullable @@ -131,4 +145,100 @@ class AutocryptInteractor { } return uncachedKeyRing; } + + public List determineAutocryptRecommendations(String... autocryptIds) { + List result = new ArrayList<>(autocryptIds.length); + + for (AutocryptKeyStatus autocryptKeyStatus : autocryptPeerDao.getAutocryptKeyStatus(packageName, autocryptIds)) { + AutocryptRecommendationResult peerResult = determineAutocryptRecommendation(autocryptKeyStatus); + result.add(peerResult); + } + + return result; + } + + /** Determines Autocrypt "ui-recommendation", according to spec. + * See https://autocrypt.org/level1.html#recommendations-for-single-recipient-messages + */ + private AutocryptRecommendationResult determineAutocryptRecommendation(AutocryptKeyStatus autocryptKeyStatus) { + AutocryptRecommendationResult keyRecommendation = determineAutocryptKeyRecommendation(autocryptKeyStatus); + if (keyRecommendation != null) return keyRecommendation; + + AutocryptRecommendationResult gossipRecommendation = determineAutocryptGossipRecommendation(autocryptKeyStatus); + if (gossipRecommendation != null) return gossipRecommendation; + + return new AutocryptRecommendationResult(autocryptKeyStatus.autocryptPeer().identifier(), AutocryptState.DISABLE, null, false); + } + + @Nullable + private AutocryptRecommendationResult determineAutocryptKeyRecommendation(AutocryptKeyStatus autocryptKeyStatus) { + AutocryptPeer autocryptPeer = autocryptKeyStatus.autocryptPeer(); + + Long masterKeyId = autocryptPeer.master_key_id(); + boolean hasKey = masterKeyId != null; + boolean isRevoked = autocryptKeyStatus.isKeyRevoked(); + boolean isExpired = autocryptKeyStatus.isKeyExpired(); + if (!hasKey || isRevoked || isExpired) { + return null; + } + + Date lastSeen = autocryptPeer.last_seen(); + Date lastSeenKey = autocryptPeer.last_seen_key(); + boolean isVerified = autocryptKeyStatus.isKeyVerified(); + boolean isLastSeenOlderThanDiscourageTimespan = lastSeen != null && lastSeenKey != null && + lastSeenKey.getTime() < (lastSeen.getTime() - AUTOCRYPT_DISCOURAGE_THRESHOLD_MILLIS); + if (isLastSeenOlderThanDiscourageTimespan) { + return new AutocryptRecommendationResult(autocryptPeer.identifier(), AutocryptState.DISCOURAGED_OLD, masterKeyId, isVerified); + } + + boolean isMutual = autocryptPeer.is_mutual(); + if (isMutual) { + return new AutocryptRecommendationResult(autocryptPeer.identifier(), AutocryptState.MUTUAL, masterKeyId, isVerified); + } else { + return new AutocryptRecommendationResult(autocryptPeer.identifier(), AutocryptState.AVAILABLE, masterKeyId, isVerified); + } + } + + @Nullable + private AutocryptRecommendationResult determineAutocryptGossipRecommendation(AutocryptKeyStatus autocryptKeyStatus) { + boolean gossipHasKey = autocryptKeyStatus.hasGossipKey(); + boolean gossipIsRevoked = autocryptKeyStatus.isGossipKeyRevoked(); + boolean gossipIsExpired = autocryptKeyStatus.isGossipKeyExpired(); + boolean isVerified = autocryptKeyStatus.isGossipKeyVerified(); + + if (!gossipHasKey || gossipIsRevoked || gossipIsExpired) { + return null; + } + + Long masterKeyId = autocryptKeyStatus.autocryptPeer().gossip_master_key_id(); + return new AutocryptRecommendationResult(autocryptKeyStatus.autocryptPeer().identifier(), AutocryptState.DISCOURAGED_GOSSIP, masterKeyId, isVerified); + } + + public void updateKeyGossipFromSignature(String autocryptId, Date effectiveDate, long masterKeyId) { + autocryptPeerDao.updateKeyGossip(packageName, autocryptId, effectiveDate, masterKeyId, GossipOrigin.SIGNATURE); + } + + public void updateKeyGossipFromDedup(String autocryptId, long masterKeyId) { + autocryptPeerDao.updateKeyGossip(packageName, autocryptId, new Date(), masterKeyId, GossipOrigin.DEDUP); + } + + public static class AutocryptRecommendationResult { + public final String peerId; + public final Long masterKeyId; + public final AutocryptState autocryptState; + public final boolean isVerified; + + AutocryptRecommendationResult(String peerId, AutocryptState autocryptState, Long masterKeyId, + boolean isVerified) { + this.peerId = peerId; + this.autocryptState = autocryptState; + this.masterKeyId = masterKeyId; + this.isVerified = isVerified; + } + + } + + public enum AutocryptState { + DISABLE, DISCOURAGED_OLD, DISCOURAGED_GOSSIP, AVAILABLE, MUTUAL + } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/KeychainExternalProvider.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/KeychainExternalProvider.java index 2253e3e4d..85b6979c8 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/KeychainExternalProvider.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/KeychainExternalProvider.java @@ -23,6 +23,7 @@ import java.util.Arrays; import java.util.HashMap; import java.util.List; +import android.arch.persistence.db.SupportSQLiteDatabase; import android.content.ContentProvider; import android.content.ContentValues; import android.content.Context; @@ -37,44 +38,33 @@ import android.text.TextUtils; import org.sufficientlysecure.keychain.BuildConfig; import org.sufficientlysecure.keychain.Constants; -import org.sufficientlysecure.keychain.provider.ApiDataAccessObject; -import org.sufficientlysecure.keychain.provider.AutocryptPeerDataAccessObject; -import org.sufficientlysecure.keychain.provider.AutocryptPeerDataAccessObject.AutocryptRecommendationResult; -import org.sufficientlysecure.keychain.provider.AutocryptPeerDataAccessObject.AutocryptState; -import org.sufficientlysecure.keychain.provider.DatabaseNotifyManager; -import org.sufficientlysecure.keychain.provider.KeychainContract.ApiApps; +import org.sufficientlysecure.keychain.daos.ApiAppDao; import org.sufficientlysecure.keychain.provider.KeychainContract.Certs; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; import org.sufficientlysecure.keychain.provider.KeychainContract.Keys; import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets; -import org.sufficientlysecure.keychain.provider.KeychainDatabase; -import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables; +import org.sufficientlysecure.keychain.KeychainDatabase; +import org.sufficientlysecure.keychain.KeychainDatabase.Tables; import org.sufficientlysecure.keychain.provider.KeychainExternalContract; import org.sufficientlysecure.keychain.provider.KeychainExternalContract.AutocryptStatus; import org.sufficientlysecure.keychain.provider.KeychainExternalContract.EmailStatus; -import org.sufficientlysecure.keychain.provider.KeychainProvider; -import org.sufficientlysecure.keychain.provider.SimpleContentResolverInterface; -import org.sufficientlysecure.keychain.util.CloseDatabaseCursorFactory; +import org.sufficientlysecure.keychain.remote.AutocryptInteractor.AutocryptRecommendationResult; +import org.sufficientlysecure.keychain.remote.AutocryptInteractor.AutocryptState; import timber.log.Timber; -public class KeychainExternalProvider extends ContentProvider implements SimpleContentResolverInterface { +public class KeychainExternalProvider extends ContentProvider { private static final int EMAIL_STATUS = 101; private static final int AUTOCRYPT_STATUS = 201; private static final int AUTOCRYPT_STATUS_INTERNAL = 202; - private static final int API_APPS = 301; - private static final int API_APPS_BY_PACKAGE_NAME = 302; - public static final String TEMP_TABLE_QUERIED_ADDRESSES = "queried_addresses"; public static final String TEMP_TABLE_COLUMN_ADDRES = "address"; private UriMatcher uriMatcher; private ApiPermissionHelper apiPermissionHelper; - private KeychainProvider internalKeychainProvider; - private DatabaseNotifyManager databaseNotifyManager; /** @@ -111,10 +101,7 @@ public class KeychainExternalProvider extends ContentProvider implements SimpleC throw new NullPointerException("Context can't be null during onCreate!"); } - internalKeychainProvider = new KeychainProvider(); - internalKeychainProvider.attachInfo(context, null); - apiPermissionHelper = new ApiPermissionHelper(context, new ApiDataAccessObject(internalKeychainProvider)); - databaseNotifyManager = DatabaseNotifyManager.create(context); + apiPermissionHelper = new ApiPermissionHelper(context, ApiAppDao.getInstance(getContext())); return true; } @@ -127,13 +114,6 @@ public class KeychainExternalProvider extends ContentProvider implements SimpleC switch (match) { case EMAIL_STATUS: return EmailStatus.CONTENT_TYPE; - - case API_APPS: - return ApiApps.CONTENT_TYPE; - - case API_APPS_BY_PACKAGE_NAME: - return ApiApps.CONTENT_ITEM_TYPE; - default: throw new UnsupportedOperationException("Unknown uri: " + uri); } @@ -154,7 +134,7 @@ public class KeychainExternalProvider extends ContentProvider implements SimpleC String groupBy = null; - SQLiteDatabase db = new KeychainDatabase(getContext()).getReadableDatabase(); + SupportSQLiteDatabase db = KeychainDatabase.getInstance(getContext()).getReadableDatabase(); String callingPackageName = apiPermissionHelper.getCurrentCallingPackage(); @@ -169,7 +149,7 @@ public class KeychainExternalProvider extends ContentProvider implements SimpleC ContentValues cv = new ContentValues(); for (String address : selectionArgs) { cv.put(TEMP_TABLE_COLUMN_ADDRES, address); - db.insert(TEMP_TABLE_QUERIED_ADDRESSES, null, cv); + db.insert(TEMP_TABLE_QUERIED_ADDRESSES, SQLiteDatabase.CONFLICT_FAIL, cv); } HashMap projectionMap = new HashMap<>(); @@ -256,7 +236,7 @@ public class KeychainExternalProvider extends ContentProvider implements SimpleC ContentValues cv = new ContentValues(); for (String address : selectionArgs) { cv.put(TEMP_TABLE_COLUMN_ADDRES, address); - db.insert(TEMP_TABLE_QUERIED_ADDRESSES, null, cv); + db.insert(TEMP_TABLE_QUERIED_ADDRESSES, SQLiteDatabase.CONFLICT_FAIL, cv); } boolean isWildcardSelector = selectionArgs.length == 1 && selectionArgs[0].contains("%"); @@ -278,10 +258,9 @@ public class KeychainExternalProvider extends ContentProvider implements SimpleC throw new UnsupportedOperationException("Cannot wildcard-query autocrypt results!"); } if (!isWildcardSelector && queriesAutocryptResult) { - AutocryptPeerDataAccessObject autocryptPeerDao = - new AutocryptPeerDataAccessObject(internalKeychainProvider, callingPackageName, - databaseNotifyManager); - fillTempTableWithAutocryptRecommendations(db, autocryptPeerDao, selectionArgs); + AutocryptInteractor autocryptInteractor = + AutocryptInteractor.getInstance(getContext(), callingPackageName); + fillTempTableWithAutocryptRecommendations(db, autocryptInteractor, selectionArgs); } HashMap projectionMap = new HashMap<>(); @@ -321,8 +300,8 @@ public class KeychainExternalProvider extends ContentProvider implements SimpleC } qb.setStrict(true); - qb.setCursorFactory(new CloseDatabaseCursorFactory()); - Cursor cursor = qb.query(db, projection, null, null, groupBy, null, orderBy); + String query = qb.buildQuery(projection, null, groupBy, null, orderBy, null); + Cursor cursor = db.query(query); if (cursor != null) { // Tell the cursor what uri to watch, so it knows when its source data changes cursor.setNotificationUri(getContext().getContentResolver(), uri); @@ -337,15 +316,15 @@ public class KeychainExternalProvider extends ContentProvider implements SimpleC return cursor; } - private void fillTempTableWithAutocryptRecommendations(SQLiteDatabase db, - AutocryptPeerDataAccessObject autocryptPeerDao, String[] peerIds) { + private void fillTempTableWithAutocryptRecommendations(SupportSQLiteDatabase db, + AutocryptInteractor autocryptInteractor, String[] peerIds) { List autocryptStates = - autocryptPeerDao.determineAutocryptRecommendations(peerIds); + autocryptInteractor.determineAutocryptRecommendations(peerIds); fillTempTableWithAutocryptRecommendations(db, autocryptStates); } - private void fillTempTableWithAutocryptRecommendations(SQLiteDatabase db, + private void fillTempTableWithAutocryptRecommendations(SupportSQLiteDatabase db, List autocryptRecommendations) { ContentValues cv = new ContentValues(); for (AutocryptRecommendationResult peerResult : autocryptRecommendations) { @@ -359,12 +338,12 @@ public class KeychainExternalProvider extends ContentProvider implements SimpleC KeychainExternalContract.KEY_STATUS_UNVERIFIED); } - db.update(TEMP_TABLE_QUERIED_ADDRESSES, cv,TEMP_TABLE_COLUMN_ADDRES + "=?", + db.update(TEMP_TABLE_QUERIED_ADDRESSES, SQLiteDatabase.CONFLICT_IGNORE, cv,TEMP_TABLE_COLUMN_ADDRES + "=?", new String[] { peerResult.peerId }); } } - private void fillTempTableWithUidResult(SQLiteDatabase db, boolean isWildcardSelector) { + private void fillTempTableWithUidResult(SupportSQLiteDatabase db, boolean isWildcardSelector) { String cmpOperator = isWildcardSelector ? " LIKE " : " = "; long unixSeconds = System.currentTimeMillis() / 1000; db.execSQL("REPLACE INTO " + TEMP_TABLE_QUERIED_ADDRESSES + diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService.java index 6b1ea3ef6..b173e65bc 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService.java @@ -52,6 +52,7 @@ import org.openintents.openpgp.OpenPgpSignatureResult; import org.openintents.openpgp.OpenPgpSignatureResult.AutocryptPeerResult; import org.openintents.openpgp.util.OpenPgpApi; import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.model.SubKey.UnifiedKeyInfo; import org.sufficientlysecure.keychain.operations.BackupOperation; import org.sufficientlysecure.keychain.operations.results.DecryptVerifyResult; import org.sufficientlysecure.keychain.operations.results.ExportResult; @@ -66,15 +67,12 @@ import org.sufficientlysecure.keychain.pgp.PgpSignEncryptData; import org.sufficientlysecure.keychain.pgp.PgpSignEncryptOperation; import org.sufficientlysecure.keychain.pgp.Progressable; import org.sufficientlysecure.keychain.pgp.SecurityProblem; -import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; -import org.sufficientlysecure.keychain.provider.ApiDataAccessObject; -import org.sufficientlysecure.keychain.provider.AutocryptPeerDataAccessObject; -import org.sufficientlysecure.keychain.provider.CachedPublicKeyRing; -import org.sufficientlysecure.keychain.provider.KeyRepository; -import org.sufficientlysecure.keychain.provider.KeychainContract; -import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; +import org.sufficientlysecure.keychain.daos.ApiAppDao; +import org.sufficientlysecure.keychain.daos.AutocryptPeerDao; +import org.sufficientlysecure.keychain.daos.KeyRepository; +import org.sufficientlysecure.keychain.daos.KeyRepository.NotFoundException; import org.sufficientlysecure.keychain.provider.KeychainExternalContract.AutocryptStatus; -import org.sufficientlysecure.keychain.provider.OverriddenWarningsRepository; +import org.sufficientlysecure.keychain.daos.OverriddenWarningsDao; import org.sufficientlysecure.keychain.remote.OpenPgpServiceKeyIdExtractor.KeyIdResult; import org.sufficientlysecure.keychain.remote.OpenPgpServiceKeyIdExtractor.KeyIdResultStatus; import org.sufficientlysecure.keychain.service.BackupKeyringParcel; @@ -98,7 +96,7 @@ public class OpenPgpService extends Service { private ApiPermissionHelper mApiPermissionHelper; private KeyRepository mKeyRepository; - private ApiDataAccessObject mApiDao; + private ApiAppDao mApiAppDao; private OpenPgpServiceKeyIdExtractor mKeyIdExtractor; private ApiPendingIntentFactory mApiPendingIntentFactory; @@ -106,8 +104,8 @@ public class OpenPgpService extends Service { public void onCreate() { super.onCreate(); mKeyRepository = KeyRepository.create(this); - mApiDao = new ApiDataAccessObject(this); - mApiPermissionHelper = new ApiPermissionHelper(this, mApiDao); + mApiAppDao = ApiAppDao.getInstance(this); + mApiPermissionHelper = new ApiPermissionHelper(this, mApiAppDao); mApiPendingIntentFactory = new ApiPendingIntentFactory(getBaseContext()); mKeyIdExtractor = OpenPgpServiceKeyIdExtractor.getInstance(getContentResolver(), mApiPendingIntentFactory); } @@ -140,9 +138,9 @@ public class OpenPgpService extends Service { // get first usable subkey capable of signing try { - long signSubKeyId = mKeyRepository.getCachedPublicKeyRing(signKeyId).getSecretSignId(); + long signSubKeyId = mKeyRepository.getSecretSignId(signKeyId); pgpData.setSignatureSubKeyId(signSubKeyId); - } catch (PgpKeyNotFoundException e) { + } catch (NotFoundException e) { throw new Exception("signing subkey not found!", e); } } @@ -231,7 +229,7 @@ public class OpenPgpService extends Service { if (signKeyId == Constants.key.none) { throw new Exception("No signing key given"); } - long signSubKeyId = mKeyRepository.getCachedPublicKeyRing(signKeyId).getSecretSignId(); + long signSubKeyId = mKeyRepository.getSecretSignId(signKeyId); pgpData.setSignatureMasterKeyId(signKeyId) .setSignatureSubKeyId(signSubKeyId) @@ -472,7 +470,7 @@ public class OpenPgpService extends Service { SecurityProblem prioritySecurityProblem = securityProblem.getPrioritySecurityProblem(); if (prioritySecurityProblem.isIdentifiable()) { String identifier = prioritySecurityProblem.getIdentifier(); - boolean isOverridden = OverriddenWarningsRepository.createOverriddenWarningsRepository(this) + boolean isOverridden = OverriddenWarningsDao.create(this) .isWarningOverridden(identifier); result.putExtra(OpenPgpApi.RESULT_OVERRIDE_CRYPTO_WARNING, isOverridden); } @@ -585,8 +583,8 @@ public class OpenPgpService extends Service { return signatureResult; } - AutocryptPeerDataAccessObject autocryptPeerentityDao = new AutocryptPeerDataAccessObject(getBaseContext(), - mApiPermissionHelper.getCurrentCallingPackage()); + AutocryptPeerDao autocryptPeerentityDao = + AutocryptPeerDao.getInstance(getBaseContext()); Long autocryptPeerMasterKeyId = autocryptPeerentityDao.getMasterKeyIdForAutocryptPeer(autocryptPeerId); long masterKeyId = signatureResult.getKeyId(); @@ -596,7 +594,9 @@ public class OpenPgpService extends Service { if (effectiveTime.after(now)) { effectiveTime = now; } - autocryptPeerentityDao.updateKeyGossipFromSignature(autocryptPeerId, effectiveTime, masterKeyId); + AutocryptInteractor autocryptInteractor = + AutocryptInteractor.getInstance(this, mApiPermissionHelper.getCurrentCallingPackage()); + autocryptInteractor.updateKeyGossipFromSignature(autocryptPeerId, effectiveTime, masterKeyId); return signatureResult.withAutocryptPeerResult(AutocryptPeerResult.NEW); } else if (masterKeyId == autocryptPeerMasterKeyId) { return signatureResult.withAutocryptPeerResult(AutocryptPeerResult.OK); @@ -626,10 +626,8 @@ public class OpenPgpService extends Service { } try { - // try to find key, throws NotFoundException if not in db! CanonicalizedPublicKeyRing keyRing = - mKeyRepository.getCanonicalizedPublicKeyRing( - KeyRings.buildUnifiedKeyRingsFindBySubkeyUri(masterKeyId)); + mKeyRepository.getCanonicalizedPublicKeyRing(masterKeyId); Intent result = new Intent(); result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS); @@ -744,17 +742,16 @@ public class OpenPgpService extends Service { result.putExtra(OpenPgpApi.RESULT_SIGN_KEY_ID, signKeyId); if (signKeyId != Constants.key.none) { - try { - CachedPublicKeyRing cachedPublicKeyRing = mKeyRepository.getCachedPublicKeyRing(signKeyId); - String userId = cachedPublicKeyRing.getPrimaryUserId(); - long creationTime = cachedPublicKeyRing.getCreationTime() * 1000; - - result.putExtra(OpenPgpApi.RESULT_PRIMARY_USER_ID, userId); - result.putExtra(OpenPgpApi.RESULT_KEY_CREATION_TIME, creationTime); - } catch (PgpKeyNotFoundException e) { - Timber.e(e, "Error loading key info"); - return createErrorResultIntent(OpenPgpError.GENERIC_ERROR, e.getMessage()); + UnifiedKeyInfo unifiedKeyInfo = mKeyRepository.getUnifiedKeyInfo(signKeyId); + if (unifiedKeyInfo == null) { + Timber.e("Error loading key info"); + return createErrorResultIntent(OpenPgpError.GENERIC_ERROR, "Signing key not found!"); } + String userId = unifiedKeyInfo.user_id(); + long creationTime = unifiedKeyInfo.creation() * 1000; + + result.putExtra(OpenPgpApi.RESULT_PRIMARY_USER_ID, userId); + result.putExtra(OpenPgpApi.RESULT_KEY_CREATION_TIME, creationTime); } return result; @@ -860,9 +857,8 @@ public class OpenPgpService extends Service { private Intent updateAutocryptPeerImpl(Intent data) { try { - AutocryptPeerDataAccessObject autocryptPeerDao = new AutocryptPeerDataAccessObject(getBaseContext(), - mApiPermissionHelper.getCurrentCallingPackage()); - AutocryptInteractor autocryptInteractor = AutocryptInteractor.getInstance(getBaseContext(), autocryptPeerDao); + AutocryptInteractor autocryptInteractor = AutocryptInteractor.getInstance( + getBaseContext(), mApiPermissionHelper.getCurrentCallingPackage()); if (data.hasExtra(OpenPgpApi.EXTRA_AUTOCRYPT_PEER_ID) && data.hasExtra(OpenPgpApi.EXTRA_AUTOCRYPT_PEER_UPDATE)) { @@ -915,8 +911,7 @@ public class OpenPgpService extends Service { private HashSet getAllowedKeyIds() { String currentPkg = mApiPermissionHelper.getCurrentCallingPackage(); - return mApiDao.getAllowedKeyIdsForApp( - KeychainContract.ApiAllowedKeys.buildBaseUri(currentPkg)); + return mApiAppDao.getAllowedKeyIdsForApp(currentPkg); } /** diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/PackageUninstallReceiver.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/PackageUninstallReceiver.java index 338807ec0..2c1013ac9 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/PackageUninstallReceiver.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/PackageUninstallReceiver.java @@ -17,12 +17,13 @@ package org.sufficientlysecure.keychain.remote; + import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.net.Uri; -import org.sufficientlysecure.keychain.provider.KeychainContract; +import org.sufficientlysecure.keychain.daos.ApiAppDao; public class PackageUninstallReceiver extends BroadcastReceiver { @@ -34,8 +35,9 @@ public class PackageUninstallReceiver extends BroadcastReceiver { return; } String packageName = uri.getEncodedSchemeSpecificPart(); - Uri appUri = KeychainContract.ApiApps.buildByPackageNameUri(packageName); - context.getContentResolver().delete(appUri, null, null); + + ApiAppDao apiAppDao = ApiAppDao.getInstance(context); + apiAppDao.deleteApiApp(packageName); } } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/SshAuthenticationService.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/SshAuthenticationService.java index 57dab705e..590e309f1 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/SshAuthenticationService.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/SshAuthenticationService.java @@ -17,11 +17,19 @@ package org.sufficientlysecure.keychain.remote; + +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.util.Collections; +import java.util.Date; +import java.util.HashSet; +import java.util.List; + import android.app.PendingIntent; import android.app.Service; import android.content.Intent; import android.os.IBinder; -import android.util.Log; + import org.bouncycastle.bcpg.HashAlgorithmTags; import org.bouncycastle.bcpg.PublicKeyAlgorithmTags; import org.openintents.ssh.authentication.ISshAuthenticationService; @@ -32,15 +40,15 @@ import org.openintents.ssh.authentication.response.PublicKeyResponse; import org.openintents.ssh.authentication.response.SigningResponse; import org.openintents.ssh.authentication.response.SshPublicKeyResponse; import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.model.SubKey.UnifiedKeyInfo; import org.sufficientlysecure.keychain.operations.results.OperationResult.LogEntryParcel; import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKey; import org.sufficientlysecure.keychain.pgp.SshPublicKey; import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; -import org.sufficientlysecure.keychain.provider.ApiDataAccessObject; -import org.sufficientlysecure.keychain.provider.CachedPublicKeyRing; -import org.sufficientlysecure.keychain.provider.KeyRepository; -import org.sufficientlysecure.keychain.provider.KeychainContract; +import org.sufficientlysecure.keychain.daos.ApiAppDao; +import org.sufficientlysecure.keychain.daos.KeyRepository; +import org.sufficientlysecure.keychain.daos.KeyRepository.NotFoundException; import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; import org.sufficientlysecure.keychain.ssh.AuthenticationData; @@ -50,20 +58,11 @@ import org.sufficientlysecure.keychain.ssh.AuthenticationResult; import org.sufficientlysecure.keychain.ssh.signature.SshSignatureConverter; import timber.log.Timber; -import java.security.NoSuchAlgorithmException; -import java.security.PublicKey; -import java.util.Collections; -import java.util.Date; -import java.util.HashSet; -import java.util.List; - public class SshAuthenticationService extends Service { - private static final String TAG = "SshAuthService"; - private ApiPermissionHelper mApiPermissionHelper; private KeyRepository mKeyRepository; - private ApiDataAccessObject mApiDao; + private ApiAppDao mApiAppDao; private ApiPendingIntentFactory mApiPendingIntentFactory; private static final List SUPPORTED_VERSIONS = Collections.unmodifiableList(Collections.singletonList(1)); @@ -74,9 +73,9 @@ public class SshAuthenticationService extends Service { @Override public void onCreate() { super.onCreate(); - mApiPermissionHelper = new ApiPermissionHelper(this, new ApiDataAccessObject(this)); + mApiPermissionHelper = new ApiPermissionHelper(this, ApiAppDao.getInstance(this)); mKeyRepository = KeyRepository.create(this); - mApiDao = new ApiDataAccessObject(this); + mApiAppDao = ApiAppDao.getInstance(this); mApiPendingIntentFactory = new ApiPendingIntentFactory(getBaseContext()); } @@ -144,23 +143,18 @@ public class SshAuthenticationService extends Service { AuthenticationData.Builder authData = AuthenticationData.builder(); authData.setAuthenticationMasterKeyId(masterKeyId); - CachedPublicKeyRing cachedPublicKeyRing = mKeyRepository.getCachedPublicKeyRing(masterKeyId); - long authSubKeyId; int authSubKeyAlgorithm; String authSubKeyCurveOid = null; try { // get first usable subkey capable of authentication - authSubKeyId = cachedPublicKeyRing.getSecretAuthenticationId(); + authSubKeyId = mKeyRepository.getSecretAuthenticationId(masterKeyId); // needed for encoding the resulting signature authSubKeyAlgorithm = getPublicKey(masterKeyId).getAlgorithm(); if (authSubKeyAlgorithm == PublicKeyAlgorithmTags.ECDSA) { authSubKeyCurveOid = getPublicKey(masterKeyId).getCurveOid(); } - } catch (PgpKeyNotFoundException e) { - return createExceptionErrorResult(SshAuthenticationApiError.NO_AUTH_KEY, - "authentication key for master key id not found in keychain", e); - } catch (KeyRepository.NotFoundException e) { + } catch (NotFoundException e) { return createExceptionErrorResult(SshAuthenticationApiError.NO_SUCH_KEY, "Key for master key id not found", e); } @@ -272,7 +266,7 @@ public class SshAuthenticationService extends Service { try { description = getDescription(masterKeyId); - } catch (PgpKeyNotFoundException e) { + } catch (NotFoundException e) { return createExceptionErrorResult(SshAuthenticationApiError.NO_SUCH_KEY, "Could not create description", e); } @@ -372,21 +366,21 @@ public class SshAuthenticationService extends Service { return new SshPublicKeyResponse(sshPublicKeyBlob).toIntent(); } - private CanonicalizedPublicKey getPublicKey(long masterKeyId) - throws PgpKeyNotFoundException, KeyRepository.NotFoundException { + private CanonicalizedPublicKey getPublicKey(long masterKeyId) throws NotFoundException { KeyRepository keyRepository = KeyRepository.create(getApplicationContext()); - long authSubKeyId = keyRepository.getCachedPublicKeyRing(masterKeyId) - .getAuthenticationId(); - return keyRepository.getCanonicalizedPublicKeyRing(masterKeyId) - .getPublicKey(authSubKeyId); + UnifiedKeyInfo unifiedKeyInfo = keyRepository.getUnifiedKeyInfo(masterKeyId); + if (unifiedKeyInfo == null) { + throw new NotFoundException(); + } + return keyRepository.getCanonicalizedPublicKeyRing(masterKeyId).getPublicKey(unifiedKeyInfo.has_auth_key_int()); } - private String getDescription(long masterKeyId) throws PgpKeyNotFoundException { - CachedPublicKeyRing cachedPublicKeyRing = mKeyRepository.getCachedPublicKeyRing(masterKeyId); + private String getDescription(long masterKeyId) throws NotFoundException { + UnifiedKeyInfo unifiedKeyInfo = mKeyRepository.getUnifiedKeyInfo(masterKeyId); String description = ""; - long authSubKeyId = cachedPublicKeyRing.getSecretAuthenticationId(); - description += cachedPublicKeyRing.getPrimaryUserId(); + long authSubKeyId = mKeyRepository.getSecretAuthenticationId(masterKeyId); + description += unifiedKeyInfo.user_id(); description += " (" + Long.toHexString(authSubKeyId) + ")"; return description; @@ -394,7 +388,7 @@ public class SshAuthenticationService extends Service { private HashSet getAllowedKeyIds() { String currentPkg = mApiPermissionHelper.getCurrentCallingPackage(); - return mApiDao.getAllowedKeyIdsForApp(KeychainContract.ApiAllowedKeys.buildBaseUri(currentPkg)); + return mApiAppDao.getAllowedKeyIdsForApp(currentPkg); } /** diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppSettingsActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppSettingsActivity.java index a54da0fb7..1d43a0b70 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppSettingsActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppSettingsActivity.java @@ -26,7 +26,6 @@ import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.graphics.drawable.Drawable; -import android.net.Uri; import android.os.Bundle; import android.support.v4.app.FragmentManager; import android.view.Menu; @@ -36,24 +35,26 @@ import android.widget.TextView; import org.bouncycastle.util.encoders.Hex; import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.model.ApiApp; import org.sufficientlysecure.keychain.operations.results.OperationResult; -import org.sufficientlysecure.keychain.provider.ApiDataAccessObject; -import org.sufficientlysecure.keychain.provider.KeychainContract; -import org.sufficientlysecure.keychain.remote.AppSettings; +import org.sufficientlysecure.keychain.daos.ApiAppDao; import org.sufficientlysecure.keychain.ui.base.BaseActivity; import org.sufficientlysecure.keychain.ui.dialog.AdvancedAppSettingsDialogFragment; import timber.log.Timber; public class AppSettingsActivity extends BaseActivity { - private Uri mAppUri; + public static final String EXTRA_PACKAGE_NAME = "package_name"; + + private String packageName; private TextView mAppNameView; private ImageView mAppIconView; // model - AppSettings mAppSettings; + ApiApp mApiApp; + private ApiAppDao apiAppDao; @Override protected void onCreate(Bundle savedInstanceState) { @@ -68,15 +69,16 @@ public class AppSettingsActivity extends BaseActivity { setTitle(null); Intent intent = getIntent(); - mAppUri = intent.getData(); - if (mAppUri == null) { - Timber.e("Intent data missing. Should be Uri of app!"); + packageName = intent.getStringExtra(EXTRA_PACKAGE_NAME); + if (packageName == null) { + Timber.e("Required extra package_name missing!"); finish(); return; } - Timber.d("uri: %s", mAppUri); - loadData(savedInstanceState, mAppUri); + apiAppDao = ApiAppDao.getInstance(this); + + loadData(savedInstanceState); } private void save() { @@ -138,7 +140,7 @@ public class AppSettingsActivity extends BaseActivity { // advanced info: package certificate SHA-256 try { MessageDigest md = MessageDigest.getInstance("SHA-256"); - md.update(mAppSettings.getPackageCertificate()); + md.update(mApiApp.package_signature()); byte[] digest = md.digest(); certificate = new String(Hex.encode(digest)); } catch (NoSuchAlgorithmException e) { @@ -146,7 +148,7 @@ public class AppSettingsActivity extends BaseActivity { } AdvancedAppSettingsDialogFragment dialogFragment = - AdvancedAppSettingsDialogFragment.newInstance(mAppSettings.getPackageName(), certificate); + AdvancedAppSettingsDialogFragment.newInstance(mApiApp.package_name(), certificate); dialogFragment.show(getSupportFragmentManager(), "advancedDialog"); } @@ -155,7 +157,7 @@ public class AppSettingsActivity extends BaseActivity { Intent i; PackageManager manager = getPackageManager(); try { - i = manager.getLaunchIntentForPackage(mAppSettings.getPackageName()); + i = manager.getLaunchIntentForPackage(mApiApp.package_name()); if (i == null) throw new PackageManager.NameNotFoundException(); // start like the Android launcher would do @@ -167,31 +169,29 @@ public class AppSettingsActivity extends BaseActivity { } } - private void loadData(Bundle savedInstanceState, Uri appUri) { - mAppSettings = new ApiDataAccessObject(this).getApiAppSettings(appUri); + private void loadData(Bundle savedInstanceState) { + mApiApp = apiAppDao.getApiApp(packageName); // get application name and icon from package manager String appName; Drawable appIcon = null; PackageManager pm = getApplicationContext().getPackageManager(); try { - ApplicationInfo ai = pm.getApplicationInfo(mAppSettings.getPackageName(), 0); + ApplicationInfo ai = pm.getApplicationInfo(mApiApp.package_name(), 0); appName = (String) pm.getApplicationLabel(ai); appIcon = pm.getApplicationIcon(ai); } catch (PackageManager.NameNotFoundException e) { // fallback - appName = mAppSettings.getPackageName(); + appName = mApiApp.package_name(); } mAppNameView.setText(appName); mAppIconView.setImageDrawable(appIcon); - Uri allowedKeysUri = appUri.buildUpon().appendPath(KeychainContract.PATH_ALLOWED_KEYS).build(); - Timber.d("allowedKeysUri: " + allowedKeysUri); - startListFragments(savedInstanceState, allowedKeysUri); + startListFragments(savedInstanceState); } - private void startListFragments(Bundle savedInstanceState, Uri allowedKeysUri) { + private void startListFragments(Bundle savedInstanceState) { // However, if we're being restored from a previous state, // then we don't need to do anything and should return or else // we could end up with overlapping fragments. @@ -199,7 +199,8 @@ public class AppSettingsActivity extends BaseActivity { return; } - AppSettingsAllowedKeysListFragment allowedKeysFragment = AppSettingsAllowedKeysListFragment.newInstance(allowedKeysUri); + // Create an instance of the fragments + AppSettingsAllowedKeysListFragment allowedKeysFragment = AppSettingsAllowedKeysListFragment.newInstance(packageName); // Add the fragment to the 'fragment_container' FrameLayout // NOTE: We use commitAllowingStateLoss() to prevent weird crashes! getSupportFragmentManager().beginTransaction() @@ -210,9 +211,7 @@ public class AppSettingsActivity extends BaseActivity { } private void revokeAccess() { - if (getContentResolver().delete(mAppUri, null, null) <= 0) { - throw new RuntimeException(); - } + apiAppDao.deleteApiApp(packageName); finish(); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppSettingsAllowedKeysListFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppSettingsAllowedKeysListFragment.java index fd4f37525..e85806428 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppSettingsAllowedKeysListFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppSettingsAllowedKeysListFragment.java @@ -18,48 +18,36 @@ package org.sufficientlysecure.keychain.remote.ui; +import java.util.List; import java.util.Set; -import android.content.OperationApplicationException; -import android.database.Cursor; -import android.net.Uri; +import android.arch.lifecycle.LiveData; +import android.arch.lifecycle.ViewModelProviders; import android.os.Bundle; -import android.os.RemoteException; -import android.support.v4.app.LoaderManager; -import android.support.v4.content.CursorLoader; -import android.support.v4.content.Loader; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ListView; +import android.support.v7.widget.LinearLayoutManager; import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.compatibility.ListFragmentWorkaround; -import org.sufficientlysecure.keychain.provider.ApiDataAccessObject; -import org.sufficientlysecure.keychain.provider.KeychainContract; -import org.sufficientlysecure.keychain.ui.adapter.KeyAdapter; -import org.sufficientlysecure.keychain.ui.adapter.KeySelectableAdapter; -import org.sufficientlysecure.keychain.ui.widget.FixedListView; -import timber.log.Timber; +import org.sufficientlysecure.keychain.model.SubKey.UnifiedKeyInfo; +import org.sufficientlysecure.keychain.daos.ApiAppDao; +import org.sufficientlysecure.keychain.daos.KeyRepository; +import org.sufficientlysecure.keychain.ui.adapter.KeyChoiceAdapter; +import org.sufficientlysecure.keychain.ui.base.RecyclerFragment; +import org.sufficientlysecure.keychain.ui.keyview.GenericViewModel; -public class AppSettingsAllowedKeysListFragment extends ListFragmentWorkaround implements LoaderManager.LoaderCallbacks { - private static final String ARG_DATA_URI = "uri"; +public class AppSettingsAllowedKeysListFragment extends RecyclerFragment { + private static final String ARG_PACKAGE_NAME = "package_name"; - private KeySelectableAdapter mAdapter; - private ApiDataAccessObject mApiDao; + private KeyChoiceAdapter keyChoiceAdapter; + private ApiAppDao apiAppDao; - private Uri mDataUri; + private String packageName; - /** - * Creates new instance of this fragment - */ - public static AppSettingsAllowedKeysListFragment newInstance(Uri dataUri) { + public static AppSettingsAllowedKeysListFragment newInstance(String packageName) { AppSettingsAllowedKeysListFragment frag = new AppSettingsAllowedKeysListFragment(); + Bundle args = new Bundle(); - - args.putParcelable(ARG_DATA_URI, dataUri); - + args.putString(ARG_PACKAGE_NAME, packageName); frag.setArguments(args); return frag; @@ -69,112 +57,48 @@ public class AppSettingsAllowedKeysListFragment extends ListFragmentWorkaround i public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - mApiDao = new ApiDataAccessObject(getActivity()); + apiAppDao = ApiAppDao.getInstance(getActivity()); } - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - View layout = super.onCreateView(inflater, container, savedInstanceState); - ListView lv = layout.findViewById(android.R.id.list); - ViewGroup parent = (ViewGroup) lv.getParent(); - - /* - * http://stackoverflow.com/a/15880684 - * Remove ListView and add FixedListView in its place. - * This is done here programatically to be still able to use the progressBar of ListFragment. - * - * We want FixedListView to be able to put this ListFragment inside a ScrollView - */ - int lvIndex = parent.indexOfChild(lv); - parent.removeViewAt(lvIndex); - FixedListView newLv = new FixedListView(getActivity()); - newLv.setId(android.R.id.list); - parent.addView(newLv, lvIndex, lv.getLayoutParams()); - return layout; - } - - /** - * Define Adapter and Loader on create of Activity - */ @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - mDataUri = getArguments().getParcelable(ARG_DATA_URI); + packageName = getArguments().getString(ARG_PACKAGE_NAME); // Give some text to display if there is no data. In a real // application this would come from a resource. setEmptyText(getString(R.string.list_empty)); - Set checked = mApiDao.getAllowedKeyIdsForApp(mDataUri); - mAdapter = new KeySelectableAdapter(getActivity(), null, 0, checked); - setListAdapter(mAdapter); - getListView().setOnItemClickListener(mAdapter); + getRecyclerView().setLayoutManager(new LinearLayoutManager(requireContext())); // Start out with a progress indicator. - setListShown(false); - - // Prepare the loader. Either re-connect with an existing one, - // or start a new one. - getLoaderManager().initLoader(0, null, this); + hideList(false); + KeyRepository keyRepository = KeyRepository.create(requireContext()); + GenericViewModel viewModel = ViewModelProviders.of(this).get(GenericViewModel.class); + LiveData> liveData = + viewModel.getGenericLiveData(requireContext(), keyRepository::getAllUnifiedKeyInfoWithSecret); + liveData.observe(this, this::onLoadUnifiedKeyData); } - /** Returns all selected master key ids. */ - public Set getSelectedMasterKeyIds() { - return mAdapter.getSelectedMasterKeyIds(); - } - - /** Returns all selected user ids. - public String[] getSelectedUserIds() { - Vector userIds = new Vector<>(); - for (int i = 0; i < getListView().getCount(); ++i) { - if (getListView().isItemChecked(i)) { - userIds.add(mAdapter.getUserId(i)); - } - } - - // make empty array to not return null - String userIdArray[] = new String[0]; - return userIds.toArray(userIdArray); - } */ public void saveAllowedKeys() { - try { - mApiDao.saveAllowedKeyIdsForApp(mDataUri, getSelectedMasterKeyIds()); - } catch (RemoteException | OperationApplicationException e) { - Timber.e(e, "Problem saving allowed key ids!"); - } + Set longs = keyChoiceAdapter.getSelectionIds(); + apiAppDao.saveAllowedKeyIdsForApp(packageName, longs); } - @Override - public Loader onCreateLoader(int loaderId, Bundle data) { - Uri baseUri = KeychainContract.KeyRings.buildUnifiedKeyRingsUri(); - String where = KeychainContract.KeyRings.HAS_ANY_SECRET + " = 1"; - - return new CursorLoader(getActivity(), baseUri, KeyAdapter.PROJECTION, where, null, null); - } - - @Override - public void onLoadFinished(Loader loader, Cursor data) { - // Swap the new cursor in. (The framework will take care of closing the - // old cursor once we return.) - mAdapter.swapCursor(data); - - // The list should now be shown. - if (isResumed()) { - setListShown(true); + public void onLoadUnifiedKeyData(List data) { + if (keyChoiceAdapter == null) { + keyChoiceAdapter = KeyChoiceAdapter.createMultiChoiceAdapter(requireContext(), data, null); + setAdapter(keyChoiceAdapter); + Set checkedIds = apiAppDao.getAllowedKeyIdsForApp(packageName); + keyChoiceAdapter.setSelectionByIds(checkedIds); } else { - setListShownNoAnimation(true); + keyChoiceAdapter.setUnifiedKeyInfoItems(data); } - } - @Override - public void onLoaderReset(Loader loader) { - // This is called when the last Cursor provided to onLoadFinished() - // above is about to be closed. We need to make sure we are no - // longer using it. - mAdapter.swapCursor(null); + boolean animateShowList = !isResumed(); + showList(animateShowList); } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppSettingsHeaderFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppSettingsHeaderFragment.java deleted file mode 100644 index dc9b4d487..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppSettingsHeaderFragment.java +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright (C) 2017 Schürmann & Breitmoser GbR - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sufficientlysecure.keychain.remote.ui; - -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; -import android.content.pm.PackageManager.NameNotFoundException; -import android.graphics.drawable.Drawable; -import android.os.Bundle; -import android.support.v4.app.Fragment; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.TextView; - -import org.bouncycastle.util.encoders.Hex; -import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.remote.AppSettings; -import timber.log.Timber; - -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; - -public class AppSettingsHeaderFragment extends Fragment { - - // model - private AppSettings mAppSettings; - - // view - private TextView mAppNameView; - private ImageView mAppIconView; - private TextView mPackageName; - private TextView mPackageCertificate; - - public AppSettings getAppSettings() { - return mAppSettings; - } - - public void setAppSettings(AppSettings appSettings) { - this.mAppSettings = appSettings; - updateView(appSettings); - } - - /** - * Inflate the layout for this fragment - */ - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.api_app_settings_fragment, container, false); - mAppNameView = view.findViewById(R.id.api_app_settings_app_name); - mAppIconView = view.findViewById(R.id.api_app_settings_app_icon); - mPackageName = view.findViewById(R.id.api_app_settings_package_name); - mPackageCertificate = view.findViewById(R.id.api_app_settings_package_certificate); - return view; - } - - private void updateView(AppSettings appSettings) { - // get application name and icon from package manager - String appName; - Drawable appIcon = null; - PackageManager pm = getActivity().getApplicationContext().getPackageManager(); - try { - ApplicationInfo ai = pm.getApplicationInfo(appSettings.getPackageName(), 0); - - appName = (String) pm.getApplicationLabel(ai); - appIcon = pm.getApplicationIcon(ai); - } catch (NameNotFoundException e) { - // fallback - appName = appSettings.getPackageName(); - } - mAppNameView.setText(appName); - mAppIconView.setImageDrawable(appIcon); - - // advanced info: package name - mPackageName.setText(appSettings.getPackageName()); - - // advanced info: package signature SHA-256 - try { - MessageDigest md = MessageDigest.getInstance("SHA-256"); - md.update(appSettings.getPackageCertificate()); - byte[] digest = md.digest(); - String signature = new String(Hex.encode(digest)); - - mPackageCertificate.setText(signature); - } catch (NoSuchAlgorithmException e) { - Timber.e(e, "Should not happen!"); - } - } - - -} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppsListFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppsListFragment.java index 6410d8f10..13406266e 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppsListFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppsListFragment.java @@ -17,87 +17,77 @@ package org.sufficientlysecure.keychain.remote.ui; + +import java.util.List; + +import android.arch.lifecycle.LiveData; +import android.arch.lifecycle.ViewModel; +import android.arch.lifecycle.ViewModelProviders; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; -import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; -import android.content.res.Resources; -import android.database.Cursor; -import android.database.CursorJoiner; -import android.database.MatrixCursor; import android.net.Uri; import android.os.Bundle; -import android.support.v4.app.ListFragment; -import android.support.v4.app.LoaderManager; -import android.support.v4.content.CursorLoader; -import android.support.v4.content.Loader; -import android.support.v4.widget.CursorAdapter; +import android.support.annotation.NonNull; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.RecyclerView.Adapter; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.AdapterView.OnItemClickListener; import android.widget.ImageView; import android.widget.TextView; import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.provider.KeychainContract; -import org.sufficientlysecure.keychain.provider.KeychainContract.ApiApps; +import org.sufficientlysecure.keychain.livedata.ApiAppsLiveData; +import org.sufficientlysecure.keychain.livedata.ApiAppsLiveData.ListedApp; +import org.sufficientlysecure.keychain.remote.ui.AppsListFragment.ApiAppAdapter; +import org.sufficientlysecure.keychain.ui.base.RecyclerFragment; import timber.log.Timber; -public class AppsListFragment extends ListFragment implements - LoaderManager.LoaderCallbacks, OnItemClickListener { - - AppsAdapter mAdapter; +public class AppsListFragment extends RecyclerFragment { + private ApiAppAdapter adapter; @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - getListView().setOnItemClickListener(this); - - // NOTE: No setEmptyText(), we always have the default entries - - // We have a menu item to show in action bar. setHasOptionsMenu(true); - // Create an empty adapter we will use to display the loaded data. - mAdapter = new AppsAdapter(getActivity(), null, 0); - setListAdapter(mAdapter); + adapter = new ApiAppAdapter(getActivity()); + setAdapter(adapter); + setLayoutManager(new LinearLayoutManager(getActivity(), LinearLayoutManager.VERTICAL, false)); - // NOTE: Loader is started in onResume! + ApiAppsViewModel viewModel = ViewModelProviders.of(this).get(ApiAppsViewModel.class); + viewModel.getListedAppLiveData(requireContext()).observe(this, this::onLoad); } - @Override - public void onResume() { - super.onResume(); - - // Start out with a progress indicator. - setListShown(false); - - // After coming back from Google Play -> reload - getLoaderManager().restartLoader(0, null, this); + private void onLoad(List apiApps) { + if (apiApps == null) { + hideList(false); + adapter.setData(null); + return; + } + adapter.setData(apiApps); + showList(true); } - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - String selectedPackageName = mAdapter.getItemPackageName(position); - boolean installed = mAdapter.getItemIsInstalled(position); - boolean registered = mAdapter.getItemIsRegistered(position); + public void onItemClick(int position) { + ListedApp listedApp = adapter.data.get(position); - if (installed) { - if (registered) { + if (listedApp.isInstalled) { + if (listedApp.isRegistered) { // Edit app settings Intent intent = new Intent(getActivity(), AppSettingsActivity.class); - intent.setData(KeychainContract.ApiApps.buildByPackageNameUri(selectedPackageName)); + intent.putExtra(AppSettingsActivity.EXTRA_PACKAGE_NAME, listedApp.packageName); startActivity(intent); } else { Intent i; - PackageManager manager = getActivity().getPackageManager(); + PackageManager manager = requireActivity().getPackageManager(); try { - i = manager.getLaunchIntentForPackage(selectedPackageName); + i = manager.getLaunchIntentForPackage(listedApp.packageName); if (i == null) { throw new PackageManager.NameNotFoundException(); } @@ -112,255 +102,81 @@ public class AppsListFragment extends ListFragment implements } else { try { startActivity(new Intent(Intent.ACTION_VIEW, - Uri.parse("market://details?id=" + selectedPackageName))); + Uri.parse("market://details?id=" + listedApp.packageName))); } catch (ActivityNotFoundException anfe) { startActivity(new Intent(Intent.ACTION_VIEW, - Uri.parse("https://play.google.com/store/apps/details?id=" + selectedPackageName))); + Uri.parse("https://play.google.com/store/apps/details?id=" + listedApp.packageName))); } } } - private static final String TEMP_COLUMN_NAME = "NAME"; - private static final String TEMP_COLUMN_INSTALLED = "INSTALLED"; - private static final String TEMP_COLUMN_REGISTERED = "REGISTERED"; - private static final String TEMP_COLUMN_ICON_RES_ID = "ICON_RES_ID"; + public class ApiAppAdapter extends Adapter { + private final LayoutInflater inflater; - static final String[] PROJECTION = new String[]{ - ApiApps._ID, // 0 - ApiApps.PACKAGE_NAME, // 1 - "null as " + TEMP_COLUMN_NAME, // installed apps can retrieve app name from Android OS - "0 as " + TEMP_COLUMN_INSTALLED, // changed later in cursor joiner - "1 as " + TEMP_COLUMN_REGISTERED, // if it is in db it is registered - "0 as " + TEMP_COLUMN_ICON_RES_ID // not used - }; + private List data; - private static final int INDEX_ID = 0; - private static final int INDEX_PACKAGE_NAME = 1; - private static final int INDEX_NAME = 2; - private static final int INDEX_INSTALLED = 3; - private static final int INDEX_REGISTERED = 4; - private static final int INDEX_ICON_RES_ID = 5; + ApiAppAdapter(Context context) { + super(); - public Loader onCreateLoader(int id, Bundle args) { - // This is called when a new Loader needs to be created. This - // sample only has one Loader, so we don't care about the ID. - // First, pick the base URI to use depending on whether we are - // currently filtering. - Uri baseUri = ApiApps.CONTENT_URI; - - // Now create and return a CursorLoader that will take care of - // creating a Cursor for the data being displayed. - return new AppsLoader(getActivity(), baseUri, PROJECTION, null, null, - ApiApps.PACKAGE_NAME + " COLLATE LOCALIZED ASC"); - } - - public void onLoadFinished(Loader loader, Cursor data) { - // Swap the new cursor in. (The framework will take care of closing the - // old cursor once we return.) - mAdapter.swapCursor(data); - - // The list should now be shown. - setListShown(true); - } - - public void onLoaderReset(Loader loader) { - // This is called when the last Cursor provided to onLoadFinished() - // above is about to be closed. We need to make sure we are no - // longer using it. - mAdapter.swapCursor(null); - } - - /** - * Besides the queried cursor with all registered apps, this loader also returns non-installed - * proposed apps using a MatrixCursor. - */ - private static class AppsLoader extends CursorLoader { - - public AppsLoader(Context context) { - super(context); + inflater = LayoutInflater.from(context); } - public AppsLoader(Context context, Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { - super(context, uri, projection, selection, selectionArgs, sortOrder); + @NonNull + @Override + public ApiAppViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + return new ApiAppViewHolder(inflater.inflate(R.layout.api_apps_adapter_list_item, parent, false)); } @Override - public Cursor loadInBackground() { - // Load registered apps from content provider - Cursor data = super.loadInBackground(); - - MatrixCursor availableAppsCursor = new MatrixCursor(new String[]{ - ApiApps._ID, - ApiApps.PACKAGE_NAME, - TEMP_COLUMN_NAME, - TEMP_COLUMN_INSTALLED, - TEMP_COLUMN_REGISTERED, - TEMP_COLUMN_ICON_RES_ID - }); - // NOTE: SORT ascending by package name, this is REQUIRED for CursorJoiner! - // Drawables taken from projects res/drawables-xxhdpi/ic_launcher.png - availableAppsCursor.addRow(new Object[]{1, "com.fsck.k9", "K-9 Mail", 0, 0, R.drawable.apps_k9}); - availableAppsCursor.addRow(new Object[]{1, "com.zeapo.pwdstore", "Password Store", 0, 0, R.drawable.apps_password_store}); - availableAppsCursor.addRow(new Object[]{1, "eu.siacs.conversations", "Conversations (Instant Messaging)", 0, 0, R.drawable.apps_conversations}); - - MatrixCursor mergedCursor = new MatrixCursor(new String[]{ - ApiApps._ID, - ApiApps.PACKAGE_NAME, - TEMP_COLUMN_NAME, - TEMP_COLUMN_INSTALLED, - TEMP_COLUMN_REGISTERED, - TEMP_COLUMN_ICON_RES_ID - }); - - CursorJoiner joiner = new CursorJoiner( - availableAppsCursor, - new String[]{ApiApps.PACKAGE_NAME}, - data, - new String[]{ApiApps.PACKAGE_NAME}); - for (CursorJoiner.Result joinerResult : joiner) { - switch (joinerResult) { - case LEFT: { - // handle case where a row in availableAppsCursor is unique - String packageName = availableAppsCursor.getString(INDEX_PACKAGE_NAME); - - mergedCursor.addRow(new Object[]{ - 1, // no need for unique _ID - packageName, - availableAppsCursor.getString(INDEX_NAME), - isInstalled(packageName), - 0, - availableAppsCursor.getInt(INDEX_ICON_RES_ID) - }); - break; - } - case RIGHT: { - // handle case where a row in data is unique - String packageName = data.getString(INDEX_PACKAGE_NAME); - - mergedCursor.addRow(new Object[]{ - 1, // no need for unique _ID - packageName, - null, - isInstalled(packageName), - 1, // registered! - R.mipmap.ic_launcher // icon is retrieved later - }); - break; - } - case BOTH: { - // handle case where a row with the same key is in both cursors - String packageName = data.getString(INDEX_PACKAGE_NAME); - - String name; - if (isInstalled(packageName) == 1) { - name = data.getString(INDEX_NAME); - } else { - // if not installed take name from available apps list - name = availableAppsCursor.getString(INDEX_NAME); - } - - mergedCursor.addRow(new Object[]{ - 1, // no need for unique _ID - packageName, - name, - isInstalled(packageName), - 1, // registered! - R.mipmap.ic_launcher // icon is retrieved later - }); - break; - } - } - } - - return mergedCursor; + public void onBindViewHolder(@NonNull ApiAppViewHolder holder, int position) { + ListedApp item = data.get(position); + holder.bind(item); } - private int isInstalled(String packageName) { - try { - getContext().getPackageManager().getApplicationInfo(packageName, 0); - return 1; - } catch (final PackageManager.NameNotFoundException e) { - return 0; - } + @Override + public int getItemCount() { + return data != null ? data.size() : 0; + } + + public void setData(List data) { + this.data = data; + notifyDataSetChanged(); } } - private class AppsAdapter extends CursorAdapter { + public class ApiAppViewHolder extends RecyclerView.ViewHolder { + private final TextView text; + private final ImageView icon; + private final ImageView installIcon; - private LayoutInflater mInflater; - private PackageManager mPM; + ApiAppViewHolder(View itemView) { + super(itemView); - public AppsAdapter(Context context, Cursor c, int flags) { - super(context, c, flags); - - mInflater = LayoutInflater.from(context); - mPM = context.getApplicationContext().getPackageManager(); + text = itemView.findViewById(R.id.api_apps_adapter_item_name); + icon = itemView.findViewById(R.id.api_apps_adapter_item_icon); + installIcon = itemView.findViewById(R.id.api_apps_adapter_install_icon); + itemView.setOnClickListener((View view) -> onItemClick(getAdapterPosition())); } - /** - * Similar to CursorAdapter.getItemId(). - * Required to build Uris for api apps, which are not based on row ids - */ - public String getItemPackageName(int position) { - if (mDataValid && mCursor != null && mCursor.moveToPosition(position)) { - return mCursor.getString(INDEX_PACKAGE_NAME); + void bind(ListedApp listedApp) { + text.setText(listedApp.readableName); + if (listedApp.applicationIconRes != null) { + icon.setImageResource(listedApp.applicationIconRes); } else { - return null; + icon.setImageDrawable(listedApp.applicationIcon); } + installIcon.setVisibility(listedApp.isInstalled ? View.GONE : View.VISIBLE); } + } - public boolean getItemIsInstalled(int position) { - return mDataValid && mCursor != null - && mCursor.moveToPosition(position) && (mCursor.getInt(INDEX_INSTALLED) == 1); - } + public static class ApiAppsViewModel extends ViewModel { + LiveData> listedAppLiveData; - public boolean getItemIsRegistered(int position) { - return mDataValid && mCursor != null - && mCursor.moveToPosition(position) && (mCursor.getInt(INDEX_REGISTERED) == 1); - } - - @Override - public void bindView(View view, Context context, Cursor cursor) { - TextView text = view.findViewById(R.id.api_apps_adapter_item_name); - ImageView icon = view.findViewById(R.id.api_apps_adapter_item_icon); - ImageView installIcon = view.findViewById(R.id.api_apps_adapter_install_icon); - - String packageName = cursor.getString(INDEX_PACKAGE_NAME); - Timber.d("packageName: " + packageName); - int installed = cursor.getInt(INDEX_INSTALLED); - String name = cursor.getString(INDEX_NAME); - int iconResName = cursor.getInt(INDEX_ICON_RES_ID); - - // get application name and icon - try { - ApplicationInfo ai = mPM.getApplicationInfo(packageName, 0); - - text.setText(mPM.getApplicationLabel(ai)); - icon.setImageDrawable(mPM.getApplicationIcon(ai)); - } catch (final PackageManager.NameNotFoundException e) { - // fallback - if (name == null) { - text.setText(packageName); - } else { - text.setText(name); - try { - icon.setImageDrawable(getResources().getDrawable(iconResName)); - } catch (Resources.NotFoundException e1) { - // silently fail - } - } + LiveData> getListedAppLiveData(Context context) { + if (listedAppLiveData == null) { + listedAppLiveData = new ApiAppsLiveData(context); } - - if (installed == 1) { - installIcon.setVisibility(View.GONE); - } else { - installIcon.setVisibility(View.VISIBLE); - } - } - - @Override - public View newView(Context context, Cursor cursor, ViewGroup parent) { - return mInflater.inflate(R.layout.api_apps_adapter_list_item, null); + return listedAppLiveData; } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/RemoteRegisterPresenter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/RemoteRegisterPresenter.java index e753654ab..e87ac4eb1 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/RemoteRegisterPresenter.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/RemoteRegisterPresenter.java @@ -26,26 +26,26 @@ import android.content.pm.PackageManager.NameNotFoundException; import android.graphics.drawable.Drawable; import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.provider.ApiDataAccessObject; -import org.sufficientlysecure.keychain.remote.AppSettings; +import org.sufficientlysecure.keychain.model.ApiApp; +import org.sufficientlysecure.keychain.daos.ApiAppDao; import timber.log.Timber; class RemoteRegisterPresenter { - private final ApiDataAccessObject apiDao; + private final ApiAppDao apiAppDao; private final PackageManager packageManager; private final Context context; private RemoteRegisterView view; private Intent resultData; - private AppSettings appSettings; + private ApiApp apiApp; RemoteRegisterPresenter(Context context) { this.context = context; - apiDao = new ApiDataAccessObject(context); + apiAppDao = ApiAppDao.getInstance(context); packageManager = context.getPackageManager(); } @@ -54,7 +54,7 @@ class RemoteRegisterPresenter { } void setupFromIntentData(Intent resultData, String packageName, byte[] packageSignature) { - this.appSettings = new AppSettings(packageName, packageSignature); + this.apiApp = ApiApp.create(packageName, packageSignature); this.resultData = resultData; try { @@ -76,7 +76,7 @@ class RemoteRegisterPresenter { } void onClickAllow() { - apiDao.insertApiApp(appSettings); + apiAppDao.insertApiApp(apiApp); view.finishWithResult(resultData); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/RequestKeyPermissionActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/RequestKeyPermissionActivity.java index 27accfb26..884674445 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/RequestKeyPermissionActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/RequestKeyPermissionActivity.java @@ -152,8 +152,8 @@ public class RequestKeyPermissionActivity extends FragmentActivity { } @Override - public void displayKeyInfo(UserId userId) { - keyUserIdView.setText(userId.name); + public void displayKeyInfo(String userIdName) { + keyUserIdView.setText(userIdName); } @Override diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/RequestKeyPermissionPresenter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/RequestKeyPermissionPresenter.java index a3c4e1bad..700936342 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/RequestKeyPermissionPresenter.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/RequestKeyPermissionPresenter.java @@ -25,15 +25,13 @@ import android.content.pm.PackageManager.NameNotFoundException; import android.graphics.drawable.Drawable; import android.support.annotation.Nullable; -import org.openintents.openpgp.util.OpenPgpUtils.UserId; import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.model.SubKey.UnifiedKeyInfo; import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey.SecretKeyType; import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; -import org.sufficientlysecure.keychain.provider.ApiDataAccessObject; -import org.sufficientlysecure.keychain.provider.CachedPublicKeyRing; -import org.sufficientlysecure.keychain.provider.KeyRepository; -import org.sufficientlysecure.keychain.provider.KeyRepository.NotFoundException; -import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; +import org.sufficientlysecure.keychain.daos.ApiAppDao; +import org.sufficientlysecure.keychain.daos.KeyRepository; +import org.sufficientlysecure.keychain.daos.KeyRepository.NotFoundException; import org.sufficientlysecure.keychain.remote.ApiPermissionHelper; import org.sufficientlysecure.keychain.remote.ApiPermissionHelper.WrongPackageCertificateException; import timber.log.Timber; @@ -42,7 +40,7 @@ import timber.log.Timber; class RequestKeyPermissionPresenter { private final Context context; private final PackageManager packageManager; - private final ApiDataAccessObject apiDataAccessObject; + private final ApiAppDao apiAppDao; private final ApiPermissionHelper apiPermissionHelper; private RequestKeyPermissionMvpView view; @@ -54,19 +52,19 @@ class RequestKeyPermissionPresenter { static RequestKeyPermissionPresenter createRequestKeyPermissionPresenter(Context context) { PackageManager packageManager = context.getPackageManager(); - ApiDataAccessObject apiDataAccessObject = new ApiDataAccessObject(context); - ApiPermissionHelper apiPermissionHelper = new ApiPermissionHelper(context, apiDataAccessObject); + ApiAppDao apiAppDao = ApiAppDao.getInstance(context); + ApiPermissionHelper apiPermissionHelper = new ApiPermissionHelper(context, apiAppDao); KeyRepository keyRepository = KeyRepository.create(context); - return new RequestKeyPermissionPresenter(context, apiDataAccessObject, apiPermissionHelper, packageManager, + return new RequestKeyPermissionPresenter(context, apiAppDao, apiPermissionHelper, packageManager, keyRepository); } - private RequestKeyPermissionPresenter(Context context, ApiDataAccessObject apiDataAccessObject, + private RequestKeyPermissionPresenter(Context context, ApiAppDao apiAppDao, ApiPermissionHelper apiPermissionHelper, PackageManager packageManager, KeyRepository keyRepository) { this.context = context; - this.apiDataAccessObject = apiDataAccessObject; + this.apiAppDao = apiAppDao; this.apiPermissionHelper = apiPermissionHelper; this.packageManager = packageManager; this.keyRepository = keyRepository; @@ -95,18 +93,16 @@ class RequestKeyPermissionPresenter { } private void setRequestedMasterKeyId(long[] subKeyIds) throws PgpKeyNotFoundException { - CachedPublicKeyRing secretKeyRingOrPublicFallback = findSecretKeyRingOrPublicFallback(subKeyIds); + UnifiedKeyInfo secretKeyRingOrPublicFallback = findSecretKeyRingOrPublicFallback(subKeyIds); if (secretKeyRingOrPublicFallback == null) { throw new PgpKeyNotFoundException("No key found among requested!"); } - this.masterKeyId = secretKeyRingOrPublicFallback.getMasterKeyId(); + masterKeyId = secretKeyRingOrPublicFallback.master_key_id(); + view.displayKeyInfo(secretKeyRingOrPublicFallback.name()); - UserId userId = secretKeyRingOrPublicFallback.getSplitPrimaryUserIdWithFallback(); - view.displayKeyInfo(userId); - - if (secretKeyRingOrPublicFallback.hasAnySecret()) { + if (secretKeyRingOrPublicFallback.has_any_secret()) { view.switchToLayoutRequestKeyChoice(); } else { view.switchToLayoutNoSecret(); @@ -114,22 +110,24 @@ class RequestKeyPermissionPresenter { } @Nullable - private CachedPublicKeyRing findSecretKeyRingOrPublicFallback(long[] subKeyIds) { - CachedPublicKeyRing publicFallbackRing = null; + private UnifiedKeyInfo findSecretKeyRingOrPublicFallback(long[] subKeyIds) { + UnifiedKeyInfo publicFallbackRing = null; for (long candidateSubKeyId : subKeyIds) { try { - CachedPublicKeyRing cachedPublicKeyRing = keyRepository.getCachedPublicKeyRing( - KeyRings.buildUnifiedKeyRingsFindBySubkeyUri(candidateSubKeyId) - ); + Long masterKeyId = keyRepository.getMasterKeyIdBySubkeyId(candidateSubKeyId); + if (masterKeyId == null) { + continue; + } + UnifiedKeyInfo unifiedKeyInfo = keyRepository.getUnifiedKeyInfo(masterKeyId); - SecretKeyType secretKeyType = cachedPublicKeyRing.getSecretKeyType(candidateSubKeyId); + SecretKeyType secretKeyType = keyRepository.getSecretKeyType(candidateSubKeyId); if (secretKeyType.isUsable()) { - return cachedPublicKeyRing; + return unifiedKeyInfo; } if (publicFallbackRing == null) { - publicFallbackRing = cachedPublicKeyRing; + publicFallbackRing = unifiedKeyInfo; } - } catch (PgpKeyNotFoundException | NotFoundException e) { + } catch (NotFoundException e) { // no matter } } @@ -160,7 +158,7 @@ class RequestKeyPermissionPresenter { } void onClickAllow() { - apiDataAccessObject.addAllowedKeyIdForApp(packageName, masterKeyId); + apiAppDao.addAllowedKeyIdForApp(packageName, masterKeyId); view.finish(); } @@ -179,7 +177,7 @@ class RequestKeyPermissionPresenter { void setTitleText(String text); void setTitleClientIcon(Drawable drawable); - void displayKeyInfo(UserId userId); + void displayKeyInfo(String userIdName); void finish(); void finishAsCancelled(); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/SecurityProblemPresenter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/SecurityProblemPresenter.java index 3b8741458..f1609f99a 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/SecurityProblemPresenter.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/SecurityProblemPresenter.java @@ -36,8 +36,7 @@ import org.sufficientlysecure.keychain.pgp.SecurityProblem.MissingMdc; import org.sufficientlysecure.keychain.pgp.SecurityProblem.NotWhitelistedCurve; import org.sufficientlysecure.keychain.pgp.SecurityProblem.EncryptionAlgorithmProblem; import org.sufficientlysecure.keychain.pgp.SecurityProblem.UnidentifiedKeyProblem; -import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; -import org.sufficientlysecure.keychain.provider.OverriddenWarningsRepository; +import org.sufficientlysecure.keychain.daos.OverriddenWarningsDao; import org.sufficientlysecure.keychain.ui.keyview.ViewKeyActivity; @@ -47,7 +46,7 @@ class SecurityProblemPresenter { private final Context context; private final PackageManager packageManager; - private final OverriddenWarningsRepository overriddenWarningsRepository; + private final OverriddenWarningsDao overriddenWarningsDao; private RemoteSecurityProblemView view; @@ -63,7 +62,7 @@ class SecurityProblemPresenter { SecurityProblemPresenter(Context context) { this.context = context; packageManager = context.getPackageManager(); - overriddenWarningsRepository = OverriddenWarningsRepository.createOverriddenWarningsRepository(context); + overriddenWarningsDao = OverriddenWarningsDao.create(context); } public void setView(RemoteSecurityProblemView view) { @@ -161,7 +160,7 @@ class SecurityProblemPresenter { private void refreshOverrideStatusView() { if (supportOverride) { - if (overriddenWarningsRepository.isWarningOverridden(securityProblemIdentifier)) { + if (overriddenWarningsDao.isWarningOverridden(securityProblemIdentifier)) { view.showOverrideUndoButton(); } else { view.showOverrideButton(); @@ -192,14 +191,14 @@ class SecurityProblemPresenter { overrideCounter++; view.showOverrideMessage(overrideCountLeft); } else { - overriddenWarningsRepository.putOverride(securityProblemIdentifier); + overriddenWarningsDao.putOverride(securityProblemIdentifier); view.finishAsSuppressed(); } } private void resetOverrideStatus() { overrideCounter = 0; - overriddenWarningsRepository.deleteOverride(securityProblemIdentifier); + overriddenWarningsDao.deleteOverride(securityProblemIdentifier); } void onClickGotIt() { @@ -207,9 +206,9 @@ class SecurityProblemPresenter { } void onClickViewKey() { - Intent viewKeyIntent = new Intent(context, ViewKeyActivity.class); - viewKeyIntent.setData(KeyRings.buildGenericKeyRingUri(viewKeyMasterKeyId)); - context.startActivity(viewKeyIntent); + Intent intent = new Intent(context, ViewKeyActivity.class); + intent.putExtra(ViewKeyActivity.EXTRA_MASTER_KEY_ID, viewKeyMasterKeyId); + context.startActivity(intent); } void onClickOverride() { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/SelectIdentityKeyListFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/SelectIdentityKeyListFragment.java deleted file mode 100644 index 3b6d033c2..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/SelectIdentityKeyListFragment.java +++ /dev/null @@ -1,167 +0,0 @@ -/* - * Copyright (C) 2015 Dominik Schürmann - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sufficientlysecure.keychain.remote.ui; - - -import android.app.Activity; -import android.database.Cursor; -import android.net.Uri; -import android.os.Bundle; -import android.support.v4.app.LoaderManager; -import android.support.v4.content.CursorLoader; -import android.support.v4.content.Loader; -import android.support.v7.widget.LinearLayoutManager; - -import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; -import org.sufficientlysecure.keychain.remote.ui.adapter.SelectIdentityKeyAdapter; -import org.sufficientlysecure.keychain.ui.base.RecyclerFragment; -import org.sufficientlysecure.keychain.ui.util.adapter.CursorAdapter; -import org.sufficientlysecure.keychain.ui.util.recyclerview.DividerItemDecoration; - - -public class SelectIdentityKeyListFragment extends RecyclerFragment - implements SelectIdentityKeyAdapter.SelectSignKeyListener, LoaderManager.LoaderCallbacks { - private static final String ARG_API_IDENTITY = "api_identity"; - private String apiIdentity; - private boolean listAllKeys; - - @Override - public void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - outState.getString(ARG_API_IDENTITY, apiIdentity); - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - - if (savedInstanceState != null && apiIdentity == null) { - apiIdentity = getArguments().getString(ARG_API_IDENTITY); - } - - SelectIdentityKeyAdapter adapter = new SelectIdentityKeyAdapter(getContext(), null); - adapter.setListener(this); - - setAdapter(adapter); - LinearLayoutManager layoutManager = new LinearLayoutManager(getContext()); - DividerItemDecoration dividerItemDecoration = - new DividerItemDecoration(getContext(), layoutManager.getOrientation(), true); - setLayoutManager(layoutManager); - getRecyclerView().addItemDecoration(dividerItemDecoration); - - // Start out with a progress indicator. - hideList(false); - - // Prepare the loader. Either re-connect with an existing one, - // or start a new one. - getLoaderManager().initLoader(0, null, this); - } - - @Override - public Loader onCreateLoader(int id, Bundle args) { - // These are the rows that we will retrieve. - String[] projection = new String[]{ - KeyRings._ID, - KeyRings.MASTER_KEY_ID, - KeyRings.USER_ID, - KeyRings.IS_EXPIRED, - KeyRings.IS_REVOKED, - KeyRings.HAS_ENCRYPT, - KeyRings.VERIFIED, - KeyRings.HAS_ANY_SECRET, - KeyRings.HAS_DUPLICATE_USER_ID, - KeyRings.CREATION, - KeyRings.NAME, - KeyRings.EMAIL, - KeyRings.COMMENT, - }; - - String selection = KeyRings.HAS_ANY_SECRET + " != 0"; - Uri baseUri = listAllKeys ? KeyRings.buildUnifiedKeyRingsUri() : - KeyRings.buildUnifiedKeyRingsFindByEmailUri(apiIdentity); - - String orderBy = KeyRings.USER_ID + " ASC"; - // Now create and return a CursorLoader that will take care of - // creating a Cursor for the data being displayed. - return new CursorLoader(getActivity(), baseUri, projection, selection, null, orderBy); - } - - @Override - public void onLoadFinished(Loader loader, Cursor data) { - // Swap the new cursor in. (The framework will take care of closing the - // old cursor once we return.) - getAdapter().swapCursor(CursorAdapter.KeyCursor.wrap(data)); - - // The list should now be shown. - if (isResumed()) { - showList(true); - } else { - showList(false); - } - - boolean isEmpty = data.getCount() == 0; - getKeySelectFragmentListener().onChangeListEmptyStatus(isEmpty); - } - - @Override - public void onLoaderReset(Loader loader) { - // This is called when the last Cursor provided to onLoadFinished() - // above is about to be closed. We need to make sure we are no - // longer using it. - getAdapter().swapCursor(null); - } - - @Override - public void onDestroy() { - getAdapter().setListener(null); - super.onDestroy(); - } - - @Override - public void onSelectKeyItemClicked(long masterKeyId) { - getKeySelectFragmentListener().onKeySelected(masterKeyId); - } - - SelectIdentityKeyFragmentListener getKeySelectFragmentListener() { - Activity activity = getActivity(); - if (activity == null) { - return null; - } - - if (!(activity instanceof SelectIdentityKeyFragmentListener)) { - throw new IllegalStateException("SelectIdentityKeyListFragment must be attached to KeySelectFragmentListener!"); - } - - return (SelectIdentityKeyFragmentListener) activity; - } - - public void setApiIdentity(String apiIdentity) { - this.apiIdentity = apiIdentity; - } - - public void setListAllKeys(boolean listAllKeys) { - this.listAllKeys = listAllKeys; - getLoaderManager().restartLoader(0, null, this); - } - - public interface SelectIdentityKeyFragmentListener { - void onKeySelected(Long masterKeyId); - void onChangeListEmptyStatus(boolean isEmpty); - } - -} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/SelectPublicKeyFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/SelectPublicKeyFragment.java index febeb4e8e..f696f78e3 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/SelectPublicKeyFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/SelectPublicKeyFragment.java @@ -17,49 +17,33 @@ package org.sufficientlysecure.keychain.remote.ui; -import android.content.Context; -import android.database.Cursor; -import android.database.DatabaseUtils; -import android.net.Uri; + +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import android.arch.lifecycle.LiveData; +import android.arch.lifecycle.ViewModelProviders; import android.os.Bundle; -import android.support.v4.app.LoaderManager; -import android.support.v4.content.ContextCompat; -import android.support.v4.content.CursorLoader; -import android.support.v4.content.Loader; +import android.support.annotation.NonNull; import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; -import android.text.Editable; -import android.text.TextUtils; -import android.text.TextWatcher; -import android.view.Gravity; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.EditText; -import android.widget.FrameLayout; -import android.widget.LinearLayout; -import android.widget.ProgressBar; -import android.widget.TextView; -import org.openintents.openpgp.util.OpenPgpUtils; import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; -import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables; -import org.sufficientlysecure.keychain.remote.ui.adapter.SelectEncryptKeyAdapter; -import org.sufficientlysecure.keychain.ui.util.FormattingUtils; +import org.sufficientlysecure.keychain.model.SubKey.UnifiedKeyInfo; +import org.sufficientlysecure.keychain.daos.KeyRepository; +import org.sufficientlysecure.keychain.ui.adapter.KeyChoiceAdapter; import org.sufficientlysecure.keychain.ui.base.RecyclerFragment; +import org.sufficientlysecure.keychain.ui.keyview.GenericViewModel; -public class SelectPublicKeyFragment extends RecyclerFragment - implements TextWatcher, LoaderManager.LoaderCallbacks { +public class SelectPublicKeyFragment extends RecyclerFragment { public static final String ARG_PRESELECTED_KEY_IDS = "preselected_key_ids"; -// private EditText mSearchView; - private long mSelectedMasterKeyIds[]; - private String mQuery; + private Set selectedMasterKeyIds; + private KeyChoiceAdapter keyChoiceAdapter; + private KeyRepository keyRepository; - /** - * Creates new instance of this fragment - */ public static SelectPublicKeyFragment newInstance(long[] preselectedKeyIds) { SelectPublicKeyFragment frag = new SelectPublicKeyFragment(); Bundle args = new Bundle(); @@ -73,203 +57,92 @@ public class SelectPublicKeyFragment extends RecyclerFragment(); + for (long preselectedKey : getArguments().getLongArray(ARG_PRESELECTED_KEY_IDS)) { + selectedMasterKeyIds.add(preselectedKey); + } } - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - final Context context = getContext(); - FrameLayout root = new FrameLayout(context); - - LinearLayout progressContainer = new LinearLayout(context); - progressContainer.setId(INTERNAL_PROGRESS_CONTAINER_ID); - progressContainer.setOrientation(LinearLayout.VERTICAL); - progressContainer.setGravity(Gravity.CENTER); - progressContainer.setVisibility(View.GONE); - - ProgressBar progressBar = new ProgressBar(context, null, - android.R.attr.progressBarStyleLarge); - - progressContainer.addView(progressBar, new LinearLayout.LayoutParams( - ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); - - root.addView(progressContainer, new FrameLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); - - FrameLayout listContainer = new FrameLayout(context); - listContainer.setId(INTERNAL_LIST_CONTAINER_ID); - - TextView textView = new TextView(context); - textView.setId(INTERNAL_EMPTY_VIEW_ID); - textView.setGravity(Gravity.CENTER); - - listContainer.addView(textView, new FrameLayout.LayoutParams( - ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); - - LinearLayout innerListContainer = new LinearLayout(context); - innerListContainer.setOrientation(LinearLayout.VERTICAL); - - // TODO: search is broken: When searching, previously selected keys are no longer selected -// mSearchView = new EditText(context); -// mSearchView.setId(android.R.id.input); -// mSearchView.setHint(R.string.menu_search); -// mSearchView.setCompoundDrawablesWithIntrinsicBounds( -// ContextCompat.getDrawable( -// context, -// R.drawable.ic_search_grey_24dp -// ), null, null, null); -// -// innerListContainer.addView(mSearchView, new LinearLayout.LayoutParams( -// ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); - - RecyclerView listView = new RecyclerView(context); - listView.setId(INTERNAL_LIST_VIEW_ID); - - int padding = FormattingUtils.dpToPx(context, 8); - listView.setPadding(padding, 0, padding, 0); - listView.setClipToPadding(false); - - innerListContainer.addView(listView, new LinearLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); - - listContainer.addView(innerListContainer, new FrameLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); - - root.addView(listContainer, new FrameLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); - - - root.setLayoutParams(new FrameLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); - - return root; - } - - /** - * Define Adapter and Loader on create of Activity - */ @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - // Give some text to display if there is no data. In a real - // application this would come from a resource. setEmptyText(getString(R.string.list_empty)); -// mSearchView.addTextChangedListener(this); - - setAdapter(new SelectEncryptKeyAdapter(getContext(), null)); setLayoutManager(new LinearLayoutManager(getContext())); - - // Start out with a progress indicator. hideList(false); - // Prepare the loader. Either re-connect with an existing one, - // or start a new one. - getLoaderManager().initLoader(0, null, this); + GenericViewModel viewModel = ViewModelProviders.of(this).get(GenericViewModel.class); + LiveData> liveData = viewModel.getGenericLiveData(requireContext(), this::loadSortedUnifiedKeyInfo); + liveData.observe(this, this::onLoadUnifiedKeyData); + } + + @NonNull + private List loadSortedUnifiedKeyInfo() { + List keyInfos = keyRepository.getAllUnifiedKeyInfo(); + Collections.sort(keyInfos, sortKeysByPreselectionComparator()); + return keyInfos; + } + + @NonNull + private Comparator sortKeysByPreselectionComparator() { + return (first, second) -> { + if (first == second) { + return 0; + } + boolean firstIsPreselected = selectedMasterKeyIds.contains(first.master_key_id()); + boolean secondIsPreselected = selectedMasterKeyIds.contains(second.master_key_id()); + if (firstIsPreselected != secondIsPreselected) { + return firstIsPreselected ? -1 : 1; + } + String firstUid = first.user_id(); + String secondUid = second.user_id(); + if (firstUid != null && secondUid != null) { + return firstUid.compareTo(secondUid); + } else { + return firstUid == null ? -1 : -1; + } + }; } public long[] getSelectedMasterKeyIds() { - return getAdapter() != null ? - getAdapter().getMasterKeyIds() : new long[0]; + if (keyChoiceAdapter == null) { + return null; + } + // *sigh + Set selectionIds = keyChoiceAdapter.getSelectionIds(); + long[] result = new long[selectionIds.size()]; + int i = 0; + for (Long selectionId : selectionIds) { + result[i++] = selectionId; + } + return result; } - @Override - public Loader onCreateLoader(int id, Bundle args) { - Uri baseUri = KeyRings.buildUnifiedKeyRingsUri(); - - // These are the rows that we will retrieve. - String[] projection = new String[]{ - KeyRings._ID, - KeyRings.MASTER_KEY_ID, - KeyRings.USER_ID, - KeyRings.IS_EXPIRED, - KeyRings.IS_REVOKED, - KeyRings.HAS_ENCRYPT, - KeyRings.VERIFIED, - KeyRings.HAS_DUPLICATE_USER_ID, - KeyRings.CREATION, - KeyRings.NAME, - KeyRings.EMAIL, - KeyRings.COMMENT - }; - - String inMasterKeyList = null; - if (mSelectedMasterKeyIds != null && mSelectedMasterKeyIds.length > 0) { - inMasterKeyList = Tables.KEYS + "." + KeyRings.MASTER_KEY_ID + " IN ("; - for (int i = 0; i < mSelectedMasterKeyIds.length; ++i) { - if (i != 0) { - inMasterKeyList += ", "; - } - inMasterKeyList += DatabaseUtils.sqlEscapeString("" + mSelectedMasterKeyIds[i]); - } - inMasterKeyList += ")"; - } - - String orderBy = KeyRings.USER_ID + " ASC"; - if (inMasterKeyList != null) { - // sort by selected master keys - orderBy = inMasterKeyList + " DESC, " + orderBy; - } - String where = null; - String whereArgs[] = null; - if (mQuery != null) { - String[] words = mQuery.trim().split("\\s+"); - whereArgs = new String[words.length]; - for (int i = 0; i < words.length; ++i) { - if (where == null) { - where = ""; + public void onLoadUnifiedKeyData(List data) { + if (keyChoiceAdapter == null) { + keyChoiceAdapter = KeyChoiceAdapter.createMultiChoiceAdapter(requireContext(), data, (keyInfo -> { + if (keyInfo.is_revoked()) { + return R.string.keychoice_revoked; + } else if (keyInfo.is_expired()) { + return R.string.keychoice_expired; + } else if (!keyInfo.is_secure()) { + return R.string.keychoice_insecure; + } else if (!keyInfo.has_encrypt_key()) { + return R.string.keychoice_cannot_encrypt; } else { - where += " AND "; + return null; } - where += KeyRings.USER_ID + " LIKE ?"; - whereArgs[i] = "%" + words[i] + "%"; - } - } - - // Now create and return a CursorLoader that will take care of - // creating a Cursor for the data being displayed. - return new CursorLoader(getActivity(), baseUri, projection, where, whereArgs, orderBy); - } - - @Override - public void onLoadFinished(Loader loader, Cursor data) { - // Swap the new cursor in. (The framework will take care of closing the - // old cursor once we return.) - getAdapter().setQuery(mQuery); - getAdapter().swapCursor(SelectEncryptKeyAdapter.PublicKeyCursor.wrap(data)); - - // The list should now be shown. - if (isResumed()) { - showList(true); + })); + setAdapter(keyChoiceAdapter); + keyChoiceAdapter.setSelectionByIds(selectedMasterKeyIds); } else { - showList(false); + keyChoiceAdapter.setUnifiedKeyInfoItems(data); } - // preselect given master keys - getAdapter().preselectMasterKeyIds(mSelectedMasterKeyIds); - } - - @Override - public void onLoaderReset(Loader loader) { - // This is called when the last Cursor provided to onLoadFinished() - // above is about to be closed. We need to make sure we are no - // longer using it. - getAdapter().swapCursor(null); - } - - @Override - public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) { - - } - - @Override - public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) { - - } - - @Override - public void afterTextChanged(Editable editable) { - mQuery = !TextUtils.isEmpty(editable.toString()) ? editable.toString() : null; - getLoaderManager().restartLoader(0, null, this); + boolean animateShowList = !isResumed(); + showList(animateShowList); } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/SelectSignKeyIdActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/SelectSignKeyIdActivity.java index 058b4ffac..91440e018 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/SelectSignKeyIdActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/SelectSignKeyIdActivity.java @@ -34,6 +34,7 @@ import timber.log.Timber; public class SelectSignKeyIdActivity extends BaseActivity { + public static final String EXTRA_PACKAGE_NAME = "package_name"; public static final String EXTRA_USER_ID = OpenPgpApi.EXTRA_USER_ID; public static final String EXTRA_DATA = "data"; @@ -68,19 +69,18 @@ public class SelectSignKeyIdActivity extends BaseActivity { }); Intent intent = getIntent(); - Uri appUri = intent.getData(); + String packageName = intent.getStringExtra(EXTRA_PACKAGE_NAME); mPreferredUserId = intent.getStringExtra(EXTRA_USER_ID); mData = intent.getParcelableExtra(EXTRA_DATA); - if (appUri == null) { + if (packageName == null) { Timber.e("Intent data missing. Should be Uri of app!"); finish(); } else { - Timber.d("uri: " + appUri); - startListFragments(savedInstanceState, appUri, mData, mPreferredUserId); + startListFragments(savedInstanceState, packageName, mData, mPreferredUserId); } } - private void startListFragments(Bundle savedInstanceState, Uri dataUri, Intent data, String preferredUserId) { + private void startListFragments(Bundle savedInstanceState, String packageName, Intent data, String preferredUserId) { // However, if we're being restored from a previous state, // then we don't need to do anything and should return or else // we could end up with overlapping fragments. @@ -90,7 +90,7 @@ public class SelectSignKeyIdActivity extends BaseActivity { // Create an instance of the fragments SelectSignKeyIdListFragment listFragment = SelectSignKeyIdListFragment - .newInstance(dataUri, data, preferredUserId); + .newInstance(packageName, data, preferredUserId); // Add the fragment to the 'fragment_container' FrameLayout // NOTE: We use commitAllowingStateLoss() to prevent weird crashes! getSupportFragmentManager().beginTransaction() diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/SelectSignKeyIdListFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/SelectSignKeyIdListFragment.java index 7ddbdd82e..557f606df 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/SelectSignKeyIdListFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/SelectSignKeyIdListFragment.java @@ -18,50 +18,51 @@ package org.sufficientlysecure.keychain.remote.ui; +import java.util.List; + import android.app.Activity; +import android.arch.lifecycle.LiveData; +import android.arch.lifecycle.ViewModelProviders; import android.content.Intent; -import android.database.Cursor; -import android.net.Uri; import android.os.Bundle; -import android.support.v4.app.LoaderManager; -import android.support.v4.content.CursorLoader; -import android.support.v4.content.Loader; import android.support.v7.widget.LinearLayoutManager; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; import org.openintents.openpgp.util.OpenPgpApi; import org.openintents.openpgp.util.OpenPgpUtils; import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.model.SubKey.UnifiedKeyInfo; import org.sufficientlysecure.keychain.pgp.KeyRing; -import org.sufficientlysecure.keychain.provider.ApiDataAccessObject; -import org.sufficientlysecure.keychain.provider.KeychainContract; -import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; -import org.sufficientlysecure.keychain.remote.ui.adapter.SelectSignKeyAdapter; +import org.sufficientlysecure.keychain.daos.ApiAppDao; +import org.sufficientlysecure.keychain.daos.KeyRepository; import org.sufficientlysecure.keychain.ui.CreateKeyActivity; -import org.sufficientlysecure.keychain.ui.util.adapter.CursorAdapter; +import org.sufficientlysecure.keychain.ui.adapter.KeyChoiceAdapter; import org.sufficientlysecure.keychain.ui.base.RecyclerFragment; -import timber.log.Timber; +import org.sufficientlysecure.keychain.ui.keyview.GenericViewModel; -public class SelectSignKeyIdListFragment extends RecyclerFragment - implements SelectSignKeyAdapter.SelectSignKeyListener, LoaderManager.LoaderCallbacks { - - private static final String ARG_DATA_URI = "uri"; +public class SelectSignKeyIdListFragment extends RecyclerFragment { + private static final String ARG_PACKAGE_NAME = "package_name"; private static final String ARG_PREF_UID = "pref_uid"; public static final String ARG_DATA = "data"; - private Uri mDataUri; - private Intent mResult; - private String mPrefUid; - private ApiDataAccessObject mApiDao; + private ApiAppDao apiAppDao; + private KeyRepository keyRepository; - /** - * Creates new instance of this fragment - */ - public static SelectSignKeyIdListFragment newInstance(Uri dataUri, Intent data, String preferredUserId) { + private KeyChoiceAdapter keyChoiceAdapter; + + private Intent resultIntent; + private String prefUid; + private String packageName; + + public static SelectSignKeyIdListFragment newInstance(String packageName, Intent data, String preferredUserId) { SelectSignKeyIdListFragment frag = new SelectSignKeyIdListFragment(); Bundle args = new Bundle(); - args.putParcelable(ARG_DATA_URI, dataUri); + args.putString(ARG_PACKAGE_NAME, packageName); args.putParcelable(ARG_DATA, data); args.putString(ARG_PREF_UID, preferredUserId); @@ -73,119 +74,86 @@ public class SelectSignKeyIdListFragment extends RecyclerFragment onCreateKeyDummyClicked()); + + return linearLayout; } - /** - * Define Adapter and Loader on create of Activity - */ @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - mResult = getArguments().getParcelable(ARG_DATA); - mPrefUid = getArguments().getString(ARG_PREF_UID); - mDataUri = getArguments().getParcelable(ARG_DATA_URI); + resultIntent = getArguments().getParcelable(ARG_DATA); + prefUid = getArguments().getString(ARG_PREF_UID); + packageName = getArguments().getString(ARG_PACKAGE_NAME); - // Give some text to display if there is no data. In a real - // application this would come from a resource. setEmptyText(getString(R.string.list_empty)); - - SelectSignKeyAdapter adapter = new SelectSignKeyAdapter(getContext(), null); - adapter.setListener(this); - - setAdapter(adapter); setLayoutManager(new LinearLayoutManager(getContext())); - - // Start out with a progress indicator. hideList(false); - // Prepare the loader. Either re-connect with an existing one, - // or start a new one. - getLoaderManager().initLoader(0, null, this); + GenericViewModel viewModel = ViewModelProviders.of(this).get(GenericViewModel.class); + LiveData> liveData = viewModel.getGenericLiveData( + requireContext(), keyRepository::getAllUnifiedKeyInfoWithSecret); + liveData.observe(this, this::onLoadUnifiedKeyData); } - @Override - public Loader onCreateLoader(int id, Bundle args) { - Uri baseUri = KeyRings.buildUnifiedKeyRingsUri(); - - // These are the rows that we will retrieve. - String[] projection = new String[]{ - KeyRings._ID, - KeyRings.MASTER_KEY_ID, - KeyRings.USER_ID, - KeyRings.IS_EXPIRED, - KeyRings.IS_REVOKED, - KeyRings.HAS_ENCRYPT, - KeyRings.VERIFIED, - KeyRings.HAS_ANY_SECRET, - KeyRings.HAS_DUPLICATE_USER_ID, - KeyRings.CREATION, - KeyRings.NAME, - KeyRings.EMAIL, - KeyRings.COMMENT - }; - - String selection = KeyRings.HAS_ANY_SECRET + " != 0"; - - String orderBy = KeyRings.USER_ID + " ASC"; - // Now create and return a CursorLoader that will take care of - // creating a Cursor for the data being displayed. - return new CursorLoader(getActivity(), baseUri, projection, selection, null, orderBy); - } - - @Override - public void onLoadFinished(Loader loader, Cursor data) { - // Swap the new cursor in. (The framework will take care of closing the - // old cursor once we return.) - getAdapter().swapCursor(CursorAdapter.KeyCursor.wrap(data)); - - // The list should now be shown. - if (isResumed()) { - showList(true); + public void onLoadUnifiedKeyData(List data) { + if (keyChoiceAdapter == null) { + keyChoiceAdapter = KeyChoiceAdapter.createSingleClickableAdapter(requireContext(), data, this::onSelectKeyItemClicked, (keyInfo -> { + if (keyInfo.is_revoked()) { + return R.string.keychoice_revoked; + } else if (keyInfo.is_expired()) { + return R.string.keychoice_expired; + } else if (!keyInfo.is_secure()) { + return R.string.keychoice_insecure; + } else if (!keyInfo.has_sign_key()) { + return R.string.keychoice_cannot_sign; + } else { + return null; + } + })); + setAdapter(keyChoiceAdapter); } else { - showList(false); + keyChoiceAdapter.setUnifiedKeyInfoItems(data); } + boolean animateShowList = !isResumed(); + showList(animateShowList); } - @Override - public void onLoaderReset(Loader loader) { - // This is called when the last Cursor provided to onLoadFinished() - // above is about to be closed. We need to make sure we are no - // longer using it. - getAdapter().swapCursor(null); - } - - @Override - public void onDestroy() { - getAdapter().setListener(null); - super.onDestroy(); - } - - @Override public void onCreateKeyDummyClicked() { - OpenPgpUtils.UserId userIdSplit = KeyRing.splitUserId(mPrefUid); + OpenPgpUtils.UserId userIdSplit = KeyRing.splitUserId(prefUid); Intent intent = new Intent(getActivity(), CreateKeyActivity.class); intent.putExtra(CreateKeyActivity.EXTRA_NAME, userIdSplit.name); intent.putExtra(CreateKeyActivity.EXTRA_EMAIL, userIdSplit.email); - getActivity().startActivityForResult(intent, SelectSignKeyIdActivity.REQUEST_CODE_CREATE_KEY); + + requireActivity().startActivityForResult(intent, SelectSignKeyIdActivity.REQUEST_CODE_CREATE_KEY); } - @Override - public void onSelectKeyItemClicked(long masterKeyId) { - Uri allowedKeysUri = mDataUri.buildUpon() - .appendPath(KeychainContract.PATH_ALLOWED_KEYS) - .build(); + private void onSelectKeyItemClicked(UnifiedKeyInfo keyInfo) { + apiAppDao.addAllowedKeyIdForApp(packageName, keyInfo.master_key_id()); + resultIntent.putExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, keyInfo.master_key_id()); - mApiDao.addAllowedKeyIdForApp(allowedKeysUri, masterKeyId); - mResult.putExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, masterKeyId); - - Timber.d("allowedKeyId: " + masterKeyId); - Timber.d("allowedKeysUri: " + allowedKeysUri); - - getActivity().setResult(Activity.RESULT_OK, mResult); - getActivity().finish(); + Activity activity = requireActivity(); + activity.setResult(Activity.RESULT_OK, resultIntent); + activity.finish(); } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/adapter/SelectEncryptKeyAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/adapter/SelectEncryptKeyAdapter.java deleted file mode 100644 index 48c75b29a..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/adapter/SelectEncryptKeyAdapter.java +++ /dev/null @@ -1,300 +0,0 @@ -/* - * Copyright (C) 2017 Schürmann & Breitmoser GbR - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sufficientlysecure.keychain.remote.ui.adapter; - -import android.content.Context; -import android.database.Cursor; -import android.support.v4.content.ContextCompat; -import android.support.v7.widget.RecyclerView; -import android.text.format.DateUtils; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.CheckBox; -import android.widget.ImageView; -import android.widget.TextView; - -import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.provider.KeychainContract; -import org.sufficientlysecure.keychain.ui.adapter.KeyCursorAdapter; -import org.sufficientlysecure.keychain.ui.util.FormattingUtils; -import org.sufficientlysecure.keychain.ui.util.Highlighter; -import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; -import org.sufficientlysecure.keychain.ui.util.adapter.CursorAdapter; - -import java.util.ArrayList; -import java.util.Arrays; - -public class SelectEncryptKeyAdapter extends KeyCursorAdapter { - - private ArrayList mSelected; - - public SelectEncryptKeyAdapter(Context context, PublicKeyCursor cursor) { - super(context, cursor); - - mSelected = new ArrayList<>(); - } - - private boolean isSelected(int position) { - return mSelected.contains(position); - } - - private void select(int position) { - if (!isSelected(position)) { - mSelected.add(position); - notifyItemChanged(position); - } - } - - private void switchSelected(int position) { - int index = mSelected.indexOf(position); - if (index < 0) { - mSelected.add(position); - } else { - mSelected.remove(index); - } - - notifyItemChanged(position); - } - - public long[] getMasterKeyIds() { - long[] selected = new long[mSelected.size()]; - for (int i = 0; i < selected.length; i++) { - int position = mSelected.get(i); - if (!moveCursor(position)) { - return selected; - } - - selected[i] = getCursor().getKeyId(); - } - - return selected; - } - - public void preselectMasterKeyIds(long[] keyIds) { - if (keyIds != null) { - int count = 0; - for (int i = 0; i < getItemCount(); i++) { - if (!moveCursor(i)) { - continue; - } - - long id = getCursor().getKeyId(); - for (long keyId : keyIds) { - if (id == keyId) { - select(i); - count++; - break; - } - } - - if (count >= keyIds.length) { - return; - } - } - } - } - - @Override - public void onContentChanged() { - mSelected.clear(); - super.onContentChanged(); - } - - @Override - public void onBindViewHolder(EncryptKeyItemHolder holder, PublicKeyCursor cursor, String query) { - holder.bind(cursor, query, isSelected(holder.getAdapterPosition())); - } - - @Override - public EncryptKeyItemHolder onCreateViewHolder(ViewGroup parent, int viewType) { - return new EncryptKeyItemHolder(LayoutInflater.from(parent.getContext()) - .inflate(R.layout.select_encrypt_key_item, parent, false)); - } - - class EncryptKeyItemHolder extends RecyclerView.ViewHolder implements View.OnClickListener { - private TextView mUserIdText; - private TextView mUserIdRestText; - private TextView mCreationText; - private ImageView mStatusIcon; - private CheckBox mChecked; - - public EncryptKeyItemHolder(View itemView) { - super(itemView); - itemView.setOnClickListener(this); - - mUserIdText = itemView.findViewById(R.id.select_key_item_name); - mUserIdRestText = itemView.findViewById(R.id.select_key_item_email); - mCreationText = itemView.findViewById(R.id.select_key_item_creation); - mStatusIcon = itemView.findViewById(R.id.select_key_item_status_icon); - mChecked = itemView.findViewById(R.id.selected); - } - - public void bind(PublicKeyCursor cursor, String query, boolean selected) { - Highlighter highlighter = new Highlighter(itemView.getContext(), query); - Context context = itemView.getContext(); - - { // set name and stuff, common to both key types - String name = cursor.getName(); - String email = cursor.getEmail(); - if (name != null) { - mUserIdText.setText(highlighter.highlight(name)); - } else { - mUserIdText.setText(R.string.user_id_no_name); - } - if (email != null) { - mUserIdRestText.setText(highlighter.highlight(email)); - mUserIdRestText.setVisibility(View.VISIBLE); - } else { - mUserIdRestText.setVisibility(View.GONE); - } - } - - boolean enabled; - { // set edit button and status, specific by key type. Note: order is important! - int textColor; - if (cursor.isRevoked()) { - KeyFormattingUtils.setStatusImage( - context, - mStatusIcon, - null, - KeyFormattingUtils.State.REVOKED, - R.color.key_flag_gray - ); - - mStatusIcon.setVisibility(View.VISIBLE); - - enabled = false; - textColor = ContextCompat.getColor(context, R.color.key_flag_gray); - } else if (cursor.isExpired()) { - KeyFormattingUtils.setStatusImage( - context, - mStatusIcon, - null, - KeyFormattingUtils.State.EXPIRED, - R.color.key_flag_gray - ); - - mStatusIcon.setVisibility(View.VISIBLE); - - enabled = false; - textColor = ContextCompat.getColor(context, R.color.key_flag_gray); - } else if (!cursor.hasEncrypt()) { - KeyFormattingUtils.setStatusImage( - context, - mStatusIcon, - KeyFormattingUtils.State.UNAVAILABLE - ); - - mStatusIcon.setVisibility(View.VISIBLE); - - enabled = false; - textColor = ContextCompat.getColor(context, R.color.key_flag_gray); - } else if (cursor.isVerified()) { - KeyFormattingUtils.setStatusImage( - context, - mStatusIcon, - KeyFormattingUtils.State.VERIFIED - ); - - mStatusIcon.setVisibility(View.VISIBLE); - - enabled = true; - textColor = FormattingUtils.getColorFromAttr(context, R.attr.colorText); - } else { - KeyFormattingUtils.setStatusImage( - context, - mStatusIcon, - KeyFormattingUtils.State.UNVERIFIED - ); - - mStatusIcon.setVisibility(View.VISIBLE); - - enabled = true; - textColor = FormattingUtils.getColorFromAttr(context, R.attr.colorText); - } - - mUserIdText.setTextColor(textColor); - mUserIdRestText.setTextColor(textColor); - - if (cursor.hasDuplicate()) { - String dateTime = DateUtils.formatDateTime(context, - cursor.getCreationTime(), - DateUtils.FORMAT_SHOW_DATE - | DateUtils.FORMAT_SHOW_TIME - | DateUtils.FORMAT_SHOW_YEAR - | DateUtils.FORMAT_ABBREV_MONTH); - mCreationText.setText(context.getString(R.string.label_key_created, - dateTime)); - mCreationText.setTextColor(textColor); - mCreationText.setVisibility(View.VISIBLE); - } else { - mCreationText.setVisibility(View.GONE); - } - } - - itemView.setEnabled(enabled); - itemView.setClickable(enabled); - mChecked.setChecked(enabled && selected); - - } - - @Override - public void onClick(View v) { - switchSelected(getAdapterPosition()); - } - } - - public static class PublicKeyCursor extends CursorAdapter.KeyCursor { - public static final String[] PROJECTION; - - static { - ArrayList arr = new ArrayList<>(); - arr.addAll(Arrays.asList(KeyCursor.PROJECTION)); - arr.addAll(Arrays.asList( - KeychainContract.KeyRings.HAS_ENCRYPT, - KeychainContract.KeyRings.VERIFIED - )); - - PROJECTION = arr.toArray(new String[arr.size()]); - } - - public static PublicKeyCursor wrap(Cursor cursor) { - if (cursor != null) { - return new PublicKeyCursor(cursor); - } else { - return null; - } - } - - private PublicKeyCursor(Cursor cursor) { - super(cursor); - } - - public boolean hasEncrypt() { - int index = getColumnIndexOrThrow(KeychainContract.KeyRings.HAS_ENCRYPT); - return getInt(index) != 0; - } - - public boolean isVerified() { - int index = getColumnIndexOrThrow(KeychainContract.KeyRings.VERIFIED); - return getInt(index) != 0; - } - } -} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/adapter/SelectIdentityKeyAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/adapter/SelectIdentityKeyAdapter.java deleted file mode 100644 index 66e5baee8..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/adapter/SelectIdentityKeyAdapter.java +++ /dev/null @@ -1,152 +0,0 @@ -/* - * Copyright (C) 2016 Tobias Erthal - * Copyright (C) 2014-2016 Dominik Schürmann - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sufficientlysecure.keychain.remote.ui.adapter; - -import android.content.Context; -import android.database.Cursor; -import android.support.v4.content.ContextCompat; -import android.support.v7.widget.RecyclerView; -import android.text.format.DateUtils; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.TextView; - -import org.openintents.openpgp.util.OpenPgpUtils; -import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.ui.adapter.KeyCursorAdapter; -import org.sufficientlysecure.keychain.ui.util.FormattingUtils; -import org.sufficientlysecure.keychain.ui.util.Highlighter; -import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; -import org.sufficientlysecure.keychain.ui.util.adapter.CursorAdapter; - - -public class SelectIdentityKeyAdapter extends KeyCursorAdapter { - private SelectSignKeyListener mListener; - - public SelectIdentityKeyAdapter(Context context, Cursor cursor) { - super(context, KeyCursor.wrap(cursor)); - } - - public void setListener(SelectSignKeyListener listener) { - mListener = listener; - } - - @Override - public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - return new SignKeyItemHolder(LayoutInflater.from(parent.getContext()) - .inflate(R.layout.select_identity_key_item, parent, false)); - } - - @Override - public void onBindViewHolder(RecyclerView.ViewHolder holder, KeyCursor cursor, String query) { - ((SignKeyItemHolder) holder).bind(cursor, query); - } - - private class SignKeyItemHolder extends RecyclerView.ViewHolder - implements View.OnClickListener { - - private TextView userIdText; - private TextView creationText; - private ImageView statusIcon; - - SignKeyItemHolder(View itemView) { - super(itemView); - itemView.setClickable(true); - itemView.setOnClickListener(this); - - userIdText = (TextView) itemView.findViewById(R.id.select_key_item_name); - creationText = (TextView) itemView.findViewById(R.id.select_key_item_creation); - statusIcon = (ImageView) itemView.findViewById(R.id.select_key_item_status_icon); - } - - public void bind(KeyCursor cursor, String query) { - Context context = itemView.getContext(); - - { // set name and stuff, common to both key types - String name = cursor.getName(); - if (name != null) { - userIdText.setText(context.getString(R.string.use_key, name)); - } else { - String email = cursor.getEmail(); - userIdText.setText(context.getString(R.string.use_key, email)); - } - } - - { // set edit button and status, specific by key type. Note: order is important! - int textColor; - if (cursor.isRevoked()) { - KeyFormattingUtils.setStatusImage( - context, - statusIcon, - null, - KeyFormattingUtils.State.REVOKED, - R.color.key_flag_gray - ); - - itemView.setEnabled(false); - statusIcon.setVisibility(View.VISIBLE); - textColor = ContextCompat.getColor(context, R.color.key_flag_gray); - } else if (cursor.isExpired()) { - KeyFormattingUtils.setStatusImage( - context, - statusIcon, - null, - KeyFormattingUtils.State.EXPIRED, - R.color.key_flag_gray - ); - - itemView.setEnabled(false); - statusIcon.setVisibility(View.VISIBLE); - textColor = ContextCompat.getColor(context, R.color.key_flag_gray); - } else { - itemView.setEnabled(true); - statusIcon.setImageResource(R.drawable.ic_vpn_key_grey_24dp); - textColor = FormattingUtils.getColorFromAttr(context, R.attr.colorText); - } - - userIdText.setTextColor(textColor); - - String dateTime = DateUtils.formatDateTime(context, - cursor.getCreationTime(), - DateUtils.FORMAT_SHOW_DATE - | DateUtils.FORMAT_SHOW_TIME - | DateUtils.FORMAT_SHOW_YEAR - | DateUtils.FORMAT_ABBREV_MONTH); - creationText.setText(context.getString(R.string.label_key_created, - dateTime)); - creationText.setTextColor(textColor); - creationText.setVisibility(View.VISIBLE); - } - - } - - @Override - public void onClick(View v) { - if (mListener != null) { - mListener.onSelectKeyItemClicked(getItemId()); - } - } - } - - public interface SelectSignKeyListener { - void onSelectKeyItemClicked(long masterKeyId); - } -} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/adapter/SelectSignKeyAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/adapter/SelectSignKeyAdapter.java deleted file mode 100644 index 45617f192..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/adapter/SelectSignKeyAdapter.java +++ /dev/null @@ -1,224 +0,0 @@ -/* - * Copyright (C) 2017 Schürmann & Breitmoser GbR - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sufficientlysecure.keychain.remote.ui.adapter; - -import android.content.Context; -import android.database.Cursor; -import android.support.v4.content.ContextCompat; -import android.support.v7.widget.RecyclerView; -import android.text.format.DateUtils; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.TextView; - -import org.openintents.openpgp.util.OpenPgpUtils; -import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.ui.adapter.KeyCursorAdapter; -import org.sufficientlysecure.keychain.ui.util.FormattingUtils; -import org.sufficientlysecure.keychain.ui.util.Highlighter; -import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; -import org.sufficientlysecure.keychain.ui.util.adapter.CursorAdapter; - -public class SelectSignKeyAdapter extends KeyCursorAdapter { - private static final int VIEW_TYPE_KEY = 0; - private static final int VIEW_TYPE_DUMMY = 1; - - private SelectSignKeyListener mListener; - - public SelectSignKeyAdapter(Context context, Cursor cursor) { - super(context, KeyCursor.wrap(cursor)); - } - - public void setListener(SelectSignKeyListener listener) { - mListener = listener; - } - - @Override - public int getItemCount() { - return super.getItemCount() + 1; // received items + 1 dummy key - } - - @Override - public long getItemId(int pos) { - if (pos < super.getItemCount()) { - return super.getItemId(pos); - } else { - return RecyclerView.NO_ID; - } - } - - @Override - public int getItemViewType(int position) { - return position == super.getItemCount() ? - VIEW_TYPE_DUMMY : VIEW_TYPE_KEY; - } - - @Override - public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - switch (viewType) { - case VIEW_TYPE_KEY: - return new SignKeyItemHolder(LayoutInflater.from(parent.getContext()) - .inflate(R.layout.select_sign_key_item, parent, false)); - - case VIEW_TYPE_DUMMY: - return new DummyViewHolder(LayoutInflater.from(parent.getContext()) - .inflate(R.layout.select_dummy_key_item, parent, false)); - - default: - return null; - } - - } - - @Override - public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { - if (holder.getItemViewType() == VIEW_TYPE_KEY) { - super.onBindViewHolder(holder, position); - } - } - - @Override - public void onBindViewHolder(RecyclerView.ViewHolder holder, KeyCursor cursor, String query) { - ((SignKeyItemHolder) holder).bind(cursor, query); - } - - private class DummyViewHolder extends RecyclerView.ViewHolder - implements View.OnClickListener { - - public DummyViewHolder(View itemView) { - super(itemView); - itemView.setClickable(true); - itemView.setOnClickListener(this); - } - - @Override - public void onClick(View v) { - if (mListener != null) { - mListener.onCreateKeyDummyClicked(); - } - } - } - - private class SignKeyItemHolder extends RecyclerView.ViewHolder - implements View.OnClickListener { - - private TextView mUserIdText; - private TextView mUserIdRestText; - private TextView mCreationText; - private ImageView mStatusIcon; - - public SignKeyItemHolder(View itemView) { - super(itemView); - itemView.setClickable(true); - itemView.setOnClickListener(this); - - mUserIdText = itemView.findViewById(R.id.select_key_item_name); - mUserIdRestText = itemView.findViewById(R.id.select_key_item_email); - mCreationText = itemView.findViewById(R.id.select_key_item_creation); - mStatusIcon = itemView.findViewById(R.id.select_key_item_status_icon); - } - - public void bind(KeyCursor cursor, String query) { - Highlighter highlighter = new Highlighter(itemView.getContext(), query); - Context context = itemView.getContext(); - - { // set name and stuff, common to both key types - String name = cursor.getName(); - String email = cursor.getEmail(); - if (name != null) { - mUserIdText.setText(highlighter.highlight(name)); - } else { - mUserIdText.setText(R.string.user_id_no_name); - } - if (email != null) { - mUserIdRestText.setText(highlighter.highlight(email)); - mUserIdRestText.setVisibility(View.VISIBLE); - } else { - mUserIdRestText.setVisibility(View.GONE); - } - } - - { // set edit button and status, specific by key type. Note: order is important! - int textColor; - if (cursor.isRevoked()) { - KeyFormattingUtils.setStatusImage( - context, - mStatusIcon, - null, - KeyFormattingUtils.State.REVOKED, - R.color.key_flag_gray - ); - - itemView.setEnabled(false); - mStatusIcon.setVisibility(View.VISIBLE); - textColor = ContextCompat.getColor(context, R.color.key_flag_gray); - } else if (cursor.isExpired()) { - KeyFormattingUtils.setStatusImage( - context, - mStatusIcon, - null, - KeyFormattingUtils.State.EXPIRED, - R.color.key_flag_gray - ); - - itemView.setEnabled(false); - mStatusIcon.setVisibility(View.VISIBLE); - textColor = ContextCompat.getColor(context, R.color.key_flag_gray); - } else { - itemView.setEnabled(true); - mStatusIcon.setVisibility(View.GONE); - textColor = FormattingUtils.getColorFromAttr(context, R.attr.colorText); - } - - mUserIdText.setTextColor(textColor); - mUserIdRestText.setTextColor(textColor); - - if (cursor.hasDuplicate()) { - String dateTime = DateUtils.formatDateTime(context, - cursor.getCreationTime(), - DateUtils.FORMAT_SHOW_DATE - | DateUtils.FORMAT_SHOW_TIME - | DateUtils.FORMAT_SHOW_YEAR - | DateUtils.FORMAT_ABBREV_MONTH); - mCreationText.setText(context.getString(R.string.label_key_created, - dateTime)); - mCreationText.setTextColor(textColor); - mCreationText.setVisibility(View.VISIBLE); - } else { - mCreationText.setVisibility(View.GONE); - } - } - - } - - @Override - public void onClick(View v) { - if (mListener != null) { - mListener.onSelectKeyItemClicked(getItemId()); - } - } - } - - public interface SelectSignKeyListener { - void onCreateKeyDummyClicked(); - - void onSelectKeyItemClicked(long masterKeyId); - } -} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/DialogKeyChoiceAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/DialogKeyChoiceAdapter.java new file mode 100644 index 000000000..7e3b0864f --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/DialogKeyChoiceAdapter.java @@ -0,0 +1,109 @@ +package org.sufficientlysecure.keychain.remote.ui.dialog; + + +import java.util.List; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.support.annotation.NonNull; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.RecyclerView.Adapter; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.model.SubKey.UnifiedKeyInfo; +import org.sufficientlysecure.keychain.remote.ui.dialog.DialogKeyChoiceAdapter.KeyChoiceViewHolder; +import org.sufficientlysecure.keychain.ui.util.KeyInfoFormatter; + + +class DialogKeyChoiceAdapter extends Adapter { + private final LayoutInflater layoutInflater; + private List data; + private Drawable iconUnselected; + private Drawable iconSelected; + private Integer activeItem; + private KeyInfoFormatter keyInfoFormatter; + + DialogKeyChoiceAdapter(Context context, LayoutInflater layoutInflater) { + this.layoutInflater = layoutInflater; + this.keyInfoFormatter = new KeyInfoFormatter(context); + } + + @NonNull + @Override + public KeyChoiceViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View keyChoiceItemView = layoutInflater.inflate(R.layout.api_select_identity_item, parent, false); + return new KeyChoiceViewHolder(keyChoiceItemView); + } + + void setActiveItem(Integer activeItem) { + this.activeItem = activeItem; + notifyDataSetChanged(); + } + + @Override + public void onBindViewHolder(@NonNull KeyChoiceViewHolder holder, int position) { + UnifiedKeyInfo keyInfo = data.get(position); + boolean hasActiveItem = activeItem != null; + boolean isActiveItem = hasActiveItem && position == activeItem; + + Drawable icon = isActiveItem ? iconSelected : iconUnselected; + holder.bind(keyInfo, icon); + + holder.itemView.setVisibility(!hasActiveItem || isActiveItem ? View.VISIBLE : View.INVISIBLE); + } + + @Override + public int getItemCount() { + return data != null ? data.size() : 0; + } + + public void setData(List data) { + this.data = data; + notifyDataSetChanged(); + } + + void setSelectionDrawables(Drawable iconSelected, Drawable iconUnselected) { + this.iconSelected = iconSelected; + this.iconUnselected = iconUnselected; + + notifyDataSetChanged(); + } + + class KeyChoiceViewHolder extends RecyclerView.ViewHolder { + private final TextView vName; + private final TextView vCreation = (TextView) itemView.findViewById(R.id.key_list_item_creation); + private final ImageView vIcon; + + KeyChoiceViewHolder(View itemView) { + super(itemView); + + vName = itemView.findViewById(R.id.key_list_item_name); + vIcon = itemView.findViewById(R.id.key_list_item_icon); + } + + void bind(UnifiedKeyInfo keyInfo, Drawable selectionIcon) { + Context context = vCreation.getContext(); + + keyInfoFormatter.setKeyInfo(keyInfo); + + String email = keyInfo.email(); + String name = keyInfo.name(); + if (email != null) { + vName.setText(context.getString(R.string.use_key, email)); + } else if (name != null) { + vName.setText(context.getString(R.string.use_key, name)); + } else { + vName.setText(context.getString(R.string.use_key_no_name)); + } + + keyInfoFormatter.formatCreationDate(vCreation); + + vIcon.setImageDrawable(selectionIcon); + } + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/KeyInfoLoader.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/KeyInfoLoader.java deleted file mode 100644 index 31c87ac58..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/KeyInfoLoader.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright (C) 2017 Schürmann & Breitmoser GbR - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sufficientlysecure.keychain.remote.ui.dialog; - - -import java.util.List; - -import android.content.ContentResolver; -import android.content.Context; -import android.support.v4.content.AsyncTaskLoader; - -import org.sufficientlysecure.keychain.livedata.KeyInfoInteractor; -import org.sufficientlysecure.keychain.livedata.KeyInfoInteractor.KeyInfo; -import org.sufficientlysecure.keychain.livedata.KeyInfoInteractor.KeySelector; - - -public class KeyInfoLoader extends AsyncTaskLoader> { - private final KeySelector keySelector; - - private List cachedResult; - private KeyInfoInteractor keyInfoInteractor; - - KeyInfoLoader(Context context, ContentResolver contentResolver, KeySelector keySelector) { - super(context); - - this.keySelector = keySelector; - this.keyInfoInteractor = new KeyInfoInteractor(contentResolver); - } - - @Override - public List loadInBackground() { - return keyInfoInteractor.loadKeyInfo(keySelector); - } - - @Override - public void deliverResult(List keySubkeyStatus) { - cachedResult = keySubkeyStatus; - - if (isStarted()) { - super.deliverResult(keySubkeyStatus); - } - } - - @Override - protected void onStartLoading() { - if (cachedResult != null) { - deliverResult(cachedResult); - } - - if (takeContentChanged() || cachedResult == null) { - forceLoad(); - } - } - - @Override - protected void onStopLoading() { - super.onStopLoading(); - - cachedResult = null; - } -} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteDeduplicateActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteDeduplicateActivity.java index adb079821..057d981a8 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteDeduplicateActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteDeduplicateActivity.java @@ -23,48 +23,42 @@ import java.util.List; import android.annotation.SuppressLint; import android.app.Activity; import android.app.Dialog; +import android.arch.lifecycle.LiveData; +import android.arch.lifecycle.ViewModel; +import android.arch.lifecycle.ViewModelProviders; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; -import android.content.res.Resources; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.Drawable.ConstantState; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.v4.app.DialogFragment; import android.support.v4.app.FragmentActivity; -import android.support.v4.content.res.ResourcesCompat; -import android.support.v4.graphics.drawable.DrawableCompat; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView.Adapter; -import android.text.format.DateUtils; import android.view.ContextThemeWrapper; import android.view.LayoutInflater; import android.view.View; -import android.view.View.OnClickListener; -import android.view.ViewGroup; import android.widget.Button; -import android.widget.ImageView; import android.widget.TextView; +import android.widget.Toast; import com.mikepenz.materialdrawer.util.KeyboardUtil; import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.livedata.GenericLiveData; +import org.sufficientlysecure.keychain.model.SubKey.UnifiedKeyInfo; +import org.sufficientlysecure.keychain.daos.KeyRepository; import org.sufficientlysecure.keychain.remote.ui.RemoteSecurityTokenOperationActivity; -import org.sufficientlysecure.keychain.livedata.KeyInfoInteractor.KeyInfo; import org.sufficientlysecure.keychain.remote.ui.dialog.RemoteDeduplicatePresenter.RemoteDeduplicateView; import org.sufficientlysecure.keychain.ui.dialog.CustomAlertDialogBuilder; import org.sufficientlysecure.keychain.ui.util.ThemeChanger; import org.sufficientlysecure.keychain.ui.util.recyclerview.DividerItemDecoration; -import org.sufficientlysecure.keychain.ui.util.recyclerview.RecyclerItemClickListener; public class RemoteDeduplicateActivity extends FragmentActivity { public static final String EXTRA_PACKAGE_NAME = "package_name"; public static final String EXTRA_DUPLICATE_EMAILS = "duplicate_emails"; - public static final int LOADER_ID_KEYS = 0; - private RemoteDeduplicatePresenter presenter; @@ -73,7 +67,7 @@ public class RemoteDeduplicateActivity extends FragmentActivity { protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - presenter = new RemoteDeduplicatePresenter(getBaseContext(), LOADER_ID_KEYS); + presenter = new RemoteDeduplicatePresenter(getBaseContext(), this); KeyboardUtil.hideKeyboard(this); @@ -92,8 +86,43 @@ public class RemoteDeduplicateActivity extends FragmentActivity { String duplicateAddress = dupAddresses.get(0); String packageName = intent.getStringExtra(EXTRA_PACKAGE_NAME); - presenter.setupFromIntentData(packageName, duplicateAddress); - presenter.startLoaders(getSupportLoaderManager()); + DeduplicateViewModel viewModel = ViewModelProviders.of(this).get(DeduplicateViewModel.class); + viewModel.setDuplicateAddress(duplicateAddress); + viewModel.setPackageName(packageName); + + presenter.setupFromViewModel(viewModel); + } + + public static class DeduplicateViewModel extends ViewModel { + private String duplicateAddress; + private LiveData> keyInfoLiveData; + private String packageName; + + public LiveData> getKeyInfoLiveData(Context context) { + if (keyInfoLiveData == null) { + keyInfoLiveData = new GenericLiveData<>(context, () -> { + KeyRepository keyRepository = KeyRepository.create(context); + return keyRepository.getUnifiedKeyInfosByMailAddress(duplicateAddress); + }); + } + return keyInfoLiveData; + } + + public void setDuplicateAddress(String duplicateAddress) { + this.duplicateAddress = duplicateAddress; + } + + public void setPackageName(String packageName) { + this.packageName = packageName; + } + + public String getPackageName() { + return packageName; + } + + public String getDuplicateAddress() { + return duplicateAddress; + } } public static class RemoteDeduplicateDialogFragment extends DialogFragment { @@ -107,7 +136,7 @@ public class RemoteDeduplicateActivity extends FragmentActivity { @NonNull @Override public Dialog onCreateDialog(Bundle savedInstanceState) { - final Activity activity = getActivity(); + Activity activity = requireActivity(); ContextThemeWrapper theme = ThemeChanger.getDialogThemeWrapper(activity); CustomAlertDialogBuilder alert = new CustomAlertDialogBuilder(theme); @@ -126,7 +155,7 @@ public class RemoteDeduplicateActivity extends FragmentActivity { new DividerItemDecoration(activity, DividerItemDecoration.VERTICAL_LIST, true)); setupListenersForPresenter(); - mvpView = createMvpView(view, layoutInflater); + mvpView = createMvpView(view); return alert.create(); } @@ -135,7 +164,7 @@ public class RemoteDeduplicateActivity extends FragmentActivity { public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - presenter = ((RemoteDeduplicateActivity) getActivity()).presenter; + presenter = ((RemoteDeduplicateActivity) requireActivity()).presenter; presenter.setView(mvpView); } @@ -159,11 +188,8 @@ public class RemoteDeduplicateActivity extends FragmentActivity { } @NonNull - private RemoteDeduplicateView createMvpView(View view, LayoutInflater layoutInflater) { - final ImageView iconClientApp = view.findViewById(R.id.icon_client_app); - final KeyChoiceAdapter keyChoiceAdapter = new KeyChoiceAdapter(layoutInflater, getResources()); + private RemoteDeduplicateView createMvpView(View view) { final TextView addressText = view.findViewById(R.id.select_key_item_name); - keyChoiceList.setAdapter(keyChoiceAdapter); return new RemoteDeduplicateView() { @Override @@ -191,9 +217,8 @@ public class RemoteDeduplicateActivity extends FragmentActivity { } @Override - public void setTitleClientIcon(Drawable drawable) { - iconClientApp.setImageDrawable(drawable); - keyChoiceAdapter.setSelectionDrawable(drawable); + public void showNoSelectionError() { + Toast.makeText(getContext(), "No key selected!", Toast.LENGTH_SHORT).show(); } @Override @@ -202,133 +227,15 @@ public class RemoteDeduplicateActivity extends FragmentActivity { } @Override - public void setKeyListData(List data) { - keyChoiceAdapter.setData(data); - } - - @Override - public void setActiveItem(Integer position) { - keyChoiceAdapter.setActiveItem(position); - } - - @Override - public void setEnableSelectButton(boolean enabled) { - buttonSelect.setEnabled(enabled); + public void setKeyListAdapter(Adapter adapter) { + keyChoiceList.setAdapter(adapter); } }; } private void setupListenersForPresenter() { - buttonSelect.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View view) { - presenter.onClickSelect(); - } - }); - - buttonCancel.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View view) { - presenter.onClickCancel(); - } - }); - - keyChoiceList.addOnItemTouchListener(new RecyclerItemClickListener(getContext(), - new RecyclerItemClickListener.OnItemClickListener() { - @Override - public void onItemClick(View view, int position) { - presenter.onKeyItemClick(position); - } - })); - } - } - - private static class KeyChoiceAdapter extends Adapter { - private final LayoutInflater layoutInflater; - private final Resources resources; - private List data; - private Drawable iconUnselected; - private Drawable iconSelected; - private Integer activeItem; - - KeyChoiceAdapter(LayoutInflater layoutInflater, Resources resources) { - this.layoutInflater = layoutInflater; - this.resources = resources; - } - - @Override - public KeyChoiceViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - View keyChoiceItemView = layoutInflater.inflate(R.layout.duplicate_key_item, parent, false); - return new KeyChoiceViewHolder(keyChoiceItemView); - } - - @Override - public void onBindViewHolder(KeyChoiceViewHolder holder, int position) { - KeyInfo keyInfo = data.get(position); - Drawable icon = (activeItem != null && position == activeItem) ? iconSelected : iconUnselected; - holder.bind(keyInfo, icon); - } - - @Override - public int getItemCount() { - return data != null ? data.size() : 0; - } - - public void setData(List data) { - this.data = data; - notifyDataSetChanged(); - } - - void setSelectionDrawable(Drawable drawable) { - ConstantState constantState = drawable.getConstantState(); - if (constantState == null) { - return; - } - - iconSelected = constantState.newDrawable(resources); - - iconUnselected = constantState.newDrawable(resources); - DrawableCompat.setTint(iconUnselected.mutate(), ResourcesCompat.getColor(resources, R.color.md_grey_300, null)); - - notifyDataSetChanged(); - } - - void setActiveItem(Integer newActiveItem) { - Integer prevActiveItem = this.activeItem; - this.activeItem = newActiveItem; - - if (prevActiveItem != null) { - notifyItemChanged(prevActiveItem); - } - if (newActiveItem != null) { - notifyItemChanged(newActiveItem); - } - } - } - - private static class KeyChoiceViewHolder extends RecyclerView.ViewHolder { - private final TextView vName; - private final TextView vCreation; - private final ImageView vIcon; - - KeyChoiceViewHolder(View itemView) { - super(itemView); - - vName = itemView.findViewById(R.id.key_list_item_name); - vCreation = itemView.findViewById(R.id.key_list_item_creation); - vIcon = itemView.findViewById(R.id.key_list_item_icon); - } - - void bind(KeyInfo keyInfo, Drawable selectionIcon) { - vName.setText(keyInfo.getName()); - - Context context = vCreation.getContext(); - String dateTime = DateUtils.formatDateTime(context, keyInfo.getCreationDate(), - DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME | - DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_ABBREV_MONTH); - vCreation.setText(context.getString(R.string.label_key_created, dateTime)); - - vIcon.setImageDrawable(selectionIcon); + buttonSelect.setOnClickListener(view -> presenter.onClickSelect()); + buttonCancel.setOnClickListener(view -> presenter.onClickCancel()); } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteDeduplicatePresenter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteDeduplicatePresenter.java index ffda9a81d..6464b4add 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteDeduplicatePresenter.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteDeduplicatePresenter.java @@ -18,111 +18,78 @@ package org.sufficientlysecure.keychain.remote.ui.dialog; -import java.util.Date; import java.util.List; +import android.arch.lifecycle.LifecycleOwner; import android.content.Context; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; -import android.content.pm.PackageManager.NameNotFoundException; -import android.graphics.drawable.Drawable; -import android.os.Bundle; -import android.support.v4.app.LoaderManager; -import android.support.v4.app.LoaderManager.LoaderCallbacks; -import android.support.v4.content.Loader; +import android.support.v7.widget.RecyclerView.Adapter; -import org.sufficientlysecure.keychain.provider.AutocryptPeerDataAccessObject; -import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; -import org.sufficientlysecure.keychain.livedata.KeyInfoInteractor.KeyInfo; -import org.sufficientlysecure.keychain.livedata.KeyInfoInteractor.KeySelector; -import timber.log.Timber; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.model.SubKey.UnifiedKeyInfo; +import org.sufficientlysecure.keychain.remote.AutocryptInteractor; +import org.sufficientlysecure.keychain.remote.ui.dialog.RemoteDeduplicateActivity.DeduplicateViewModel; +import org.sufficientlysecure.keychain.ui.adapter.KeyChoiceAdapter; -class RemoteDeduplicatePresenter implements LoaderCallbacks> { - private final PackageManager packageManager; +class RemoteDeduplicatePresenter { private final Context context; - private final int loaderId; + private final LifecycleOwner lifecycleOwner; - private AutocryptPeerDataAccessObject autocryptPeerDao; - private String duplicateAddress; + private AutocryptInteractor autocryptInteractor; + private DeduplicateViewModel viewModel; private RemoteDeduplicateView view; - private Integer selectedItem; - private List keyInfoData; + private KeyChoiceAdapter keyChoiceAdapter; - RemoteDeduplicatePresenter(Context context, int loaderId) { + RemoteDeduplicatePresenter(Context context, LifecycleOwner lifecycleOwner) { this.context = context; - - packageManager = context.getPackageManager(); - - this.loaderId = loaderId; + this.lifecycleOwner = lifecycleOwner; } public void setView(RemoteDeduplicateView view) { this.view = view; } - void setupFromIntentData(String packageName, String duplicateAddress) { - try { - setPackageInfo(packageName); - } catch (NameNotFoundException e) { - Timber.e("Unable to find info of calling app!"); - view.finishAsCancelled(); - return; - } + void setupFromViewModel(DeduplicateViewModel viewModel) { + this.viewModel = viewModel; + this.autocryptInteractor = AutocryptInteractor.getInstance(context, viewModel.getPackageName()); - autocryptPeerDao = new AutocryptPeerDataAccessObject(context, packageName); + view.setAddressText(viewModel.getDuplicateAddress()); - this.duplicateAddress = duplicateAddress; - view.setAddressText(duplicateAddress); + viewModel.getKeyInfoLiveData(context).observe(lifecycleOwner, this::onLoadKeyInfos); } - private void setPackageInfo(String packageName) throws NameNotFoundException { - ApplicationInfo applicationInfo = packageManager.getApplicationInfo(packageName, 0); - Drawable appIcon = packageManager.getApplicationIcon(applicationInfo); - - view.setTitleClientIcon(appIcon); - } - - void startLoaders(LoaderManager loaderManager) { - loaderManager.restartLoader(loaderId, null, this); - } - - @Override - public Loader> onCreateLoader(int id, Bundle args) { - KeySelector keySelector = KeySelector.create( - KeyRings.buildUnifiedKeyRingsFindByEmailUri(duplicateAddress), null); - - return new KeyInfoLoader(context, context.getContentResolver(), keySelector); - } - - @Override - public void onLoadFinished(Loader> loader, List data) { - this.keyInfoData = data; - view.setKeyListData(data); - } - - @Override - public void onLoaderReset(Loader loader) { - if (view != null) { - view.setKeyListData(null); + private void onLoadKeyInfos(List data) { + if (keyChoiceAdapter == null) { + keyChoiceAdapter = KeyChoiceAdapter.createSingleChoiceAdapter(context, data, (keyInfo -> { + if (keyInfo.is_revoked()) { + return R.string.keychoice_revoked; + } else if (keyInfo.is_expired()) { + return R.string.keychoice_expired; + } else if (!keyInfo.is_secure()) { + return R.string.keychoice_insecure; + } else if (!keyInfo.has_encrypt_key()) { + return R.string.keychoice_cannot_encrypt; + } else { + return null; + } + })); + view.setKeyListAdapter(keyChoiceAdapter); + } else { + keyChoiceAdapter.setUnifiedKeyInfoItems(data); } } void onClickSelect() { - if (keyInfoData == null) { - Timber.e("got click on select with no data…?"); + UnifiedKeyInfo activeItem = keyChoiceAdapter.getActiveItem(); + if (activeItem == null) { + view.showNoSelectionError(); return; } - if (selectedItem == null) { - Timber.e("got click on select with no selection…?"); - return; - } - - long masterKeyId = keyInfoData.get(selectedItem).getMasterKeyId(); - autocryptPeerDao.updateKeyGossipFromDedup(duplicateAddress, new Date(), masterKeyId); + long masterKeyId = activeItem.master_key_id(); + autocryptInteractor.updateKeyGossipFromDedup(viewModel.getDuplicateAddress(), masterKeyId); view.finish(); } @@ -135,25 +102,13 @@ class RemoteDeduplicatePresenter implements LoaderCallbacks> { view.finishAsCancelled(); } - void onKeyItemClick(int position) { - if (selectedItem != null && position == selectedItem) { - selectedItem = null; - } else { - selectedItem = position; - } - view.setActiveItem(selectedItem); - view.setEnableSelectButton(selectedItem != null); - } - interface RemoteDeduplicateView { + void showNoSelectionError(); void finish(); void finishAsCancelled(); void setAddressText(String text); - void setTitleClientIcon(Drawable drawable); - void setKeyListData(List data); - void setActiveItem(Integer position); - void setEnableSelectButton(boolean enabled); + void setKeyListAdapter(Adapter adapter); } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteSelectAuthenticationKeyActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteSelectAuthenticationKeyActivity.java index 367fc496b..931ddb44a 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteSelectAuthenticationKeyActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteSelectAuthenticationKeyActivity.java @@ -18,16 +18,20 @@ package org.sufficientlysecure.keychain.remote.ui.dialog; +import java.util.List; + import android.annotation.SuppressLint; import android.app.Activity; import android.app.Dialog; +import android.arch.lifecycle.LiveData; +import android.arch.lifecycle.ViewModel; +import android.arch.lifecycle.ViewModelProviders; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.res.Resources; import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable.ConstantState; -import android.net.Uri; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.v4.app.DialogFragment; @@ -36,48 +40,40 @@ import android.support.v4.content.res.ResourcesCompat; import android.support.v4.graphics.drawable.DrawableCompat; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; -import android.support.v7.widget.RecyclerView.Adapter; -import android.text.format.DateUtils; import android.view.ContextThemeWrapper; import android.view.LayoutInflater; import android.view.View; -import android.view.View.OnClickListener; -import android.view.ViewGroup; import android.widget.Button; import android.widget.ImageView; -import android.widget.TextView; import com.mikepenz.materialdrawer.util.KeyboardUtil; - import org.openintents.ssh.authentication.SshAuthenticationApi; import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.provider.ApiDataAccessObject; -import org.sufficientlysecure.keychain.provider.KeychainContract; +import org.sufficientlysecure.keychain.daos.ApiAppDao; +import org.sufficientlysecure.keychain.daos.KeyRepository; +import org.sufficientlysecure.keychain.livedata.GenericLiveData; +import org.sufficientlysecure.keychain.model.SubKey.UnifiedKeyInfo; import org.sufficientlysecure.keychain.remote.ui.RemoteSecurityTokenOperationActivity; -import org.sufficientlysecure.keychain.livedata.KeyInfoInteractor.KeyInfo; import org.sufficientlysecure.keychain.remote.ui.dialog.RemoteSelectAuthenticationKeyPresenter.RemoteSelectAuthenticationKeyView; import org.sufficientlysecure.keychain.ui.dialog.CustomAlertDialogBuilder; import org.sufficientlysecure.keychain.ui.util.ThemeChanger; import org.sufficientlysecure.keychain.ui.util.recyclerview.DividerItemDecoration; import org.sufficientlysecure.keychain.ui.util.recyclerview.RecyclerItemClickListener; -import java.util.List; - public class RemoteSelectAuthenticationKeyActivity extends FragmentActivity { public static final String EXTRA_PACKAGE_NAME = "package_name"; - public static final int LOADER_ID_KEYS = 0; - private RemoteSelectAuthenticationKeyPresenter presenter; + private String packageName; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - presenter = new RemoteSelectAuthenticationKeyPresenter(getBaseContext(), LOADER_ID_KEYS); + presenter = new RemoteSelectAuthenticationKeyPresenter(getBaseContext(), this); KeyboardUtil.hideKeyboard(this); @@ -92,11 +88,35 @@ public class RemoteSelectAuthenticationKeyActivity extends FragmentActivity { super.onStart(); Intent intent = getIntent(); - String packageName = intent.getStringExtra(EXTRA_PACKAGE_NAME); + packageName = intent.getStringExtra(EXTRA_PACKAGE_NAME); + SelectAuthKeyViewModel viewModel = ViewModelProviders.of(this).get(SelectAuthKeyViewModel.class); + viewModel.setPackageName(packageName); - presenter.setupFromIntentData(packageName); - presenter.startLoaders(getSupportLoaderManager()); + presenter.setupFromViewModel(viewModel); + } + + public static class SelectAuthKeyViewModel extends ViewModel { + private LiveData> keyInfoLiveData; + private String packageName; + + public LiveData> getKeyInfoLiveData(Context context) { + if (keyInfoLiveData == null) { + keyInfoLiveData = new GenericLiveData<>(context, () -> { + KeyRepository keyRepository = KeyRepository.create(context); + return keyRepository.getAllUnifiedKeyInfoWithSecret(); + }); + } + return keyInfoLiveData; + } + + public void setPackageName(String packageName) { + this.packageName = packageName; + } + + public String getPackageName() { + return packageName; + } } private void onKeySelected(long masterKeyId) { @@ -104,14 +124,8 @@ public class RemoteSelectAuthenticationKeyActivity extends FragmentActivity { Intent originalIntent = callingIntent.getParcelableExtra( RemoteSecurityTokenOperationActivity.EXTRA_DATA); - Uri appUri = callingIntent.getData(); - - Uri allowedKeysUri = appUri.buildUpon() - .appendPath(KeychainContract.PATH_ALLOWED_KEYS) - .build(); - - ApiDataAccessObject apiDao = new ApiDataAccessObject(getBaseContext()); - apiDao.addAllowedKeyIdForApp(allowedKeysUri, masterKeyId); + ApiAppDao apiAppDao = ApiAppDao.getInstance(getBaseContext()); + apiAppDao.addAllowedKeyIdForApp(packageName, masterKeyId); originalIntent.putExtra(SshAuthenticationApi.EXTRA_KEY_ID, String.valueOf(masterKeyId)); @@ -130,7 +144,7 @@ public class RemoteSelectAuthenticationKeyActivity extends FragmentActivity { @NonNull @Override public Dialog onCreateDialog(Bundle savedInstanceState) { - final Activity activity = getActivity(); + Activity activity = requireActivity(); ContextThemeWrapper theme = ThemeChanger.getDialogThemeWrapper(activity); CustomAlertDialogBuilder alert = new CustomAlertDialogBuilder(theme); @@ -158,7 +172,7 @@ public class RemoteSelectAuthenticationKeyActivity extends FragmentActivity { public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - presenter = ((RemoteSelectAuthenticationKeyActivity) getActivity()).presenter; + presenter = ((RemoteSelectAuthenticationKeyActivity) requireActivity()).presenter; presenter.setView(mvpView); } @@ -184,7 +198,7 @@ public class RemoteSelectAuthenticationKeyActivity extends FragmentActivity { @NonNull private RemoteSelectAuthenticationKeyView createMvpView(View view, LayoutInflater layoutInflater) { final ImageView iconClientApp = view.findViewById(R.id.icon_client_app); - final KeyChoiceAdapter keyChoiceAdapter = new KeyChoiceAdapter(layoutInflater, getResources()); + final DialogKeyChoiceAdapter keyChoiceAdapter = new DialogKeyChoiceAdapter(requireContext(), layoutInflater); keyChoiceList.setAdapter(keyChoiceAdapter); return new RemoteSelectAuthenticationKeyView() { @@ -212,11 +226,18 @@ public class RemoteSelectAuthenticationKeyActivity extends FragmentActivity { @Override public void setTitleClientIcon(Drawable drawable) { iconClientApp.setImageDrawable(drawable); - keyChoiceAdapter.setSelectionDrawable(drawable); + + Resources resources = getResources(); + ConstantState constantState = drawable.getConstantState(); + Drawable iconSelected = constantState.newDrawable(resources); + Drawable iconUnselected = constantState.newDrawable(resources); + DrawableCompat.setTint(iconUnselected.mutate(), ResourcesCompat.getColor(resources, R.color.md_grey_300, null)); + + keyChoiceAdapter.setSelectionDrawables(iconSelected, iconUnselected); } @Override - public void setKeyListData(List data) { + public void setKeyListData(List data) { keyChoiceAdapter.setData(data); } @@ -233,117 +254,10 @@ public class RemoteSelectAuthenticationKeyActivity extends FragmentActivity { } private void setupListenersForPresenter() { - buttonSelect.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View view) { - presenter.onClickSelect(); - } - }); - - buttonCancel.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View view) { - presenter.onClickCancel(); - } - }); - + buttonSelect.setOnClickListener(view -> presenter.onClickSelect()); + buttonCancel.setOnClickListener(view -> presenter.onClickCancel()); keyChoiceList.addOnItemTouchListener(new RecyclerItemClickListener(getContext(), - new RecyclerItemClickListener.OnItemClickListener() { - @Override - public void onItemClick(View view, int position) { - presenter.onKeyItemClick(position); - } - })); + (view, position) -> presenter.onKeyItemClick(position))); } } - - private static class KeyChoiceAdapter extends Adapter { - private final LayoutInflater layoutInflater; - private final Resources resources; - private List data; - private Drawable iconUnselected; - private Drawable iconSelected; - private Integer activeItem; - - KeyChoiceAdapter(LayoutInflater layoutInflater, Resources resources) { - this.layoutInflater = layoutInflater; - this.resources = resources; - } - - @Override - public KeyChoiceViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - View keyChoiceItemView = layoutInflater.inflate(R.layout.authentication_key_item, parent, false); - return new KeyChoiceViewHolder(keyChoiceItemView); - } - - @Override - public void onBindViewHolder(KeyChoiceViewHolder holder, int position) { - KeyInfo keyInfo = data.get(position); - Drawable icon = (activeItem != null && position == activeItem) ? iconSelected : iconUnselected; - holder.bind(keyInfo, icon); - } - - @Override - public int getItemCount() { - return data != null ? data.size() : 0; - } - - public void setData(List data) { - this.data = data; - notifyDataSetChanged(); - } - - void setSelectionDrawable(Drawable drawable) { - ConstantState constantState = drawable.getConstantState(); - if (constantState == null) { - return; - } - - iconSelected = constantState.newDrawable(resources); - - iconUnselected = constantState.newDrawable(resources); - DrawableCompat.setTint(iconUnselected.mutate(), ResourcesCompat.getColor(resources, R.color.md_grey_300, null)); - - notifyDataSetChanged(); - } - - void setActiveItem(Integer newActiveItem) { - Integer prevActiveItem = this.activeItem; - this.activeItem = newActiveItem; - - if (prevActiveItem != null) { - notifyItemChanged(prevActiveItem); - } - if (newActiveItem != null) { - notifyItemChanged(newActiveItem); - } - } - } - - private static class KeyChoiceViewHolder extends RecyclerView.ViewHolder { - private final TextView vName; - private final TextView vCreation; - private final ImageView vIcon; - - KeyChoiceViewHolder(View itemView) { - super(itemView); - - vName = itemView.findViewById(R.id.key_list_item_name); - vCreation = itemView.findViewById(R.id.key_list_item_creation); - vIcon = itemView.findViewById(R.id.key_list_item_icon); - } - - void bind(KeyInfo keyInfo, Drawable selectionIcon) { - vName.setText(keyInfo.getName()); - - Context context = vCreation.getContext(); - String dateTime = DateUtils.formatDateTime(context, keyInfo.getCreationDate(), - DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME | - DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_ABBREV_MONTH); - vCreation.setText(context.getString(R.string.label_key_created, dateTime)); - - vIcon.setImageDrawable(selectionIcon); - } - } - } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteSelectAuthenticationKeyPresenter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteSelectAuthenticationKeyPresenter.java index c9e0f70b0..24f93520a 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteSelectAuthenticationKeyPresenter.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteSelectAuthenticationKeyPresenter.java @@ -20,52 +20,49 @@ package org.sufficientlysecure.keychain.remote.ui.dialog; import java.util.List; +import android.arch.lifecycle.LifecycleOwner; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.graphics.drawable.Drawable; -import android.os.Bundle; -import android.support.v4.app.LoaderManager; -import android.support.v4.app.LoaderManager.LoaderCallbacks; -import android.support.v4.content.Loader; -import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; -import org.sufficientlysecure.keychain.livedata.KeyInfoInteractor.KeyInfo; -import org.sufficientlysecure.keychain.livedata.KeyInfoInteractor.KeySelector; +import org.sufficientlysecure.keychain.model.SubKey.UnifiedKeyInfo; +import org.sufficientlysecure.keychain.remote.ui.dialog.RemoteSelectAuthenticationKeyActivity.SelectAuthKeyViewModel; import timber.log.Timber; -class RemoteSelectAuthenticationKeyPresenter implements LoaderCallbacks> { +class RemoteSelectAuthenticationKeyPresenter { private final PackageManager packageManager; + private final LifecycleOwner lifecycleOwner; private final Context context; - private final int loaderId; private RemoteSelectAuthenticationKeyView view; private Integer selectedItem; - private List keyInfoData; + private List keyInfoData; - RemoteSelectAuthenticationKeyPresenter(Context context, int loaderId) { + RemoteSelectAuthenticationKeyPresenter(Context context, LifecycleOwner lifecycleOwner) { this.context = context; + this.lifecycleOwner = lifecycleOwner; packageManager = context.getPackageManager(); - - this.loaderId = loaderId; } public void setView(RemoteSelectAuthenticationKeyView view) { this.view = view; } - void setupFromIntentData(String packageName) { + void setupFromViewModel(SelectAuthKeyViewModel viewModel) { try { - setPackageInfo(packageName); + setPackageInfo(viewModel.getPackageName()); } catch (NameNotFoundException e) { Timber.e("Unable to find info of calling app!"); view.finishAsCancelled(); } + + viewModel.getKeyInfoLiveData(context).observe(lifecycleOwner, this::onLoadKeyInfos); } private void setPackageInfo(String packageName) throws NameNotFoundException { @@ -75,31 +72,11 @@ class RemoteSelectAuthenticationKeyPresenter implements LoaderCallbacks> onCreateLoader(int id, Bundle args) { - String selection = KeyRings.HAS_AUTHENTICATE_SECRET + " != 0"; - KeySelector keySelector = KeySelector.create( - KeyRings.buildUnifiedKeyRingsUri(), selection); - return new KeyInfoLoader(context, context.getContentResolver(), keySelector); - } - - @Override - public void onLoadFinished(Loader> loader, List data) { + private void onLoadKeyInfos(List data) { this.keyInfoData = data; view.setKeyListData(data); } - @Override - public void onLoaderReset(Loader loader) { - if (view != null) { - view.setKeyListData(null); - } - } - void onClickSelect() { if (keyInfoData == null) { Timber.e("got click on select with no data…?"); @@ -110,7 +87,7 @@ class RemoteSelectAuthenticationKeyPresenter implements LoaderCallbacks data); + void setKeyListData(List data); void setActiveItem(Integer position); void setEnableSelectButton(boolean enabled); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteSelectIdKeyActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteSelectIdKeyActivity.java index fa9674e73..3370ebfb6 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteSelectIdKeyActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteSelectIdKeyActivity.java @@ -23,6 +23,8 @@ import java.util.List; import android.annotation.SuppressLint; import android.app.Activity; import android.app.Dialog; +import android.arch.lifecycle.LiveData; +import android.arch.lifecycle.ViewModel; import android.arch.lifecycle.ViewModelProviders; import android.content.Context; import android.content.DialogInterface; @@ -43,8 +45,6 @@ import android.support.v4.content.res.ResourcesCompat; import android.support.v4.graphics.drawable.DrawableCompat; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; -import android.support.v7.widget.RecyclerView.Adapter; -import android.text.format.DateUtils; import android.view.ContextThemeWrapper; import android.view.LayoutInflater; import android.view.View; @@ -58,7 +58,10 @@ import android.widget.Toast; import com.mikepenz.materialdrawer.util.KeyboardUtil; import org.openintents.openpgp.util.OpenPgpApi; import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.livedata.KeyInfoInteractor.KeyInfo; +import org.sufficientlysecure.keychain.daos.KeyRepository; +import org.sufficientlysecure.keychain.livedata.GenericLiveData; +import org.sufficientlysecure.keychain.livedata.PgpKeyGenerationLiveData; +import org.sufficientlysecure.keychain.model.SubKey.UnifiedKeyInfo; import org.sufficientlysecure.keychain.operations.results.ImportKeyResult; import org.sufficientlysecure.keychain.remote.ui.dialog.RemoteSelectIdentityKeyPresenter.RemoteSelectIdentityKeyView; import org.sufficientlysecure.keychain.service.ImportKeyringParcel; @@ -89,13 +92,18 @@ public class RemoteSelectIdKeyActivity extends FragmentActivity { protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - RemoteSelectIdViewModel viewModel = - ViewModelProviders.of(this).get(RemoteSelectIdViewModel.class); - - presenter = new RemoteSelectIdentityKeyPresenter(getBaseContext(), viewModel, this); + presenter = new RemoteSelectIdentityKeyPresenter(getBaseContext(), this); KeyboardUtil.hideKeyboard(this); + RemoteSelectIdViewModel viewModel = ViewModelProviders.of(this).get(RemoteSelectIdViewModel.class); + + Intent intent = getIntent(); + viewModel.rawUserId = intent.getStringExtra(EXTRA_USER_ID); + viewModel.packageName = intent.getStringExtra(EXTRA_PACKAGE_NAME); + viewModel.packageSignature = intent.getByteArrayExtra(EXTRA_PACKAGE_SIGNATURE); + viewModel.clientHasAutocryptSetupMsg = intent.getBooleanExtra(EXTRA_SHOW_AUTOCRYPT_HINT, false); + if (savedInstanceState == null) { RemoteSelectIdentityKeyDialogFragment frag = new RemoteSelectIdentityKeyDialogFragment(); frag.show(getSupportFragmentManager(), "requestKeyDialog"); @@ -106,13 +114,45 @@ public class RemoteSelectIdKeyActivity extends FragmentActivity { protected void onStart() { super.onStart(); - Intent intent = getIntent(); - String userId = intent.getStringExtra(EXTRA_USER_ID); - String packageName = intent.getStringExtra(EXTRA_PACKAGE_NAME); - byte[] packageSignature = intent.getByteArrayExtra(EXTRA_PACKAGE_SIGNATURE); - boolean showAutocryptHint = intent.getBooleanExtra(EXTRA_SHOW_AUTOCRYPT_HINT, false); + RemoteSelectIdViewModel viewModel = ViewModelProviders.of(this).get(RemoteSelectIdViewModel.class); + presenter.setupFromViewModel(viewModel); + } + + public static class RemoteSelectIdViewModel extends ViewModel { + public String packageName; + public byte[] packageSignature; + public String rawUserId; + public boolean clientHasAutocryptSetupMsg; + + public List filteredKeyInfo; + + private LiveData> keyInfo; + private PgpKeyGenerationLiveData keyGenerationData; + private boolean listAllKeys; + + public LiveData> getSecretUnifiedKeyInfo(Context context) { + if (keyInfo == null) { + KeyRepository keyRepository = KeyRepository.create(context); + keyInfo = new GenericLiveData<>(context, keyRepository::getAllUnifiedKeyInfoWithSecret); + } + return keyInfo; + } + + public PgpKeyGenerationLiveData getKeyGenerationLiveData(Context context) { + if (keyGenerationData == null) { + keyGenerationData = new PgpKeyGenerationLiveData(context); + } + return keyGenerationData; + } + + public boolean isListAllKeys() { + return listAllKeys; + } + + public void setListAllKeys(boolean listAllKeys) { + this.listAllKeys = listAllKeys; + } - presenter.setupFromIntentData(packageName, packageSignature, userId, showAutocryptHint); } public static class RemoteSelectIdentityKeyDialogFragment extends DialogFragment { @@ -135,7 +175,7 @@ public class RemoteSelectIdKeyActivity extends FragmentActivity { @NonNull @Override public Dialog onCreateDialog(Bundle savedInstanceState) { - final Activity activity = getActivity(); + Activity activity = requireActivity(); ContextThemeWrapper theme = ThemeChanger.getDialogThemeWrapper(activity); CustomAlertDialogBuilder alert = new CustomAlertDialogBuilder(theme); @@ -177,7 +217,7 @@ public class RemoteSelectIdKeyActivity extends FragmentActivity { public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - presenter = ((RemoteSelectIdKeyActivity) getActivity()).presenter; + presenter = ((RemoteSelectIdKeyActivity) requireActivity()).presenter; presenter.setView(mvpView); } @@ -203,7 +243,7 @@ public class RemoteSelectIdKeyActivity extends FragmentActivity { @NonNull private RemoteSelectIdentityKeyView createMvpView(final ViewGroup rootView, LayoutInflater layoutInflater) { // final ImageView iconClientApp = rootView.findViewById(R.id.icon_client_app); - final KeyChoiceAdapter keyChoiceAdapter = new KeyChoiceAdapter(layoutInflater, getResources()); + final DialogKeyChoiceAdapter keyChoiceAdapter = new DialogKeyChoiceAdapter(requireContext(), layoutInflater); final TextView titleText = rootView.findViewById(R.id.text_title_select_key); final TextView addressText = rootView.findViewById(R.id.text_user_id); final TextView autocryptHint = rootView.findViewById(R.id.key_import_autocrypt_hint); @@ -309,7 +349,7 @@ public class RemoteSelectIdKeyActivity extends FragmentActivity { } @Override - public void setKeyListData(List data) { + public void setKeyListData(List data) { keyChoiceAdapter.setData(data); } @@ -403,94 +443,6 @@ public class RemoteSelectIdKeyActivity extends FragmentActivity { importOpHelper.cryptoOperation(); } - private static class KeyChoiceAdapter extends Adapter { - private final LayoutInflater layoutInflater; - private final Resources resources; - private List data; - private Drawable iconUnselected; - private Drawable iconSelected; - private Integer activeItem; - - KeyChoiceAdapter(LayoutInflater layoutInflater, Resources resources) { - this.layoutInflater = layoutInflater; - this.resources = resources; - } - - @Override - public KeyChoiceViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - View keyChoiceItemView = layoutInflater.inflate(R.layout.api_select_identity_item, parent, false); - return new KeyChoiceViewHolder(keyChoiceItemView); - } - - void setActiveItem(Integer activeItem) { - this.activeItem = activeItem; - notifyDataSetChanged(); - } - - @Override - public void onBindViewHolder(KeyChoiceViewHolder holder, int position) { - KeyInfo keyInfo = data.get(position); - boolean hasActiveItem = activeItem != null; - boolean isActiveItem = hasActiveItem && position == activeItem; - - Drawable icon = isActiveItem ? iconSelected : iconUnselected; - holder.bind(keyInfo, icon); - - holder.itemView.setVisibility(!hasActiveItem || isActiveItem ? View.VISIBLE : View.INVISIBLE); - } - - @Override - public int getItemCount() { - return data != null ? data.size() : 0; - } - - public void setData(List data) { - this.data = data; - notifyDataSetChanged(); - } - - void setSelectionDrawables(Drawable iconSelected, Drawable iconUnselected) { - this.iconSelected = iconSelected; - this.iconUnselected = iconUnselected; - - notifyDataSetChanged(); - } - } - - private static class KeyChoiceViewHolder extends RecyclerView.ViewHolder { - private final TextView vName; - private final TextView vCreation = (TextView) itemView.findViewById(R.id.key_list_item_creation); - private final ImageView vIcon; - - KeyChoiceViewHolder(View itemView) { - super(itemView); - - vName = itemView.findViewById(R.id.key_list_item_name); - vIcon = itemView.findViewById(R.id.key_list_item_icon); - } - - void bind(KeyInfo keyInfo, Drawable selectionIcon) { - Context context = vCreation.getContext(); - - String email = keyInfo.getEmail(); - String name = keyInfo.getName(); - if (email != null) { - vName.setText(context.getString(R.string.use_key, email)); - } else if (name != null) { - vName.setText(context.getString(R.string.use_key, name)); - } else { - vName.setText(context.getString(R.string.use_key_no_name)); - } - - String dateTime = DateUtils.formatDateTime(context, keyInfo.getCreationDate(), - DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME | - DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_ABBREV_MONTH); - vCreation.setText(context.getString(R.string.label_key_created, dateTime)); - - vIcon.setImageDrawable(selectionIcon); - } - } - @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (importOpHelper.handleActivityResult(requestCode, resultCode, data)) { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteSelectIdViewModel.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteSelectIdViewModel.java deleted file mode 100644 index d287129fb..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteSelectIdViewModel.java +++ /dev/null @@ -1,39 +0,0 @@ -package org.sufficientlysecure.keychain.remote.ui.dialog; - - -import android.arch.lifecycle.ViewModel; -import android.content.Context; - -import org.sufficientlysecure.keychain.livedata.KeyInfoLiveData; -import org.sufficientlysecure.keychain.livedata.PgpKeyGenerationLiveData; - - -public class RemoteSelectIdViewModel extends ViewModel { - - private KeyInfoLiveData keyInfo; - private PgpKeyGenerationLiveData keyGenerationData; - private boolean listAllKeys; - - public KeyInfoLiveData getKeyInfo(Context context) { - if (keyInfo == null) { - keyInfo = new KeyInfoLiveData(context, context.getContentResolver()); - } - return keyInfo; - } - - public PgpKeyGenerationLiveData getKeyGenerationLiveData(Context context) { - if (keyGenerationData == null) { - keyGenerationData = new PgpKeyGenerationLiveData(context); - } - return keyGenerationData; - } - - public boolean isListAllKeys() { - return listAllKeys; - } - - public void setListAllKeys(boolean listAllKeys) { - this.listAllKeys = listAllKeys; - } - -} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteSelectIdentityKeyPresenter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteSelectIdentityKeyPresenter.java index 6cd4bdef6..19ab30368 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteSelectIdentityKeyPresenter.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteSelectIdentityKeyPresenter.java @@ -19,7 +19,9 @@ package org.sufficientlysecure.keychain.remote.ui.dialog; import java.io.IOException; +import java.util.ArrayList; import java.util.List; +import java.util.Objects; import android.arch.lifecycle.LifecycleOwner; import android.content.Context; @@ -27,19 +29,18 @@ import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.graphics.drawable.Drawable; -import android.net.Uri; +import android.text.TextUtils; import org.openintents.openpgp.util.OpenPgpUtils; import org.openintents.openpgp.util.OpenPgpUtils.UserId; import org.sufficientlysecure.keychain.Constants; -import org.sufficientlysecure.keychain.livedata.KeyInfoInteractor.KeyInfo; -import org.sufficientlysecure.keychain.livedata.KeyInfoInteractor.KeySelector; +import org.sufficientlysecure.keychain.model.ApiApp; +import org.sufficientlysecure.keychain.model.SubKey.UnifiedKeyInfo; import org.sufficientlysecure.keychain.operations.results.ImportKeyResult; import org.sufficientlysecure.keychain.operations.results.PgpEditKeyResult; import org.sufficientlysecure.keychain.pgp.UncachedKeyRing; -import org.sufficientlysecure.keychain.provider.ApiDataAccessObject; -import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; -import org.sufficientlysecure.keychain.remote.AppSettings; +import org.sufficientlysecure.keychain.daos.ApiAppDao; +import org.sufficientlysecure.keychain.remote.ui.dialog.RemoteSelectIdKeyActivity.RemoteSelectIdViewModel; import org.sufficientlysecure.keychain.service.ImportKeyringParcel; import org.sufficientlysecure.keychain.service.SaveKeyringParcel; import timber.log.Timber; @@ -47,55 +48,49 @@ import timber.log.Timber; class RemoteSelectIdentityKeyPresenter { private final PackageManager packageManager; + private LifecycleOwner lifecycleOwner; private final Context context; - private final RemoteSelectIdViewModel viewModel; - private RemoteSelectIdentityKeyView view; - private List keyInfoData; + private RemoteSelectIdViewModel viewModel; + private List keyInfoData; private UserId userId; - private long selectedMasterKeyId; + private Long selectedMasterKeyId; private byte[] generatedKeyData; - private ApiDataAccessObject apiDao; - private AppSettings appSettings; + private ApiAppDao apiAppDao; + private ApiApp apiApp; - RemoteSelectIdentityKeyPresenter(Context context, RemoteSelectIdViewModel viewModel, LifecycleOwner lifecycleOwner) { + RemoteSelectIdentityKeyPresenter(Context context, LifecycleOwner lifecycleOwner) { this.context = context; - this.viewModel = viewModel; - this.apiDao = new ApiDataAccessObject(context); + this.lifecycleOwner = lifecycleOwner; + this.apiAppDao = ApiAppDao.getInstance(context); packageManager = context.getPackageManager(); - - viewModel.getKeyGenerationLiveData(context).observe(lifecycleOwner, this::onChangeKeyGeneration); - viewModel.getKeyInfo(context).observe(lifecycleOwner, this::onChangeKeyInfoData); } public void setView(RemoteSelectIdentityKeyView view) { this.view = view; } - void setupFromIntentData(String packageName, byte[] packageSignature, String rawUserId, boolean clientHasAutocryptSetupMsg) { + void setupFromViewModel(RemoteSelectIdViewModel viewModel) { + this.viewModel = viewModel; + try { - setPackageInfo(packageName, packageSignature); + setPackageInfo(viewModel.packageName, viewModel.packageSignature); } catch (NameNotFoundException e) { Timber.e(e, "Unable to find info of calling app!"); view.finishAsCancelled(); return; } - this.userId = OpenPgpUtils.splitUserId(rawUserId); + this.userId = OpenPgpUtils.splitUserId(viewModel.rawUserId); view.setAddressText(userId.email); - view.setShowAutocryptHint(clientHasAutocryptSetupMsg); + view.setShowAutocryptHint(viewModel.clientHasAutocryptSetupMsg); - loadKeyInfo(); - } - - private void loadKeyInfo() { - Uri listedKeyRingUri = viewModel.isListAllKeys() ? - KeyRings.buildUnifiedKeyRingsUri() : KeyRings.buildUnifiedKeyRingsFindByUserIdUri(userId.email); - viewModel.getKeyInfo(context).setKeySelector(KeySelector.createOnlySecret(listedKeyRingUri, null)); + viewModel.getKeyGenerationLiveData(context).observe(lifecycleOwner, this::onChangeKeyGeneration); + viewModel.getSecretUnifiedKeyInfo(context).observe(lifecycleOwner, this::onChangeKeyInfoData); } private void setPackageInfo(String packageName, byte[] packageSignature) throws NameNotFoundException { @@ -103,27 +98,45 @@ class RemoteSelectIdentityKeyPresenter { Drawable appIcon = packageManager.getApplicationIcon(applicationInfo); CharSequence appLabel = packageManager.getApplicationLabel(applicationInfo); - appSettings = new AppSettings(packageName, packageSignature); + apiApp = ApiApp.create(packageName, packageSignature); view.setTitleClientIconAndName(appIcon, appLabel); } - private void onChangeKeyInfoData(List data) { + private void onChangeKeyInfoData(List data) { + if (data == null) { + return; + } keyInfoData = data; goToSelectLayout(); } private void goToSelectLayout() { - if (keyInfoData == null) { + List filteredKeyInfoData = viewModel.isListAllKeys() || TextUtils.isEmpty(userId.email) ? + keyInfoData : getFilteredKeyInfo(userId.email.toLowerCase().trim()); + + if (filteredKeyInfoData == null) { view.showLayoutEmpty(); - } else if (keyInfoData.isEmpty()) { + } else if (filteredKeyInfoData.isEmpty()) { view.showLayoutSelectNoKeys(); } else { - view.setKeyListData(keyInfoData); + view.setKeyListData(filteredKeyInfoData); view.showLayoutSelectKeyList(); } } + private List getFilteredKeyInfo(String filterString) { + if (viewModel.filteredKeyInfo == null) { + viewModel.filteredKeyInfo = new ArrayList<>(); + for (UnifiedKeyInfo unifiedKeyInfo : keyInfoData) { + if (unifiedKeyInfo.uidSearchString().contains(filterString)) { + viewModel.filteredKeyInfo.add(unifiedKeyInfo); + } + } + } + return viewModel.filteredKeyInfo; + } + private void onChangeKeyGeneration(PgpEditKeyResult pgpEditKeyResult) { if (pgpEditKeyResult == null) { return; @@ -171,7 +184,7 @@ class RemoteSelectIdentityKeyPresenter { } void onKeyItemClick(int position) { - selectedMasterKeyId = keyInfoData.get(position).getMasterKeyId(); + selectedMasterKeyId = getFilteredKeyInfo(userId.email.toLowerCase()).get(position).master_key_id(); view.highlightKey(position); } @@ -200,15 +213,15 @@ class RemoteSelectIdentityKeyPresenter { } void onHighlightFinished() { - apiDao.insertApiApp(appSettings); - apiDao.addAllowedKeyIdForApp(appSettings.getPackageName(), selectedMasterKeyId); + apiAppDao.insertApiApp(apiApp); + apiAppDao.addAllowedKeyIdForApp(apiApp.package_name(), Objects.requireNonNull(selectedMasterKeyId)); view.finishAndReturn(selectedMasterKeyId); } void onImportOpSuccess(ImportKeyResult result) { long importedMasterKeyId = result.getImportedMasterKeyIds()[0]; - apiDao.insertApiApp(appSettings); - apiDao.addAllowedKeyIdForApp(appSettings.getPackageName(), selectedMasterKeyId); + apiAppDao.insertApiApp(apiApp); + apiAppDao.addAllowedKeyIdForApp(apiApp.package_name(), importedMasterKeyId); view.finishAndReturn(importedMasterKeyId); } @@ -222,8 +235,7 @@ class RemoteSelectIdentityKeyPresenter { public void onClickMenuListAllKeys() { viewModel.setListAllKeys(!viewModel.isListAllKeys()); - loadKeyInfo(); - view.showLayoutSelectKeyList(); + goToSelectLayout(); } public void onClickGoToOpenKeychain() { @@ -246,7 +258,7 @@ class RemoteSelectIdentityKeyPresenter { void showLayoutGenerateOk(); void showLayoutGenerateSave(); - void setKeyListData(List data); + void setKeyListData(List data); void highlightKey(int position); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainService.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainService.java index 5bd3aeebe..c3e9d9384 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainService.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainService.java @@ -18,8 +18,6 @@ package org.sufficientlysecure.keychain.service; -import java.util.concurrent.atomic.AtomicBoolean; - import android.app.Service; import android.content.Intent; import android.os.Bundle; @@ -28,7 +26,10 @@ import android.os.Message; import android.os.Messenger; import android.os.Parcelable; import android.os.RemoteException; +import android.support.v4.os.CancellationSignal; +import org.sufficientlysecure.keychain.operations.KeySyncOperation; +import org.sufficientlysecure.keychain.operations.KeySyncParcel; import org.sufficientlysecure.keychain.operations.BackupOperation; import org.sufficientlysecure.keychain.operations.BaseOperation; import org.sufficientlysecure.keychain.operations.BenchmarkOperation; @@ -48,7 +49,7 @@ import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyInputParcel; import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyOperation; import org.sufficientlysecure.keychain.pgp.Progressable; import org.sufficientlysecure.keychain.pgp.SignEncryptParcel; -import org.sufficientlysecure.keychain.provider.KeyWritableRepository; +import org.sufficientlysecure.keychain.daos.KeyWritableRepository; import org.sufficientlysecure.keychain.service.ServiceProgressHandler.MessageStatus; import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import timber.log.Timber; @@ -70,7 +71,7 @@ public class KeychainService extends Service implements Progressable { public static final String ACTION_CANCEL = "action_cancel"; // this attribute can possibly merged with the one above? not sure... - private AtomicBoolean mActionCanceled = new AtomicBoolean(false); + private CancellationSignal mActionCanceled; ThreadLocal mMessenger = new ThreadLocal<>(); @@ -86,16 +87,15 @@ public class KeychainService extends Service implements Progressable { public int onStartCommand(final Intent intent, int flags, int startId) { if (intent.getAction() != null && intent.getAction().equals(ACTION_CANCEL)) { - mActionCanceled.set(true); + mActionCanceled.cancel(); return START_NOT_STICKY; } + mActionCanceled = new CancellationSignal(); + Runnable actionRunnable = new Runnable() { @Override public void run() { - // We have not been cancelled! (yet) - mActionCanceled.set(false); - Bundle extras = intent.getExtras(); // Set messenger for communication (for this particular thread) @@ -140,6 +140,8 @@ public class KeychainService extends Service implements Progressable { op = new InputDataOperation(outerThis, databaseInteractor, outerThis); } else if (inputParcel instanceof BenchmarkInputParcel) { op = new BenchmarkOperation(outerThis, databaseInteractor, outerThis); + } else if (inputParcel instanceof KeySyncParcel) { + op = new KeySyncOperation(outerThis, databaseInteractor, outerThis, mActionCanceled); } else { throw new AssertionError("Unrecognized input parcel in KeychainService!"); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeyserverSyncAdapterService.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeyserverSyncAdapterService.java deleted file mode 100644 index 2a763c105..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeyserverSyncAdapterService.java +++ /dev/null @@ -1,632 +0,0 @@ -/* - * Copyright (C) 2017 Schürmann & Breitmoser GbR - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sufficientlysecure.keychain.service; - - -import java.util.ArrayList; -import java.util.GregorianCalendar; -import java.util.Random; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; - -import android.accounts.Account; -import android.app.AlarmManager; -import android.app.Notification; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.app.Service; -import android.content.AbstractThreadedSyncAdapter; -import android.content.ContentProviderClient; -import android.content.ContentResolver; -import android.content.Context; -import android.content.Intent; -import android.content.SyncResult; -import android.database.Cursor; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.drawable.Drawable; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; -import android.os.Build; -import android.os.Bundle; -import android.os.Handler; -import android.os.IBinder; -import android.os.Message; -import android.os.Messenger; -import android.os.PowerManager; -import android.os.SystemClock; -import android.support.v4.app.NotificationCompat; - -import org.sufficientlysecure.keychain.Constants; -import org.sufficientlysecure.keychain.KeychainApplication; -import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing; -import org.sufficientlysecure.keychain.network.NetworkReceiver; -import org.sufficientlysecure.keychain.network.orbot.OrbotHelper; -import org.sufficientlysecure.keychain.operations.ImportOperation; -import org.sufficientlysecure.keychain.operations.results.ImportKeyResult; -import org.sufficientlysecure.keychain.operations.results.OperationResult; -import org.sufficientlysecure.keychain.provider.KeyWritableRepository; -import org.sufficientlysecure.keychain.provider.KeychainContract; -import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; -import org.sufficientlysecure.keychain.ui.OrbotRequiredDialogActivity; -import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; -import org.sufficientlysecure.keychain.util.ParcelableProxy; -import org.sufficientlysecure.keychain.util.Preferences; -import timber.log.Timber; - - -public class KeyserverSyncAdapterService extends Service { - - // how often a sync should be initiated, in s - public static final long SYNC_INTERVAL = - Constants.DEBUG_KEYSERVER_SYNC - ? TimeUnit.MINUTES.toSeconds(1) : TimeUnit.DAYS.toSeconds(3); - // time since last update after which a key should be updated again, in s - public static final long KEY_UPDATE_LIMIT = - Constants.DEBUG_KEYSERVER_SYNC ? 1 : TimeUnit.DAYS.toSeconds(7); - // time by which a sync is postponed in case screen is on - public static final long SYNC_POSTPONE_TIME = - Constants.DEBUG_KEYSERVER_SYNC ? 30 * 1000 : TimeUnit.MINUTES.toMillis(5); - // Time taken by Orbot before a new circuit is created - public static final int ORBOT_CIRCUIT_TIMEOUT_SECONDS = - Constants.DEBUG_KEYSERVER_SYNC ? 2 : (int) TimeUnit.MINUTES.toSeconds(10); - - - private static final String ACTION_IGNORE_TOR = "ignore_tor"; - private static final String ACTION_UPDATE_ALL = "update_all"; - public static final String ACTION_SYNC_NOW = "sync_now"; - private static final String ACTION_DISMISS_NOTIFICATION = "cancel_sync"; - private static final String ACTION_START_ORBOT = "start_orbot"; - private static final String ACTION_CANCEL = "cancel"; - - private AtomicBoolean mCancelled = new AtomicBoolean(false); - - @Override - public int onStartCommand(final Intent intent, int flags, final int startId) { - if (intent == null || intent.getAction() == null) { - // introduced due to https://github.com/open-keychain/open-keychain/issues/1573 - return START_NOT_STICKY; // we can't act on this Intent and don't want it redelivered - } - - if (!isSyncEnabled()) { - // if we have initiated a sync, but the user disabled it in preferences since - return START_NOT_STICKY; - } - - switch (intent.getAction()) { - case ACTION_CANCEL: { - mCancelled.set(true); - return START_NOT_STICKY; - } - // the reason for the separation betweyeen SYNC_NOW and UPDATE_ALL is so that starting - // the sync directly from the notification is possible while the screen is on with - // UPDATE_ALL, but a postponed sync is only started if screen is off - case ACTION_SYNC_NOW: { - // this checks for screen on/off before sync, and postpones the sync if on - ContentResolver.requestSync( - new Account(Constants.ACCOUNT_NAME, Constants.ACCOUNT_TYPE), - Constants.PROVIDER_AUTHORITY, - new Bundle() - ); - return START_NOT_STICKY; - } - case ACTION_UPDATE_ALL: { - // does not check for screen on/off - asyncKeyUpdate(this, CryptoInputParcel.createCryptoInputParcel(), startId); - // we depend on handleUpdateResult to call stopSelf when it is no longer necessary - // for the intent to be redelivered - return START_REDELIVER_INTENT; - } - case ACTION_IGNORE_TOR: { - NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); - manager.cancel(Constants.Notification.KEYSERVER_SYNC_FAIL_ORBOT); - asyncKeyUpdate(this, CryptoInputParcel.createCryptoInputParcel(ParcelableProxy.getForNoProxy()), - startId); - // we depend on handleUpdateResult to call stopSelf when it is no longer necessary - // for the intent to be redelivered - return START_REDELIVER_INTENT; - } - case ACTION_START_ORBOT: { - NotificationManager manager = (NotificationManager) - getSystemService(NOTIFICATION_SERVICE); - manager.cancel(Constants.Notification.KEYSERVER_SYNC_FAIL_ORBOT); - - Intent startOrbot = new Intent(this, OrbotRequiredDialogActivity.class); - startOrbot.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - startOrbot.putExtra(OrbotRequiredDialogActivity.EXTRA_START_ORBOT, true); - - Messenger messenger = new Messenger( - new Handler() { - @Override - public void handleMessage(Message msg) { - switch (msg.what) { - case OrbotRequiredDialogActivity.MESSAGE_ORBOT_STARTED: { - startServiceWithUpdateAll(); - break; - } - case OrbotRequiredDialogActivity.MESSAGE_ORBOT_IGNORE: - case OrbotRequiredDialogActivity.MESSAGE_DIALOG_CANCEL: { - // not possible since we proceed to Orbot's Activity - // directly, by starting OrbotRequiredDialogActivity with - // EXTRA_START_ORBOT set to true - break; - } - } - } - } - ); - startOrbot.putExtra(OrbotRequiredDialogActivity.EXTRA_MESSENGER, messenger); - startActivity(startOrbot); - // since we return START_NOT_STICKY, we also postpone the sync as a backup in case - // the service is killed before OrbotRequiredDialogActivity can get back to us - postponeSync(); - // if use START_REDELIVER_INTENT, we might annoy the user by repeatedly starting the - // Orbot Activity when our service is killed and restarted - return START_NOT_STICKY; - } - case ACTION_DISMISS_NOTIFICATION: { - NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); - manager.cancel(Constants.Notification.KEYSERVER_SYNC_FAIL_ORBOT); - return START_NOT_STICKY; - } - } - return START_NOT_STICKY; - } - - private class KeyserverSyncAdapter extends AbstractThreadedSyncAdapter { - - public KeyserverSyncAdapter() { - super(KeyserverSyncAdapterService.this, true); - } - - @Override - public void onPerformSync(Account account, Bundle extras, String authority, - ContentProviderClient provider, SyncResult syncResult) { - - Preferences prefs = Preferences.getPreferences(getContext()); - - // for a wifi-ONLY sync - if (prefs.getWifiOnlySync()) { - - ConnectivityManager connMgr = (ConnectivityManager) - getSystemService(Context.CONNECTIVITY_SERVICE); - NetworkInfo networkInfo = connMgr.getNetworkInfo(ConnectivityManager.TYPE_WIFI); - boolean isNotOnWifi = !(networkInfo.getType() == ConnectivityManager.TYPE_WIFI); - boolean isNotConnected = !(networkInfo.isConnected()); - - // if Wi-Fi connection doesn't exist then receiver is enabled - if (isNotOnWifi && isNotConnected) { - new NetworkReceiver().setWifiReceiverComponent(true, getContext()); - return; - } - } - Timber.d("Performing a keyserver sync!"); - PowerManager pm = (PowerManager) KeyserverSyncAdapterService.this - .getSystemService(Context.POWER_SERVICE); - @SuppressWarnings("deprecation") // our min is API 15, deprecated only in 20 - boolean isScreenOn = pm.isScreenOn(); - - if (!isScreenOn) { - startServiceWithUpdateAll(); - } else { - postponeSync(); - } - } - - @Override - public void onSyncCanceled() { - super.onSyncCanceled(); - cancelUpdates(KeyserverSyncAdapterService.this); - } - } - - @Override - public IBinder onBind(Intent intent) { - return new KeyserverSyncAdapter().getSyncAdapterBinder(); - } - - /** - * Since we're returning START_REDELIVER_INTENT in onStartCommand, we need to remember to call - * stopSelf(int) to prevent the Intent from being redelivered if our work is already done - * - * @param result result of keyserver sync - * @param startId startId provided to the onStartCommand call which resulted in this sync - */ - private void handleUpdateResult(ImportKeyResult result, final int startId) { - if (result.isPending()) { - Timber.d("Orbot required for sync but not running, attempting to start"); - // result is pending due to Orbot not being started - // try to start it silently, if disabled show notifications - new OrbotHelper.SilentStartManager() { - @Override - protected void onOrbotStarted() { - // retry the update - startServiceWithUpdateAll(); - stopSelf(startId); // startServiceWithUpdateAll will deliver a new Intent - } - - @Override - protected void onSilentStartDisabled() { - // show notification - NotificationManager manager = - (NotificationManager) getSystemService(NOTIFICATION_SERVICE); - manager.notify(Constants.Notification.KEYSERVER_SYNC_FAIL_ORBOT, - getOrbotNoification(KeyserverSyncAdapterService.this)); - // further action on user interaction with notification, intent should not be - // redelivered, therefore: - stopSelf(startId); - } - }.startOrbotAndListen(this, false); - // if we're killed before we get a response from Orbot, we need the intent to be - // redelivered, so no stopSelf(int) here - } else if (isUpdateCancelled()) { - Timber.d("Keyserver sync cancelled, postponing by" + SYNC_POSTPONE_TIME - + "ms"); - postponeSync(); - // postponeSync creates a new intent, so we don't need this to be redelivered - stopSelf(startId); - } else { - Timber.d("Keyserver sync completed: Updated: " + result.mUpdatedKeys - + " Failed: " + result.mBadKeys); - // key sync completed successfully, we can stop - stopSelf(startId); - } - } - - private void postponeSync() { - AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); - Intent serviceIntent = new Intent(this, KeyserverSyncAdapterService.class); - serviceIntent.setAction(ACTION_SYNC_NOW); - PendingIntent pi = PendingIntent.getService(this, 0, serviceIntent, - PendingIntent.FLAG_UPDATE_CURRENT); - alarmManager.set( - AlarmManager.ELAPSED_REALTIME_WAKEUP, - SystemClock.elapsedRealtime() + SYNC_POSTPONE_TIME, - pi - ); - } - - private void asyncKeyUpdate(final Context context, - final CryptoInputParcel cryptoInputParcel, final int startId) { - new Thread(new Runnable() { - @Override - public void run() { - ImportKeyResult result = updateKeysFromKeyserver(context, cryptoInputParcel); - handleUpdateResult(result, startId); - } - }).start(); - } - - private synchronized ImportKeyResult updateKeysFromKeyserver(final Context context, - final CryptoInputParcel cryptoInputParcel) { - mCancelled.set(false); - - ArrayList keyList = getKeysToUpdate(context); - - if (isUpdateCancelled()) { // if we've already been cancelled - return new ImportKeyResult(OperationResult.RESULT_CANCELLED, - new OperationResult.OperationLog()); - } - - if (cryptoInputParcel.getParcelableProxy() == null) { - // no explicit proxy, retrieve from preferences. Check if we should do a staggered sync - if (Preferences.getPreferences(context).getParcelableProxy().isTorEnabled()) { - return staggeredUpdate(context, keyList, cryptoInputParcel); - } else { - return directUpdate(context, keyList, cryptoInputParcel); - } - } else { - return directUpdate(context, keyList, cryptoInputParcel); - } - } - - private ImportKeyResult directUpdate(Context context, ArrayList keyList, - CryptoInputParcel cryptoInputParcel) { - Timber.d("Starting normal update"); - ImportOperation importOp = new ImportOperation(context, - KeyWritableRepository.create(context), null); - return importOp.execute( - ImportKeyringParcel.createImportKeyringParcel(keyList, - Preferences.getPreferences(context).getPreferredKeyserver()), - cryptoInputParcel - ); - } - - /** - * will perform a staggered update of user's keys using delays to ensure new Tor circuits, as - * performed by parcimonie. Relevant issue and method at: - * https://github.com/open-keychain/open-keychain/issues/1337 - * - * @return result of the sync - */ - private ImportKeyResult staggeredUpdate(Context context, ArrayList keyList, - CryptoInputParcel cryptoInputParcel) { - Timber.d("Starting staggered update"); - // final int WEEK_IN_SECONDS = (int) TimeUnit.DAYS.toSeconds(7); - // we are limiting our randomness to ORBOT_CIRCUIT_TIMEOUT_SECONDS for now - final int WEEK_IN_SECONDS = 0; - - ImportOperation.KeyImportAccumulator accumulator - = new ImportOperation.KeyImportAccumulator(keyList.size(), null); - - // so that the first key can be updated without waiting. This is so that there isn't a - // large gap between a "Start Orbot" notification and the next key update - boolean first = true; - - for (ParcelableKeyRing keyRing : keyList) { - int waitTime; - int staggeredTime = new Random().nextInt(1 + 2 * (WEEK_IN_SECONDS / keyList.size())); - if (staggeredTime >= ORBOT_CIRCUIT_TIMEOUT_SECONDS) { - waitTime = staggeredTime; - } else { - waitTime = ORBOT_CIRCUIT_TIMEOUT_SECONDS - + new Random().nextInt(1 + ORBOT_CIRCUIT_TIMEOUT_SECONDS); - } - - if (first) { - waitTime = 0; - first = false; - } - - Timber.d("Updating key with a wait time of " + waitTime + "s"); - try { - Thread.sleep(waitTime * 1000); - } catch (InterruptedException e) { - Timber.e(e, "Exception during sleep between key updates"); - // skip this one - continue; - } - ArrayList keyWrapper = new ArrayList<>(); - keyWrapper.add(keyRing); - if (isUpdateCancelled()) { - return new ImportKeyResult(ImportKeyResult.RESULT_CANCELLED, - new OperationResult.OperationLog()); - } - ImportKeyResult result = - new ImportOperation(context, KeyWritableRepository.create(context), null, mCancelled) - .execute( - ImportKeyringParcel.createImportKeyringParcel( - keyWrapper, - Preferences.getPreferences(context) - .getPreferredKeyserver() - ), - cryptoInputParcel - ); - if (result.isPending()) { - return result; - } - accumulator.accumulateKeyImport(result); - } - return accumulator.getConsolidatedResult(); - } - - /** - * 1. Get keys which have been updated recently and therefore do not need to - * be updated now - * 2. Get list of all keys and filter out ones that don't need to be updated - * 3. Return keys to be updated - * - * @return list of keys that require update - */ - private ArrayList getKeysToUpdate(Context context) { - - // 1. Get keys which have been updated recently and don't need to updated now - final int INDEX_UPDATED_KEYS_MASTER_KEY_ID = 0; - final int INDEX_LAST_UPDATED = 1; - - // all time in seconds not milliseconds - final long CURRENT_TIME = GregorianCalendar.getInstance().getTimeInMillis() / 1000; - Cursor updatedKeysCursor = context.getContentResolver().query( - KeychainContract.UpdatedKeys.CONTENT_URI, - new String[]{ - KeychainContract.UpdatedKeys.MASTER_KEY_ID, - KeychainContract.UpdatedKeys.LAST_UPDATED - }, - "? - " + KeychainContract.UpdatedKeys.LAST_UPDATED + " < " + KEY_UPDATE_LIMIT, - new String[]{"" + CURRENT_TIME}, - null - ); - - ArrayList ignoreMasterKeyIds = new ArrayList<>(); - while (updatedKeysCursor != null && updatedKeysCursor.moveToNext()) { - long masterKeyId = updatedKeysCursor.getLong(INDEX_UPDATED_KEYS_MASTER_KEY_ID); - Timber.d("Keyserver sync: Ignoring {" + masterKeyId + "} last updated at {" - + updatedKeysCursor.getLong(INDEX_LAST_UPDATED) + "}s"); - ignoreMasterKeyIds.add(masterKeyId); - } - if (updatedKeysCursor != null) { - updatedKeysCursor.close(); - } - - // 2. Make a list of public keys which should be updated - final int INDEX_MASTER_KEY_ID = 0; - final int INDEX_FINGERPRINT = 1; - Cursor keyCursor = context.getContentResolver().query( - KeychainContract.KeyRings.buildUnifiedKeyRingsUri(), - new String[]{ - KeychainContract.KeyRings.MASTER_KEY_ID, - KeychainContract.KeyRings.FINGERPRINT - }, - null, - null, - null - ); - - if (keyCursor == null) { - return new ArrayList<>(); - } - - ArrayList keyList = new ArrayList<>(); - while (keyCursor.moveToNext()) { - long keyId = keyCursor.getLong(INDEX_MASTER_KEY_ID); - if (ignoreMasterKeyIds.contains(keyId)) { - continue; - } - Timber.d("Keyserver sync: Updating {" + keyId + "}"); - byte[] fingerprint = keyCursor.getBlob(INDEX_FINGERPRINT); - String hexKeyId = KeyFormattingUtils.convertKeyIdToHex(keyId); - // we aren't updating from keybase as of now - keyList.add(ParcelableKeyRing.createFromReference(fingerprint, hexKeyId, null, null)); - } - keyCursor.close(); - - return keyList; - } - - private boolean isUpdateCancelled() { - return mCancelled.get(); - } - - /** - * will cancel an update already in progress. We send an Intent to cancel it instead of simply - * modifying a static variable since the service is running in a process that is different from - * the default application process where the UI code runs. - * - * @param context used to send an Intent to the service requesting cancellation. - */ - public static void cancelUpdates(Context context) { - Intent intent = new Intent(context, KeyserverSyncAdapterService.class); - intent.setAction(ACTION_CANCEL); - context.startService(intent); - } - - private Notification getOrbotNoification(Context context) { - NotificationCompat.Builder builder = new NotificationCompat.Builder(context); - builder.setSmallIcon(R.drawable.ic_stat_notify_24dp) - .setLargeIcon(getBitmap(R.mipmap.ic_launcher, context)) - .setContentTitle(context.getString(R.string.keyserver_sync_orbot_notif_title)) - .setContentText(context.getString(R.string.keyserver_sync_orbot_notif_msg)) - .setAutoCancel(true); - - // In case the user decides to not use tor - Intent ignoreTorIntent = new Intent(context, KeyserverSyncAdapterService.class); - ignoreTorIntent.setAction(ACTION_IGNORE_TOR); - PendingIntent ignoreTorPi = PendingIntent.getService( - context, - 0, // security not issue since we're giving this pending intent to Notification Manager - ignoreTorIntent, - PendingIntent.FLAG_CANCEL_CURRENT - ); - - builder.addAction(R.drawable.ic_stat_tor_off, - context.getString(R.string.keyserver_sync_orbot_notif_ignore), - ignoreTorPi); - - Intent startOrbotIntent = new Intent(context, KeyserverSyncAdapterService.class); - startOrbotIntent.setAction(ACTION_START_ORBOT); - PendingIntent startOrbotPi = PendingIntent.getService( - context, - 0, // security not issue since we're giving this pending intent to Notification Manager - startOrbotIntent, - PendingIntent.FLAG_CANCEL_CURRENT - ); - - builder.addAction(R.drawable.ic_stat_tor, - context.getString(R.string.keyserver_sync_orbot_notif_start), - startOrbotPi - ); - builder.setContentIntent(startOrbotPi); - - return builder.build(); - } - - public static void enableKeyserverSync(Context context) { - Account account = KeychainApplication.createAccountIfNecessary(context); - - if (account == null) { - // account failed to be created for some reason, nothing we can do here - return; - } - - ContentResolver.setIsSyncable(account, Constants.PROVIDER_AUTHORITY, 1); - ContentResolver.setSyncAutomatically(account, Constants.PROVIDER_AUTHORITY, true); - - updateInterval(context); - } - - /** - * creates a new sync if one does not exist, or updates an existing sync if the sync interval - * has changed. - */ - public static void updateInterval(Context context) { - Account account = KeychainApplication.createAccountIfNecessary(context); - - if (account == null) { - // account failed to be created for some reason, nothing we can do here - return; - } - - boolean intervalChanged = false; - boolean syncExists = Preferences.getKeyserverSyncEnabled(context); - - if (syncExists) { - long oldInterval = ContentResolver.getPeriodicSyncs( - account, Constants.PROVIDER_AUTHORITY).get(0).period; - if (oldInterval != SYNC_INTERVAL) { - intervalChanged = true; - } - } - - if (!syncExists || intervalChanged) { - ContentResolver.addPeriodicSync( - account, - Constants.PROVIDER_AUTHORITY, - new Bundle(), - SYNC_INTERVAL - ); - } - } - - private boolean isSyncEnabled() { - Account account = KeychainApplication.createAccountIfNecessary(this); - - // if account is null, it could not be created for some reason, so sync cannot exist - return account != null - && ContentResolver.getSyncAutomatically(account, Constants.PROVIDER_AUTHORITY); - } - - private void startServiceWithUpdateAll() { - Intent serviceIntent = new Intent(this, KeyserverSyncAdapterService.class); - serviceIntent.setAction(ACTION_UPDATE_ALL); - this.startService(serviceIntent); - } - - // from de.azapps.mirakel.helper.Helpers from https://github.com/MirakelX/mirakel-android - private Bitmap getBitmap(int resId, Context context) { - int mLargeIconWidth = (int) context.getResources().getDimension( - android.R.dimen.notification_large_icon_width); - int mLargeIconHeight = (int) context.getResources().getDimension( - android.R.dimen.notification_large_icon_height); - Drawable d; - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { - // noinspection deprecation (can't help it at this api level) - d = context.getResources().getDrawable(resId); - } else { - d = context.getDrawable(resId); - } - if (d == null) { - return null; - } - Bitmap b = Bitmap.createBitmap(mLargeIconWidth, mLargeIconHeight, Bitmap.Config.ARGB_8888); - Canvas c = new Canvas(b); - d.setBounds(0, 0, mLargeIconWidth, mLargeIconHeight); - d.draw(c); - return b; - } -} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/PassphraseCacheService.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/PassphraseCacheService.java index 8945a3652..8451b942f 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/PassphraseCacheService.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/PassphraseCacheService.java @@ -18,6 +18,8 @@ package org.sufficientlysecure.keychain.service; +import java.util.Date; + import android.app.AlarmManager; import android.app.Notification; import android.app.PendingIntent; @@ -38,16 +40,14 @@ import android.support.v4.app.NotificationCompat; import android.support.v4.util.LongSparseArray; import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.Constants.NotificationIds; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey.SecretKeyType; -import org.sufficientlysecure.keychain.provider.CachedPublicKeyRing; -import org.sufficientlysecure.keychain.provider.KeyRepository; +import org.sufficientlysecure.keychain.daos.KeyRepository; import org.sufficientlysecure.keychain.util.Passphrase; import org.sufficientlysecure.keychain.util.Preferences; import timber.log.Timber; -import java.util.Date; - /** * This service runs in its own process, but is available to all other processes as the main * passphrase cache. Use the static methods addCachedPassphrase and getCachedPassphrase for @@ -245,8 +245,8 @@ public class PassphraseCacheService extends Service { + masterKeyId + ", subKeyId " + subKeyId); // get the type of key (from the database) - CachedPublicKeyRing keyRing = KeyRepository.create(this).getCachedPublicKeyRing(masterKeyId); - SecretKeyType keyType = keyRing.getSecretKeyType(subKeyId); + KeyRepository keyRepository = KeyRepository.create(this); + SecretKeyType keyType = keyRepository.getSecretKeyType(subKeyId); switch (keyType) { case PASSPHRASE_EMPTY: @@ -488,7 +488,7 @@ public class PassphraseCacheService extends Service { private void updateService() { if (mPassphraseCache.size() > 0) { - startForeground(Constants.Notification.PASSPHRASE_CACHE, getNotification()); + startForeground(NotificationIds.PASSPHRASE_CACHE, getNotification()); } else { // stop whole service if no cached passphrases remaining Timber.d("PassphraseCacheService: No passphrases remaining in memory, stopping service!"); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ssh/AuthenticationOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ssh/AuthenticationOperation.java index 3df7f8724..9b377809f 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ssh/AuthenticationOperation.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ssh/AuthenticationOperation.java @@ -17,8 +17,12 @@ package org.sufficientlysecure.keychain.ssh; + +import java.util.Collection; + import android.content.Context; import android.support.annotation.NonNull; + import org.bouncycastle.openpgp.AuthenticationSignatureGenerator; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.operator.jcajce.NfcSyncPGPContentSignerBuilder; @@ -29,15 +33,13 @@ import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey; import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKeyRing; import org.sufficientlysecure.keychain.pgp.PassphraseCacheInterface; import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; -import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; -import org.sufficientlysecure.keychain.provider.KeyRepository; +import org.sufficientlysecure.keychain.daos.KeyRepository; +import org.sufficientlysecure.keychain.daos.KeyRepository.NotFoundException; import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; import org.sufficientlysecure.keychain.util.Passphrase; import timber.log.Timber; -import java.util.Collection; - import static java.lang.String.format; @@ -101,8 +103,8 @@ public class AuthenticationOperation extends BaseOperation Long authSubKeyId = data.getAuthenticationSubKeyId(); if (authSubKeyId == null) { try { // Get the key id of the authentication key belonging to the master key id - authSubKeyId = mKeyRepository.getCachedPublicKeyRing(authMasterKeyId).getSecretAuthenticationId(); - } catch (PgpKeyNotFoundException e) { + authSubKeyId = mKeyRepository.getSecretAuthenticationId(authMasterKeyId); + } catch (NotFoundException e) { log.add(LogType.MSG_AUTH_ERROR_KEY_AUTH, indent); return new AuthenticationResult(AuthenticationResult.RESULT_ERROR, log); } @@ -142,9 +144,7 @@ public class AuthenticationOperation extends BaseOperation CanonicalizedSecretKey.SecretKeyType secretKeyType; try { - secretKeyType = mKeyRepository - .getCachedPublicKeyRing(authMasterKeyId) - .getSecretKeyType(authSubKeyId); + secretKeyType = mKeyRepository.getSecretKeyType(authSubKeyId); } catch (KeyRepository.NotFoundException e) { log.add(LogType.MSG_AUTH_ERROR_KEY_AUTH, indent); return new AuthenticationResult(AuthenticationResult.RESULT_ERROR, log); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/BackupRestoreFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/BackupRestoreFragment.java index 682b0715a..e4b6271d1 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/BackupRestoreFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/BackupRestoreFragment.java @@ -17,16 +17,16 @@ package org.sufficientlysecure.keychain.ui; + import java.util.ArrayList; import java.util.Iterator; import android.app.Activity; -import android.content.ContentResolver; import android.content.Intent; -import android.database.Cursor; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentActivity; @@ -36,10 +36,11 @@ import android.view.View; import android.view.ViewGroup; import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.model.SubKey; +import org.sufficientlysecure.keychain.model.SubKey.UnifiedKeyInfo; import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey.SecretKeyType; -import org.sufficientlysecure.keychain.provider.KeychainContract; -import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; -import org.sufficientlysecure.keychain.provider.KeychainContract.Keys; +import org.sufficientlysecure.keychain.daos.KeyRepository; +import org.sufficientlysecure.keychain.daos.KeyRepository.NotFoundException; import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; import org.sufficientlysecure.keychain.ui.util.Notify; import org.sufficientlysecure.keychain.util.FileHelper; @@ -53,33 +54,16 @@ public class BackupRestoreFragment extends Fragment { private static final int REQUEST_CODE_INPUT = 0x00007003; @Override - public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View view = inflater.inflate(R.layout.backup_restore_fragment, container, false); View backupAll = view.findViewById(R.id.backup_all); View backupPublicKeys = view.findViewById(R.id.backup_public_keys); final View restore = view.findViewById(R.id.restore); - backupAll.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - exportToFile(true); - } - }); - - backupPublicKeys.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - exportToFile(false); - } - }); - - restore.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - restore(); - } - }); + backupAll.setOnClickListener(v -> exportToFile(true)); + backupPublicKeys.setOnClickListener(v -> exportToFile(false)); + restore.setOnClickListener(v -> restore()); return view; } @@ -95,75 +79,58 @@ public class BackupRestoreFragment extends Fragment { return; } - new AsyncTask>>() { + KeyRepository keyRepository = KeyRepository.create(requireContext()); + + // This can probably be optimized quite a bit. + // Typically there are only few secret keys though, so it doesn't really matter. + + new AsyncTask>>() { @Override - protected ArrayList> doInBackground(ContentResolver... resolver) { + protected ArrayList> doInBackground(Void... ignored) { + KeyRepository keyRepository = KeyRepository.create(requireContext()); ArrayList> askPassphraseIds = new ArrayList<>(); - Cursor cursor = resolver[0].query( - KeyRings.buildUnifiedKeyRingsUri(), new String[]{ - KeyRings.MASTER_KEY_ID, - KeyRings.HAS_SECRET, - }, KeyRings.HAS_SECRET + " != 0", null, null); - try { - if (cursor != null) { - while (cursor.moveToNext()) { - SecretKeyType secretKeyType = SecretKeyType.fromNum(cursor.getInt(1)); - switch (secretKeyType) { - // all of these make no sense to ask - case PASSPHRASE_EMPTY: - case DIVERT_TO_CARD: - case UNAVAILABLE: - continue; - case GNU_DUMMY: { - Long masterKeyId = cursor.getLong(0); - Long subKeyId = getFirstSubKeyWithPassphrase(masterKeyId, resolver[0]); - if(subKeyId != null) { - askPassphraseIds.add(new Pair<>(masterKeyId, subKeyId)); - } - continue; - } - default: { - long masterKeyId = cursor.getLong(0); - askPassphraseIds.add(new Pair<>(masterKeyId, masterKeyId)); - } - } - } + for (UnifiedKeyInfo keyInfo : keyRepository.getAllUnifiedKeyInfoWithSecret()) { + long masterKeyId = keyInfo.master_key_id(); + SecretKeyType secretKeyType; + try { + secretKeyType = keyRepository.getSecretKeyType(keyInfo.master_key_id()); + } catch (NotFoundException e) { + throw new IllegalStateException("Error: no secret key type for secret key!"); } - } finally { - if (cursor != null) { - cursor.close(); + switch (secretKeyType) { + // all of these make no sense to ask + case PASSPHRASE_EMPTY: + case DIVERT_TO_CARD: + case UNAVAILABLE: + continue; + case GNU_DUMMY: { + Long subKeyId = getFirstSubKeyWithPassphrase(masterKeyId); + if(subKeyId != null) { + askPassphraseIds.add(new Pair<>(masterKeyId, subKeyId)); + } + continue; + } + default: { + askPassphraseIds.add(new Pair<>(masterKeyId, masterKeyId)); + } } } return askPassphraseIds; } - private Long getFirstSubKeyWithPassphrase(long masterKeyId, ContentResolver resolver) { - Cursor cursor = resolver.query( - KeychainContract.Keys.buildKeysUri(masterKeyId), new String[]{ - Keys.KEY_ID, - Keys.HAS_SECRET, - }, Keys.HAS_SECRET + " != 0", null, null); - try { - if (cursor != null) { - while(cursor.moveToNext()) { - SecretKeyType secretKeyType = SecretKeyType.fromNum(cursor.getInt(1)); - switch (secretKeyType) { - case PASSPHRASE_EMPTY: - case DIVERT_TO_CARD: - case UNAVAILABLE: - return null; - case GNU_DUMMY: - continue; - default: { - return cursor.getLong(0); - } - } + private Long getFirstSubKeyWithPassphrase(long masterKeyId) { + for (SubKey subKey : keyRepository.getSubKeysByMasterKeyId(masterKeyId)) { + switch (subKey.has_secret()) { + case PASSPHRASE_EMPTY: + case DIVERT_TO_CARD: + case UNAVAILABLE: + return null; + case GNU_DUMMY: + continue; + default: { + return subKey.key_id(); } } - } finally { - if (cursor != null) { - cursor.close(); - } } return null; } @@ -186,7 +153,7 @@ public class BackupRestoreFragment extends Fragment { startBackup(true); } - }.execute(activity.getContentResolver()); + }.execute(); } private void startPassphraseActivity() { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyFingerprintActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyFingerprintActivity.java index 58a4c3793..6c958bf5b 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyFingerprintActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyFingerprintActivity.java @@ -17,40 +17,41 @@ package org.sufficientlysecure.keychain.ui; +import android.arch.lifecycle.ViewModelProviders; +import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.view.View; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.ui.base.BaseActivity; +import org.sufficientlysecure.keychain.ui.keyview.UnifiedKeyInfoViewModel; import timber.log.Timber; public class CertifyFingerprintActivity extends BaseActivity { - - protected Uri mDataUri; + public static final String EXTRA_MASTER_KEY_ID = "master_key_id"; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - mDataUri = getIntent().getData(); - if (mDataUri == null) { - Timber.e("Data missing. Should be uri of key!"); + Bundle extras = getIntent().getExtras(); + if (extras == null || !extras.containsKey(EXTRA_MASTER_KEY_ID)) { + Timber.e("Missing required extra master_key_id!"); finish(); return; } - setFullScreenDialogClose(new View.OnClickListener() { - @Override - public void onClick(View v) { - finish(); - } - }); + setFullScreenDialogClose(v -> finish()); - Timber.i("dataUri: " + mDataUri.toString()); + long masterKeyId = extras.getLong(EXTRA_MASTER_KEY_ID); + UnifiedKeyInfoViewModel viewModel = ViewModelProviders.of(this).get(UnifiedKeyInfoViewModel.class); + viewModel.setMasterKeyId(masterKeyId); - startFragment(savedInstanceState, mDataUri); + if (savedInstanceState == null) { + startFragment(); + } } @Override @@ -58,24 +59,9 @@ public class CertifyFingerprintActivity extends BaseActivity { setContentView(R.layout.certify_fingerprint_activity); } - private void startFragment(Bundle savedInstanceState, Uri dataUri) { - // However, if we're being restored from a previous state, - // then we don't need to do anything and should return or else - // we could end up with overlapping fragments. - if (savedInstanceState != null) { - return; - } - - // Create an instance of the fragment - CertifyFingerprintFragment frag = CertifyFingerprintFragment.newInstance(dataUri); - - // Add the fragment to the 'fragment_container' FrameLayout - // NOTE: We use commitAllowingStateLoss() to prevent weird crashes! - getSupportFragmentManager().beginTransaction() - .replace(R.id.certify_fingerprint_fragment, frag) - .commitAllowingStateLoss(); - // do it immediately! - getSupportFragmentManager().executePendingTransactions(); + private void startFragment() { + CertifyFingerprintFragment frag = CertifyFingerprintFragment.newInstance(); + getSupportFragmentManager().beginTransaction().replace(R.id.certify_fingerprint_fragment, frag).commit(); } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyFingerprintFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyFingerprintFragment.java index 2c79fee18..9abb2ad39 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyFingerprintFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyFingerprintFragment.java @@ -18,187 +18,79 @@ package org.sufficientlysecure.keychain.ui; +import android.arch.lifecycle.ViewModelProviders; import android.content.Intent; -import android.database.Cursor; -import android.net.Uri; import android.os.Bundle; -import android.support.v4.app.LoaderManager; -import android.support.v4.content.CursorLoader; -import android.support.v4.content.Loader; +import android.support.annotation.NonNull; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentActivity; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; -import org.sufficientlysecure.keychain.provider.KeyRepository; -import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; -import org.sufficientlysecure.keychain.ui.base.LoaderFragment; +import org.sufficientlysecure.keychain.model.SubKey.UnifiedKeyInfo; +import org.sufficientlysecure.keychain.ui.keyview.UnifiedKeyInfoViewModel; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; -import timber.log.Timber; -public class CertifyFingerprintFragment extends LoaderFragment implements - LoaderManager.LoaderCallbacks { - +public class CertifyFingerprintFragment extends Fragment { static final int REQUEST_CERTIFY = 1; - public static final String ARG_DATA_URI = "uri"; + private TextView vFingerprint; - private TextView mActionYes; - private TextView mFingerprint; - private TextView mIntro; - private TextView mHeader; + private UnifiedKeyInfoViewModel viewModel; - private static final int LOADER_ID_UNIFIED = 0; - - private Uri mDataUri; - - /** - * Creates new instance of this fragment - */ - public static CertifyFingerprintFragment newInstance(Uri dataUri) { - CertifyFingerprintFragment frag = new CertifyFingerprintFragment(); - Bundle args = new Bundle(); - args.putParcelable(ARG_DATA_URI, dataUri); - - frag.setArguments(args); - - return frag; + public static CertifyFingerprintFragment newInstance() { + return new CertifyFingerprintFragment(); } @Override - public View onCreateView(LayoutInflater inflater, ViewGroup superContainer, Bundle savedInstanceState) { - View root = super.onCreateView(inflater, superContainer, savedInstanceState); - View view = inflater.inflate(R.layout.certify_fingerprint_fragment, getContainer()); + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup viewGroup, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.certify_fingerprint_fragment, viewGroup, false); - TextView actionNo = view.findViewById(R.id.certify_fingerprint_button_no); - mActionYes = view.findViewById(R.id.certify_fingerprint_button_yes); + vFingerprint = view.findViewById(R.id.certify_fingerprint_fingerprint); - mFingerprint = view.findViewById(R.id.certify_fingerprint_fingerprint); - mIntro = view.findViewById(R.id.certify_fingerprint_intro); - mHeader = view.findViewById(R.id.certify_fingerprint_fingerprint_header); + view.findViewById(R.id.certify_fingerprint_button_no).setOnClickListener(v -> requireActivity().finish()); + view.findViewById(R.id.certify_fingerprint_button_yes).setOnClickListener(v -> startCertifyActivity()); - actionNo.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - getActivity().finish(); - } - }); - mActionYes.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - certify(mDataUri); - } - }); - - return root; + return view; } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - Uri dataUri = getArguments().getParcelable(ARG_DATA_URI); - if (dataUri == null) { - Timber.e("Data missing. Should be Uri of key!"); - getActivity().finish(); + viewModel = ViewModelProviders.of(requireActivity()).get(UnifiedKeyInfoViewModel.class); + viewModel.getUnifiedKeyInfoLiveData(requireContext()).observe(this, this::onLoadUnifiedKeyInfo); + } + + private void onLoadUnifiedKeyInfo(UnifiedKeyInfo unifiedKeyInfo) { + if (unifiedKeyInfo == null) { return; } - loadData(dataUri); + String fingerprint = KeyFormattingUtils.convertFingerprintToHex(unifiedKeyInfo.fingerprint()); + vFingerprint.setText(KeyFormattingUtils.formatFingerprint(fingerprint)); } - private void loadData(Uri dataUri) { - mDataUri = dataUri; - - Timber.i("dataUri: " + mDataUri.toString()); - - // Prepare the loaders. Either re-connect with an existing ones, - // or start new ones. - getLoaderManager().initLoader(LOADER_ID_UNIFIED, null, this); - } - - static final String[] UNIFIED_PROJECTION = new String[]{ - KeyRings._ID, KeyRings.FINGERPRINT, - - }; - static final int INDEX_UNIFIED_FINGERPRINT = 1; - - public Loader onCreateLoader(int id, Bundle args) { - setContentShown(false); - switch (id) { - case LOADER_ID_UNIFIED: { - Uri baseUri = KeyRings.buildUnifiedKeyRingUri(mDataUri); - return new CursorLoader(getActivity(), baseUri, UNIFIED_PROJECTION, null, null, null); - } - - default: - return null; - } - } - - public void onLoadFinished(Loader loader, Cursor data) { - /* TODO better error handling? May cause problems when a key is deleted, - * because the notification triggers faster than the activity closes. - */ - // Avoid NullPointerExceptions... - if (data.getCount() == 0) { - return; - } - // Swap the new cursor in. (The framework will take care of closing the - // old cursor once we return.) - switch (loader.getId()) { - case LOADER_ID_UNIFIED: { - if (data.moveToFirst()) { - byte[] fingerprintBlob = data.getBlob(INDEX_UNIFIED_FINGERPRINT); - - displayHexConfirm(fingerprintBlob); - - break; - } - } - - } - setContentShown(true); - } - - private void displayHexConfirm(byte[] fingerprintBlob) { - String fingerprint = KeyFormattingUtils.convertFingerprintToHex(fingerprintBlob); - mFingerprint.setText(KeyFormattingUtils.formatFingerprint(fingerprint)); - } - - /** - * This is called when the last Cursor provided to onLoadFinished() above is about to be closed. - * We need to make sure we are no longer using it. - */ - public void onLoaderReset(Loader loader) { - } - - private void certify(Uri dataUri) { - long keyId = 0; - try { - keyId = KeyRepository.create(getContext()) - .getCachedPublicKeyRing(dataUri) - .extractOrGetMasterKeyId(); - } catch (PgpKeyNotFoundException e) { - Timber.e(e, "key not found!"); - } + private void startCertifyActivity() { Intent certifyIntent = new Intent(getActivity(), CertifyKeyActivity.class); - certifyIntent.putExtras(getActivity().getIntent()); - certifyIntent.putExtra(CertifyKeyActivity.EXTRA_KEY_IDS, new long[]{keyId}); + certifyIntent.putExtras(requireActivity().getIntent()); + certifyIntent.putExtra(CertifyKeyActivity.EXTRA_KEY_IDS, new long[] { viewModel.getMasterKeyId() }); startActivityForResult(certifyIntent, REQUEST_CERTIFY); } @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { - // always just pass this one through if (requestCode == REQUEST_CERTIFY) { - getActivity().setResult(resultCode, data); - getActivity().finish(); + FragmentActivity activity = requireActivity(); + activity.setResult(resultCode, data); + activity.finish(); return; } + super.onActivityResult(requestCode, resultCode, data); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyKeyFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyKeyFragment.java index 38577923a..29ff936fb 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyKeyFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyKeyFragment.java @@ -20,101 +20,105 @@ package org.sufficientlysecure.keychain.ui; import java.util.ArrayList; import java.util.Date; +import java.util.List; import android.app.Activity; +import android.arch.lifecycle.LiveData; +import android.arch.lifecycle.ViewModelProviders; import android.content.Intent; import android.graphics.PorterDuff; import android.os.Bundle; +import android.support.annotation.NonNull; import android.view.LayoutInflater; import android.view.View; -import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.CheckBox; import android.widget.ImageView; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.model.SubKey.UnifiedKeyInfo; import org.sufficientlysecure.keychain.operations.results.CertifyResult; import org.sufficientlysecure.keychain.operations.results.OperationResult; -import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; -import org.sufficientlysecure.keychain.provider.CachedPublicKeyRing; -import org.sufficientlysecure.keychain.provider.KeyRepository; +import org.sufficientlysecure.keychain.daos.KeyRepository; import org.sufficientlysecure.keychain.service.CertifyActionsParcel; import org.sufficientlysecure.keychain.service.CertifyActionsParcel.CertifyAction; import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import org.sufficientlysecure.keychain.ui.base.CachingCryptoOperationFragment; +import org.sufficientlysecure.keychain.ui.keyview.GenericViewModel; import org.sufficientlysecure.keychain.ui.util.FormattingUtils; import org.sufficientlysecure.keychain.ui.util.Notify; -import org.sufficientlysecure.keychain.ui.widget.CertifyKeySpinner; +import org.sufficientlysecure.keychain.ui.widget.KeySpinner; import org.sufficientlysecure.keychain.util.Preferences; -import timber.log.Timber; -public class CertifyKeyFragment - extends CachingCryptoOperationFragment { +public class CertifyKeyFragment extends CachingCryptoOperationFragment { + private KeyRepository keyRepository; - private CheckBox mUploadKeyCheckbox; + private CheckBox uploadKeyCheckbox; + private KeySpinner certifyKeySpinner; + private MultiUserIdsFragment multiUserIdsFragment; - private CertifyKeySpinner mCertifyKeySpinner; - - private MultiUserIdsFragment mMultiUserIdsFragment; + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + keyRepository = KeyRepository.create(requireContext()); + } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); + Activity activity = requireActivity(); + + GenericViewModel viewModel = ViewModelProviders.of(this).get(GenericViewModel.class); + LiveData> liveData = viewModel.getGenericLiveData( + requireContext(), keyRepository::getAllUnifiedKeyInfoWithSecret); + liveData.observe(this, certifyKeySpinner::setData); if (savedInstanceState == null) { // preselect certify key id if given - long certifyKeyId = getActivity().getIntent() + long certifyKeyId = activity.getIntent() .getLongExtra(CertifyKeyActivity.EXTRA_CERTIFY_KEY_ID, Constants.key.none); if (certifyKeyId != Constants.key.none) { - try { - CachedPublicKeyRing key = (KeyRepository - .create(getContext())) - .getCachedPublicKeyRing(certifyKeyId); - if (key.canCertify()) { - mCertifyKeySpinner.setPreSelectedKeyId(certifyKeyId); - } - } catch (PgpKeyNotFoundException e) { - Timber.e(e, "certify certify check failed"); + UnifiedKeyInfo unifiedKeyInfo = keyRepository.getUnifiedKeyInfo(certifyKeyId); + if (unifiedKeyInfo != null && unifiedKeyInfo.can_certify()) { + certifyKeySpinner.setPreSelectedKeyId(certifyKeyId); } } } - OperationResult result = getActivity().getIntent().getParcelableExtra(CertifyKeyActivity.EXTRA_RESULT); + OperationResult result = activity.getIntent().getParcelableExtra(CertifyKeyActivity.EXTRA_RESULT); if (result != null) { // display result from import - result.createNotify(getActivity()).show(); + result.createNotify(activity).show(); } } @Override - public View onCreateView(LayoutInflater inflater, ViewGroup superContainer, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.certify_key_fragment, null); + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup superContainer, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.certify_key_fragment, superContainer, false); - mCertifyKeySpinner = view.findViewById(R.id.certify_key_spinner); - mUploadKeyCheckbox = view.findViewById(R.id.sign_key_upload_checkbox); - mMultiUserIdsFragment = (MultiUserIdsFragment) + certifyKeySpinner = view.findViewById(R.id.certify_key_spinner); + uploadKeyCheckbox = view.findViewById(R.id.sign_key_upload_checkbox); + multiUserIdsFragment = (MultiUserIdsFragment) getChildFragmentManager().findFragmentById(R.id.multi_user_ids_fragment); + certifyKeySpinner.setShowNone(R.string.choice_select_cert); + // make certify image gray, like action icons ImageView vActionCertifyImage = view.findViewById(R.id.certify_key_action_certify_image); - vActionCertifyImage.setColorFilter(FormattingUtils.getColorFromAttr(getActivity(), R.attr.colorTertiaryText), + vActionCertifyImage.setColorFilter(FormattingUtils.getColorFromAttr(requireActivity(), R.attr.colorTertiaryText), PorterDuff.Mode.SRC_IN); View vCertifyButton = view.findViewById(R.id.certify_key_certify_button); - vCertifyButton.setOnClickListener(new OnClickListener() { - - @Override - public void onClick(View v) { - long selectedKeyId = mCertifyKeySpinner.getSelectedKeyId(); - if (selectedKeyId == Constants.key.none) { - Notify.create(getActivity(), getString(R.string.select_key_to_certify), - Notify.Style.ERROR).show(); - } else { - cryptoOperation(CryptoInputParcel.createCryptoInputParcel(new Date())); - } + vCertifyButton.setOnClickListener(v -> { + long selectedKeyId = certifyKeySpinner.getSelectedKeyId(); + if (selectedKeyId == Constants.key.none) { + Notify.create(getActivity(), getString(R.string.select_key_to_certify), + Notify.Style.ERROR).show(); + } else { + cryptoOperation(CryptoInputParcel.createCryptoInputParcel(new Date())); } }); @@ -123,22 +127,20 @@ public class CertifyKeyFragment @Override public CertifyActionsParcel createOperationInput() { - // Bail out if there is not at least one user id selected - ArrayList certifyActions = mMultiUserIdsFragment.getSelectedCertifyActions(); + ArrayList certifyActions = multiUserIdsFragment.getSelectedCertifyActions(); if (certifyActions.isEmpty()) { - Notify.create(getActivity(), "No identities selected!", - Notify.Style.ERROR).show(); + Notify.create(getActivity(), "No identities selected!", Notify.Style.ERROR).show(); return null; } - long selectedKeyId = mCertifyKeySpinner.getSelectedKeyId(); + long selectedKeyId = certifyKeySpinner.getSelectedKeyId(); // fill values for this action CertifyActionsParcel.Builder actionsParcel = CertifyActionsParcel.builder(selectedKeyId); actionsParcel.addActions(certifyActions); - if (mUploadKeyCheckbox.isChecked()) { + if (uploadKeyCheckbox.isChecked()) { actionsParcel.setParcelableKeyServer(Preferences.getPreferences(getActivity()).getPreferredKeyserver()); } @@ -151,7 +153,7 @@ public class CertifyKeyFragment @Override public void onQueuedOperationSuccess(CertifyResult result) { // protected by Queueing*Fragment - Activity activity = getActivity(); + Activity activity = requireActivity(); Intent intent = new Intent(); intent.putExtra(CertifyResult.EXTRA_RESULT, result); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyFinalFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyFinalFragment.java index f8431bf3f..5eabe1e44 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyFinalFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyFinalFragment.java @@ -20,11 +20,11 @@ package org.sufficientlysecure.keychain.ui; import java.util.Date; import java.util.Iterator; +import java.util.List; import java.util.regex.Pattern; import android.app.Activity; import android.content.Intent; -import android.database.Cursor; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.v4.app.Fragment; @@ -43,14 +43,13 @@ import org.openintents.openpgp.util.OpenPgpUtils; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.keyimport.HkpKeyserverAddress; +import org.sufficientlysecure.keychain.model.SubKey; import org.sufficientlysecure.keychain.operations.results.EditKeyResult; import org.sufficientlysecure.keychain.operations.results.OperationResult; import org.sufficientlysecure.keychain.operations.results.UploadResult; import org.sufficientlysecure.keychain.pgp.KeyRing; -import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; -import org.sufficientlysecure.keychain.provider.CachedPublicKeyRing; -import org.sufficientlysecure.keychain.provider.KeyRepository; -import org.sufficientlysecure.keychain.provider.KeychainContract; +import org.sufficientlysecure.keychain.daos.KeyRepository; +import org.sufficientlysecure.keychain.daos.KeyRepository.NotFoundException; import org.sufficientlysecure.keychain.service.ChangeUnlockParcel; import org.sufficientlysecure.keychain.service.SaveKeyringParcel; import org.sufficientlysecure.keychain.service.SaveKeyringParcel.SubkeyChange; @@ -410,31 +409,20 @@ public class CreateKeyFinalFragment extends Fragment { private void moveToCard(final EditKeyResult saveKeyResult) { CreateKeyActivity activity = (CreateKeyActivity) getActivity(); + KeyRepository keyRepository = KeyRepository.create(getContext()); SaveKeyringParcel.Builder builder; - CachedPublicKeyRing key = (KeyRepository.create(getContext())) - .getCachedPublicKeyRing(saveKeyResult.mMasterKeyId); try { - builder = SaveKeyringParcel.buildChangeKeyringParcel(key.getMasterKeyId(), key.getFingerprint()); - } catch (PgpKeyNotFoundException e) { + byte[] fingerprint = keyRepository.getFingerprintByKeyId(saveKeyResult.mMasterKeyId); + builder = SaveKeyringParcel.buildChangeKeyringParcel(saveKeyResult.mMasterKeyId, fingerprint); + } catch (NotFoundException e) { Timber.e("Key that should be moved to Security Token not found in database!"); return; } - // define subkeys that should be moved to the card - Cursor cursor = activity.getContentResolver().query( - KeychainContract.Keys.buildKeysUri(builder.getMasterKeyId()), - new String[]{KeychainContract.Keys.KEY_ID,}, null, null, null - ); - try { - while (cursor != null && cursor.moveToNext()) { - long subkeyId = cursor.getLong(0); - builder.addOrReplaceSubkeyChange(SubkeyChange.createMoveToSecurityTokenChange(subkeyId)); - } - } finally { - if (cursor != null) { - cursor.close(); - } + List subKeys = keyRepository.getSubKeysByMasterKeyId(saveKeyResult.mMasterKeyId); + for (SubKey subKey : subKeys) { + builder.addOrReplaceSubkeyChange(SubkeyChange.createMoveToSecurityTokenChange(subKey.key_id())); } // define new PIN and Admin PIN for the card diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DebugActionsActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DebugActionsActivity.java new file mode 100644 index 000000000..1e15a1e84 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DebugActionsActivity.java @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2017 Schürmann & Breitmoser GbR + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui; + + +import java.util.ArrayList; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.app.Activity; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.content.IntentSender.SendIntentException; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.os.Build.VERSION_CODES; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v7.widget.Toolbar; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewGroup.LayoutParams; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.ScrollView; +import android.widget.TextView; + +import org.sufficientlysecure.keychain.BuildConfig; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.daos.ApiAppDao; +import org.sufficientlysecure.keychain.daos.KeyRepository; +import org.sufficientlysecure.keychain.remote.ApiPendingIntentFactory; +import org.sufficientlysecure.keychain.ui.util.Notify; +import org.sufficientlysecure.keychain.ui.util.Notify.Style; +import timber.log.Timber; + + +@TargetApi(VERSION_CODES.LOLLIPOP) +public class DebugActionsActivity extends Activity { + + private ApiPendingIntentFactory pendingIntentFactory; + private ApiAppDao apiAppDao; + private KeyRepository keyRepository; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if (!Constants.DEBUG) { + throw new UnsupportedOperationException(); + } + + pendingIntentFactory = new ApiPendingIntentFactory(getBaseContext()); + apiAppDao = ApiAppDao.getInstance(getBaseContext()); + keyRepository = KeyRepository.create(getBaseContext()); + + setContentView(createView()); + } + + private View createView() { + Context context = getBaseContext(); + + LinearLayout verticalLayout = new LinearLayout(context); + verticalLayout.setOrientation(LinearLayout.VERTICAL); + verticalLayout.setPadding(0, 40, 0, 0); + + Toolbar toolbar = new Toolbar(this); + toolbar.setTitle("Debug Actions"); + verticalLayout.addView(toolbar, new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); + + addButtonToLayout(context, verticalLayout, "Register ApiApp").setOnClickListener((v) -> { + PendingIntent pendingIntent = pendingIntentFactory.createRegisterPendingIntent( + new Intent(), BuildConfig.APPLICATION_ID, getPackageSig()); + startPendingIntent(pendingIntent); + }); + addButtonToLayout(context, verticalLayout, "Unregister ApiApp").setOnClickListener((v) -> { + apiAppDao.deleteApiApp(BuildConfig.APPLICATION_ID); + Notify.create(DebugActionsActivity.this, "Ok", Style.OK).show(); + }); + addButtonToLayout(context, verticalLayout, "Select Public Key").setOnClickListener((v) -> { + PendingIntent pendingIntent = pendingIntentFactory.createSelectPublicKeyPendingIntent( + new Intent(), new long[] {}, new ArrayList<>(), new ArrayList<>(), false); + startPendingIntent(pendingIntent); + }); + addButtonToLayout(context, verticalLayout, "Select Signing Key (legacy)").setOnClickListener((v) -> { + PendingIntent pendingIntent = pendingIntentFactory.createSelectSignKeyIdLegacyPendingIntent( + new Intent(), BuildConfig.APPLICATION_ID, "test@openkeychain.org"); + startPendingIntent(pendingIntent); + }); + addButtonToLayout(context, verticalLayout, "Select Signing Key").setOnClickListener((v) -> { + PendingIntent pendingIntent = pendingIntentFactory.createSelectSignKeyIdPendingIntent( + new Intent(), BuildConfig.APPLICATION_ID, getPackageSig(), "test@openkeychain.org", false); + startPendingIntent(pendingIntent); + }); + addButtonToLayout(context, verticalLayout, "Select Authentication Key").setOnClickListener((v) -> { + PendingIntent pendingIntent = pendingIntentFactory.createSelectAuthenticationKeyIdPendingIntent( + new Intent(), BuildConfig.APPLICATION_ID); + startPendingIntent(pendingIntent); + }); + addButtonToLayout(context, verticalLayout, "Select Signing Key (Autocrypt)").setOnClickListener((v) -> { + PendingIntent pendingIntent = pendingIntentFactory.createSelectSignKeyIdPendingIntent( + new Intent(), BuildConfig.APPLICATION_ID, getPackageSig(), "test@openkeychain.org", true); + startPendingIntent(pendingIntent); + }); + addButtonToLayout(context, verticalLayout, "Request Permission (first secret key)").setOnClickListener((v) -> { + long firstMasterKeyId = keyRepository.getAllUnifiedKeyInfoWithSecret().get(0).master_key_id(); + PendingIntent pendingIntent = pendingIntentFactory.createRequestKeyPermissionPendingIntent( + new Intent(), BuildConfig.APPLICATION_ID, firstMasterKeyId); + startPendingIntent(pendingIntent); + }); + addButtonToLayout(context, verticalLayout, "Deduplicate (dupl@mugenguild.com)").setOnClickListener((v) -> { + ArrayList duplicateEmails = new ArrayList<>(); + duplicateEmails.add("dupl@mugenguild.com"); + PendingIntent pendingIntent = pendingIntentFactory.createDeduplicatePendingIntent( + BuildConfig.APPLICATION_ID, new Intent(), duplicateEmails); + startPendingIntent(pendingIntent); + }); + + ScrollView view = new ScrollView(context); + view.addView(verticalLayout, new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); + return view; + } + + private TextView addButtonToLayout(Context context, ViewGroup buttonContainer, String buttonLabel) { + TextView button = new TextView(context, null, 0, R.style.DebugButton); + button.setText(buttonLabel); + buttonContainer.addView(button, new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); + return button; + } + + @SuppressLint("PackageManagerGetSignatures") + private byte[] getPackageSig() { + try { + PackageManager packageManager = getPackageManager(); + PackageInfo packageInfo = + packageManager.getPackageInfo(BuildConfig.APPLICATION_ID, PackageManager.GET_SIGNATURES); + return packageInfo.signatures[0].toByteArray(); + } catch (NameNotFoundException e) { + throw new AssertionError(e); + } + } + + private void startPendingIntent(PendingIntent pendingIntent) { + try { + startIntentSenderForResult(pendingIntent.getIntentSender(), 0, null, 0, 0, 0); + } catch (SendIntentException e) { + Timber.e(e); + } + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (data != null) { + if (resultCode == RESULT_OK) { + Notify.create(DebugActionsActivity.this, "Ok", Style.OK).show(); + Timber.d("result: ok, intent: %s, extras: %s", data.toString(), data.getExtras()); + } else { + Timber.d("result: cancelled, intent: %s, extras: %s", data.toString(), data.getExtras()); + } + } else { + if (resultCode == RESULT_OK) { + Notify.create(DebugActionsActivity.this, "Ok", Style.OK).show(); + Timber.d("result: ok, intent: null"); + } else { + Timber.d("result: cancelled, intent: null"); + } + } + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptFragment.java index 0069278d5..8e233c7a9 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptFragment.java @@ -21,17 +21,12 @@ package org.sufficientlysecure.keychain.ui; import java.util.ArrayList; import android.app.Activity; +import android.arch.lifecycle.LiveData; import android.content.Intent; -import android.database.Cursor; -import android.net.Uri; import android.os.Bundle; +import android.support.annotation.NonNull; import android.support.v4.app.Fragment; -import android.support.v4.app.LoaderManager; -import android.support.v4.content.CursorLoader; -import android.support.v4.content.Loader; -import android.util.Log; import android.view.View; -import android.view.View.OnClickListener; import android.widget.Button; import android.widget.ImageView; import android.widget.LinearLayout; @@ -41,17 +36,16 @@ import android.widget.ViewAnimator; import org.openintents.openpgp.OpenPgpDecryptionResult; import org.openintents.openpgp.OpenPgpSignatureResult; import org.openintents.openpgp.util.OpenPgpUtils; -import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.keyimport.HkpKeyserverAddress; import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing; +import org.sufficientlysecure.keychain.livedata.GenericLiveData; +import org.sufficientlysecure.keychain.model.SubKey.UnifiedKeyInfo; import org.sufficientlysecure.keychain.operations.results.DecryptVerifyResult; import org.sufficientlysecure.keychain.operations.results.ImportKeyResult; +import org.sufficientlysecure.keychain.pgp.CanonicalizedKeyRing.VerificationStatus; import org.sufficientlysecure.keychain.pgp.KeyRing; -import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; -import org.sufficientlysecure.keychain.provider.KeyRepository; -import org.sufficientlysecure.keychain.provider.KeychainContract; -import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; +import org.sufficientlysecure.keychain.daos.KeyRepository; import org.sufficientlysecure.keychain.service.ImportKeyringParcel; import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper; import org.sufficientlysecure.keychain.ui.keyview.ViewKeyActivity; @@ -63,9 +57,7 @@ import org.sufficientlysecure.keychain.util.Preferences; import timber.log.Timber; -public abstract class DecryptFragment extends Fragment implements LoaderManager.LoaderCallbacks { - - public static final int LOADER_ID_UNIFIED = 0; +public abstract class DecryptFragment extends Fragment { public static final String ARG_DECRYPT_VERIFY_RESULT = "decrypt_verify_result"; protected LinearLayout mResultLayout; @@ -83,32 +75,29 @@ public abstract class DecryptFragment extends Fragment implements LoaderManager. private ViewAnimator mOverlayAnimator; private CryptoOperationHelper mImportOpHelper; + private LiveData unifiedKeyInfoLiveData; @Override - public void onViewCreated(View view, Bundle savedInstanceState) { + public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); + Activity activity = requireActivity(); // NOTE: These views are inside the activity! - mResultLayout = getActivity().findViewById(R.id.result_main_layout); + mResultLayout = activity.findViewById(R.id.result_main_layout); + mEncryptionIcon = activity.findViewById(R.id.result_encryption_icon); + mEncryptionText = activity.findViewById(R.id.result_encryption_text); + mSignatureIcon = activity.findViewById(R.id.result_signature_icon); + mSignatureText = activity.findViewById(R.id.result_signature_text); + mSignatureLayout = activity.findViewById(R.id.result_signature_layout); + mSignatureName = activity.findViewById(R.id.result_signature_name); + mSignatureEmail = activity.findViewById(R.id.result_signature_email); + mSignatureAction = activity.findViewById(R.id.result_signature_action); mResultLayout.setVisibility(View.GONE); - mEncryptionIcon = getActivity().findViewById(R.id.result_encryption_icon); - mEncryptionText = getActivity().findViewById(R.id.result_encryption_text); - mSignatureIcon = getActivity().findViewById(R.id.result_signature_icon); - mSignatureText = getActivity().findViewById(R.id.result_signature_text); - mSignatureLayout = getActivity().findViewById(R.id.result_signature_layout); - mSignatureName = getActivity().findViewById(R.id.result_signature_name); - mSignatureEmail = getActivity().findViewById(R.id.result_signature_email); - mSignatureAction = getActivity().findViewById(R.id.result_signature_action); // Overlay mOverlayAnimator = (ViewAnimator) view; Button vErrorOverlayButton = view.findViewById(R.id.decrypt_error_overlay_button); - vErrorOverlayButton.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - mOverlayAnimator.setDisplayedChild(0); - } - }); + vErrorOverlayButton.setOnClickListener(v -> mOverlayAnimator.setDisplayedChild(0)); } private void showErrorOverlay(boolean overlay) { @@ -119,7 +108,7 @@ public abstract class DecryptFragment extends Fragment implements LoaderManager. } @Override - public void onSaveInstanceState(Bundle outState) { + public void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); outState.putParcelable(ARG_DECRYPT_VERIFY_RESULT, mDecryptVerifyResult); @@ -168,7 +157,7 @@ public abstract class DecryptFragment extends Fragment implements LoaderManager. public void onCryptoOperationSuccess(ImportKeyResult result) { result.createNotify(getActivity()).show(); - getLoaderManager().restartLoader(LOADER_ID_UNIFIED, null, DecryptFragment.this); + loadSignerKeyData(); } @Override @@ -193,18 +182,14 @@ public abstract class DecryptFragment extends Fragment implements LoaderManager. } private void showKey(long keyId) { - try { - - Intent viewKeyIntent = new Intent(getActivity(), ViewKeyActivity.class); - long masterKeyId = KeyRepository.create(getContext()).getCachedPublicKeyRing( - KeyRings.buildUnifiedKeyRingsFindBySubkeyUri(keyId) - ).getMasterKeyId(); - viewKeyIntent.setData(KeyRings.buildGenericKeyRingUri(masterKeyId)); - startActivity(viewKeyIntent); - - } catch (PgpKeyNotFoundException e) { + KeyRepository keyRepository = KeyRepository.create(requireContext()); + Long masterKeyId = keyRepository.getMasterKeyIdBySubkeyId(keyId); + if (masterKeyId == null) { Notify.create(getActivity(), R.string.error_key_not_found, Style.ERROR).show(); + return; } + Intent viewKeyIntent = ViewKeyActivity.getViewKeyActivityIntent(requireActivity(), masterKeyId); + startActivity(viewKeyIntent); } protected void loadVerifyResult(DecryptVerifyResult decryptVerifyResult) { @@ -244,16 +229,13 @@ public abstract class DecryptFragment extends Fragment implements LoaderManager. mSignatureText.setText(R.string.decrypt_result_no_signature); KeyFormattingUtils.setStatusImage(getActivity(), mSignatureIcon, mSignatureText, State.NOT_SIGNED); - getLoaderManager().destroyLoader(LOADER_ID_UNIFIED); - + loadSignerKeyData(); showErrorOverlay(false); onVerifyLoaded(true); } else { // signature present - - // after loader is restarted signature results are checked - getLoaderManager().restartLoader(LOADER_ID_UNIFIED, null, this); + loadSignerKeyData(); } } @@ -264,70 +246,43 @@ public abstract class DecryptFragment extends Fragment implements LoaderManager. private void setShowAction(final long signatureKeyId) { mSignatureAction.setText(R.string.decrypt_result_action_show); mSignatureAction.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.ic_vpn_key_grey_24dp, 0); - mSignatureLayout.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - showKey(signatureKeyId); - } - }); + mSignatureLayout.setOnClickListener(v -> showKey(signatureKeyId)); } - // These are the rows that we will retrieve. - static final String[] UNIFIED_PROJECTION = new String[]{ - KeychainContract.KeyRings._ID, - KeychainContract.KeyRings.MASTER_KEY_ID, - KeychainContract.KeyRings.USER_ID, - KeychainContract.KeyRings.VERIFIED, - KeychainContract.KeyRings.HAS_ANY_SECRET, - KeyRings.NAME, - KeyRings.EMAIL, - KeyRings.COMMENT, - }; - - @SuppressWarnings("unused") - static final int INDEX_MASTER_KEY_ID = 1; - static final int INDEX_USER_ID = 2; - static final int INDEX_VERIFIED = 3; - static final int INDEX_HAS_ANY_SECRET = 4; - static final int INDEX_NAME = 5; - static final int INDEX_EMAIL = 6; - static final int INDEX_COMMENT = 7; - - @Override - public Loader onCreateLoader(int id, Bundle args) { - if (id != LOADER_ID_UNIFIED) { - return null; + public void loadSignerKeyData() { + if (unifiedKeyInfoLiveData != null) { + unifiedKeyInfoLiveData.removeObservers(this); + unifiedKeyInfoLiveData = null; } - Uri baseUri = KeychainContract.KeyRings.buildUnifiedKeyRingsFindBySubkeyUri( - mSignatureResult.getKeyId()); - return new CursorLoader(getActivity(), baseUri, UNIFIED_PROJECTION, null, null, null); - } - - @Override - public void onLoadFinished(Loader loader, Cursor data) { - - if (loader.getId() != LOADER_ID_UNIFIED) { + if (mSignatureResult == null || mSignatureResult.getResult() == OpenPgpSignatureResult.RESULT_NO_SIGNATURE) { + setSignatureLayoutVisibility(View.GONE); return; } - // If the key is unknown, show it as such - if (data.getCount() == 0 || !data.moveToFirst()) { + unifiedKeyInfoLiveData = new GenericLiveData<>(requireContext(), () -> { + KeyRepository keyRepository = KeyRepository.create(requireContext()); + Long masterKeyId = keyRepository.getMasterKeyIdBySubkeyId(mSignatureResult.getKeyId()); + return keyRepository.getUnifiedKeyInfo(masterKeyId); + }); + unifiedKeyInfoLiveData.observe(this, this::onLoadSignerKeyData); + } + + public void onLoadSignerKeyData(UnifiedKeyInfo unifiedKeyInfo) { + if (unifiedKeyInfo == null) { showUnknownKeyStatus(); return; } long signatureKeyId = mSignatureResult.getKeyId(); - String name = data.getString(INDEX_NAME); - String email = data.getString(INDEX_EMAIL); - if (name != null) { - mSignatureName.setText(name); + if (unifiedKeyInfo.name() != null) { + mSignatureName.setText(unifiedKeyInfo.name()); } else { mSignatureName.setText(R.string.user_id_no_name); } - if (email != null) { - mSignatureEmail.setText(email); + if (unifiedKeyInfo.email() != null) { + mSignatureEmail.setText(unifiedKeyInfo.email()); } else { mSignatureEmail.setText(KeyFormattingUtils.beautifyKeyIdWithPrefix( mSignatureResult.getKeyId())); @@ -338,8 +293,8 @@ public abstract class DecryptFragment extends Fragment implements LoaderManager. boolean isRevoked = mSignatureResult.getResult() == OpenPgpSignatureResult.RESULT_INVALID_KEY_REVOKED; boolean isExpired = mSignatureResult.getResult() == OpenPgpSignatureResult.RESULT_INVALID_KEY_EXPIRED; boolean isInsecure = mSignatureResult.getResult() == OpenPgpSignatureResult.RESULT_INVALID_KEY_INSECURE; - boolean isVerified = data.getInt(INDEX_VERIFIED) > 0; - boolean isYours = data.getInt(INDEX_HAS_ANY_SECRET) != 0; + boolean isVerified = unifiedKeyInfo.verified() == VerificationStatus.VERIFIED_SECRET; + boolean isYours = unifiedKeyInfo.has_any_secret(); if (isRevoked) { mSignatureText.setText(R.string.decrypt_result_signature_revoked_key); @@ -409,16 +364,6 @@ public abstract class DecryptFragment extends Fragment implements LoaderManager. } - @Override - public void onLoaderReset(Loader loader) { - - if (loader.getId() != LOADER_ID_UNIFIED) { - return; - } - - setSignatureLayoutVisibility(View.GONE); - } - private void showUnknownKeyStatus() { final long signatureKeyId = mSignatureResult.getKeyId(); @@ -453,12 +398,7 @@ public abstract class DecryptFragment extends Fragment implements LoaderManager. mSignatureAction.setText(R.string.decrypt_result_action_Lookup); mSignatureAction .setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.ic_file_download_grey_24dp, 0); - mSignatureLayout.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - lookupUnknownKey(signatureKeyId); - } - }); + mSignatureLayout.setOnClickListener(v -> lookupUnknownKey(signatureKeyId)); showErrorOverlay(false); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptListFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptListFragment.java index b1aaecc20..8e2e9604f 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptListFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptListFragment.java @@ -1043,8 +1043,7 @@ public class DecryptListFragment if (activity == null) { return; } - Intent intent = new Intent(activity, ViewKeyActivity.class); - intent.setData(KeyRings.buildUnifiedKeyRingUri(keyId)); + Intent intent = ViewKeyActivity.getViewKeyActivityIntent(requireActivity(), keyId); activity.startActivity(intent); } }); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DeleteKeyDialogActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DeleteKeyDialogActivity.java index 7d13280ce..c4fee2953 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DeleteKeyDialogActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DeleteKeyDialogActivity.java @@ -17,8 +17,10 @@ package org.sufficientlysecure.keychain.ui; + +import java.util.Date; + import android.app.Activity; -import android.support.v7.app.AlertDialog; import android.app.Dialog; import android.content.DialogInterface; import android.content.Intent; @@ -26,6 +28,7 @@ import android.os.Bundle; import android.support.annotation.NonNull; import android.support.v4.app.DialogFragment; import android.support.v4.app.FragmentActivity; +import android.support.v7.app.AlertDialog; import android.view.ContextThemeWrapper; import android.view.LayoutInflater; import android.view.View; @@ -34,12 +37,11 @@ import android.widget.Spinner; import android.widget.TextView; import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.keyimport.HkpKeyserverAddress; +import org.sufficientlysecure.keychain.model.SubKey.UnifiedKeyInfo; import org.sufficientlysecure.keychain.operations.results.DeleteResult; import org.sufficientlysecure.keychain.operations.results.OperationResult; import org.sufficientlysecure.keychain.operations.results.RevokeResult; -import org.sufficientlysecure.keychain.provider.KeychainContract; -import org.sufficientlysecure.keychain.provider.KeyRepository; +import org.sufficientlysecure.keychain.daos.KeyRepository; import org.sufficientlysecure.keychain.service.DeleteKeyringParcel; import org.sufficientlysecure.keychain.service.RevokeKeyringParcel; import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; @@ -48,9 +50,6 @@ import org.sufficientlysecure.keychain.ui.dialog.CustomAlertDialogBuilder; import org.sufficientlysecure.keychain.ui.util.ThemeChanger; import timber.log.Timber; -import java.util.Date; -import java.util.HashMap; - public class DeleteKeyDialogActivity extends FragmentActivity { public static final String EXTRA_DELETE_MASTER_KEY_IDS = "extra_delete_master_key_ids"; public static final String EXTRA_HAS_SECRET = "extra_has_secret"; @@ -81,38 +80,28 @@ public class DeleteKeyDialogActivity extends FragmentActivity { log.add(OperationResult.LogType.MSG_DEL_ERROR_MULTI_SECRET, 0); returnResult(new DeleteResult(OperationResult.RESULT_ERROR, log, 0, mMasterKeyIds.length)); + return; } if (mMasterKeyIds.length == 1 && mHasSecret) { // if mMasterKeyIds.length == 0 we let the DeleteOperation respond - try { - HashMap data = KeyRepository.create(this).getUnifiedData( - mMasterKeyIds[0], new String[]{ - KeychainContract.KeyRings.NAME, - KeychainContract.KeyRings.IS_REVOKED - }, new int[]{ - KeyRepository.FIELD_TYPE_STRING, - KeyRepository.FIELD_TYPE_INTEGER - } - ); - - String name; - - name = (String) data.get(KeychainContract.KeyRings.NAME); - - if (name == null) { - name = getString(R.string.user_id_no_name); - } - - if ((long) data.get(KeychainContract.KeyRings.IS_REVOKED) > 0) { - showNormalDeleteDialog(); - } else { - showRevokeDeleteDialog(name); - } - } catch (KeyRepository.NotFoundException e) { - Timber.e(e, "Secret key to delete not found at DeleteKeyDialogActivity for " - + mMasterKeyIds[0]); + KeyRepository keyRepository = KeyRepository.create(this); + UnifiedKeyInfo keyInfo = keyRepository.getUnifiedKeyInfo(mMasterKeyIds[0]); + if (keyInfo == null) { + Timber.e("Secret key to delete not found at DeleteKeyDialogActivity for " + mMasterKeyIds[0]); finish(); + return; + } + + String name = keyInfo.name(); + if (name == null) { + name = getString(R.string.user_id_no_name); + } + + if (keyInfo.is_revoked()) { + showNormalDeleteDialog(); + } else { + showRevokeDeleteDialog(name); } } else { showNormalDeleteDialog(); @@ -267,36 +256,26 @@ public class DeleteKeyDialogActivity extends FragmentActivity { if (masterKeyIds.length == 1) { long masterKeyId = masterKeyIds[0]; - try { - HashMap data = KeyRepository.create(getContext()) - .getUnifiedData( - masterKeyId, new String[]{ - KeychainContract.KeyRings.NAME, - KeychainContract.KeyRings.HAS_ANY_SECRET - }, new int[]{ - KeyRepository.FIELD_TYPE_STRING, - KeyRepository.FIELD_TYPE_INTEGER - } - ); - String name; - - name = (String) data.get(KeychainContract.KeyRings.NAME); - if (name == null) { - name = getString(R.string.user_id_no_name); - } - - if (hasSecret) { - // show title only for secret key deletions, - // see http://www.google.com/design/spec/components/dialogs.html#dialogs-behavior - builder.setTitle(getString(R.string.title_delete_secret_key, name)); - mMainMessage.setText(getString(R.string.secret_key_deletion_confirmation, name)); - } else { - mMainMessage.setText(getString(R.string.public_key_deletetion_confirmation, name)); - } - } catch (KeyRepository.NotFoundException e) { + KeyRepository keyRepository = KeyRepository.create(getContext()); + UnifiedKeyInfo keyInfo = keyRepository.getUnifiedKeyInfo(masterKeyId); + if (keyInfo == null) { dismiss(); return null; } + String name = keyInfo.name(); + + if (name == null) { + name = getString(R.string.user_id_no_name); + } + + if (keyInfo.has_any_secret()) { + // show title only for secret key deletions, + // see http://www.google.com/design/spec/components/dialogs.html#dialogs-behavior + builder.setTitle(getString(R.string.title_delete_secret_key, name)); + mMainMessage.setText(getString(R.string.secret_key_deletion_confirmation, name)); + } else { + mMainMessage.setText(getString(R.string.public_key_deletetion_confirmation, name)); + } } else { mMainMessage.setText(R.string.key_deletion_confirmation_multi); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DisplayTextFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DisplayTextFragment.java index 5102d74fc..eb1c2e0ea 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DisplayTextFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DisplayTextFragment.java @@ -23,6 +23,7 @@ import android.content.ClipboardManager; import android.content.Context; import android.content.Intent; import android.os.Bundle; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.text.util.Linkify; import android.view.LayoutInflater; @@ -100,7 +101,7 @@ public class DisplayTextFragment extends DecryptFragment { } @Override - public void onViewCreated(View view, Bundle savedInstanceState) { + public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); Bundle args = getArguments(); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EditKeyActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EditKeyActivity.java index 7e5995a80..90184bc00 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EditKeyActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EditKeyActivity.java @@ -17,7 +17,7 @@ package org.sufficientlysecure.keychain.ui; -import android.net.Uri; + import android.os.Bundle; import org.sufficientlysecure.keychain.R; @@ -27,24 +27,20 @@ import timber.log.Timber; public class EditKeyActivity extends BaseActivity { - public static final String EXTRA_SAVE_KEYRING_PARCEL = "save_keyring_parcel"; - private EditKeyFragment mEditKeyFragment; - @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - Uri dataUri = getIntent().getData(); SaveKeyringParcel saveKeyringParcel = getIntent().getParcelableExtra(EXTRA_SAVE_KEYRING_PARCEL); - if (dataUri == null && saveKeyringParcel == null) { + if (saveKeyringParcel == null) { Timber.e("Either a key Uri or EXTRA_SAVE_KEYRING_PARCEL is required!"); finish(); return; } - loadFragment(savedInstanceState, dataUri, saveKeyringParcel); + loadFragment(savedInstanceState, saveKeyringParcel); } @Override @@ -52,7 +48,7 @@ public class EditKeyActivity extends BaseActivity { setContentView(R.layout.edit_key_activity); } - private void loadFragment(Bundle savedInstanceState, Uri dataUri, SaveKeyringParcel saveKeyringParcel) { + private void loadFragment(Bundle savedInstanceState, SaveKeyringParcel saveKeyringParcel) { // However, if we're being restored from a previous state, // then we don't need to do anything and should return or else // we could end up with overlapping fragments. @@ -61,16 +57,12 @@ public class EditKeyActivity extends BaseActivity { } // Create an instance of the fragment - if (dataUri != null) { - mEditKeyFragment = EditKeyFragment.newInstance(dataUri); - } else { - mEditKeyFragment = EditKeyFragment.newInstance(saveKeyringParcel); - } + EditKeyFragment editKeyFragment = EditKeyFragment.newInstance(saveKeyringParcel); // Add the fragment to the 'fragment_container' FrameLayout // NOTE: We use commitAllowingStateLoss() to prevent weird crashes! getSupportFragmentManager().beginTransaction() - .replace(R.id.edit_key_fragment_container, mEditKeyFragment) + .replace(R.id.edit_key_fragment_container, editKeyFragment) .commitAllowingStateLoss(); // do it immediately! getSupportFragmentManager().executePendingTransactions(); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EditKeyFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EditKeyFragment.java index aec5fac3c..0bbabeab6 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EditKeyFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EditKeyFragment.java @@ -18,104 +18,49 @@ package org.sufficientlysecure.keychain.ui; -import java.util.Date; - import android.app.Activity; import android.content.Intent; -import android.database.Cursor; -import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.os.Messenger; -import android.support.v4.app.LoaderManager; -import android.support.v4.content.CursorLoader; -import android.support.v4.content.Loader; +import android.support.annotation.NonNull; +import android.support.v4.app.Fragment; import android.view.LayoutInflater; import android.view.View; -import android.view.View.OnClickListener; import android.view.ViewGroup; -import android.widget.AdapterView; import android.widget.ListView; import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.compatibility.DialogFragmentWorkaround; -import org.sufficientlysecure.keychain.operations.results.OperationResult; -import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType; -import org.sufficientlysecure.keychain.operations.results.SingletonResult; -import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey.SecretKeyType; import org.sufficientlysecure.keychain.pgp.KeyRing; -import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; -import org.sufficientlysecure.keychain.provider.CachedPublicKeyRing; -import org.sufficientlysecure.keychain.provider.KeyRepository; -import org.sufficientlysecure.keychain.provider.KeyRepository.NotFoundException; -import org.sufficientlysecure.keychain.provider.KeychainContract; -import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets; import org.sufficientlysecure.keychain.service.ChangeUnlockParcel; import org.sufficientlysecure.keychain.service.SaveKeyringParcel; -import org.sufficientlysecure.keychain.service.SaveKeyringParcel.SubkeyChange; -import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; -import org.sufficientlysecure.keychain.ui.adapter.SubkeysAdapter; import org.sufficientlysecure.keychain.ui.adapter.SubkeysAddedAdapter; -import org.sufficientlysecure.keychain.ui.adapter.UserIdsAdapter; import org.sufficientlysecure.keychain.ui.adapter.UserIdsAddedAdapter; -import org.sufficientlysecure.keychain.ui.base.QueueingCryptoOperationFragment; import org.sufficientlysecure.keychain.ui.dialog.AddSubkeyDialogFragment; import org.sufficientlysecure.keychain.ui.dialog.AddUserIdDialogFragment; -import org.sufficientlysecure.keychain.ui.dialog.EditSubkeyDialogFragment; -import org.sufficientlysecure.keychain.ui.dialog.EditSubkeyExpiryDialogFragment; -import org.sufficientlysecure.keychain.ui.dialog.EditUserIdDialogFragment; import org.sufficientlysecure.keychain.ui.dialog.SetPassphraseDialogFragment; import org.sufficientlysecure.keychain.ui.util.Notify; -import org.sufficientlysecure.keychain.util.Passphrase; import timber.log.Timber; -public class EditKeyFragment extends QueueingCryptoOperationFragment - implements LoaderManager.LoaderCallbacks { +public class EditKeyFragment extends Fragment { + private static final String ARG_SAVE_KEYRING_PARCEL = "save_keyring_parcel"; - public static final String ARG_DATA_URI = "uri"; - public static final String ARG_SAVE_KEYRING_PARCEL = "save_keyring_parcel"; - - private ListView mUserIdsList; - private ListView mSubkeysList; private ListView mUserIdsAddedList; private ListView mSubkeysAddedList; private View mChangePassphrase; private View mAddUserId; private View mAddSubkey; - private static final int LOADER_ID_USER_IDS = 0; - private static final int LOADER_ID_SUBKEYS = 1; - - // cursor adapter - private UserIdsAdapter mUserIdsAdapter; - private SubkeysAdapter mSubkeysAdapter; - // array adapter private UserIdsAddedAdapter mUserIdsAddedAdapter; private SubkeysAddedAdapter mSubkeysAddedAdapter; - private Uri mDataUri; - private SaveKeyringParcel.Builder mSkpBuilder; private String mPrimaryUserId; - /** - * Creates new instance of this fragment - */ - public static EditKeyFragment newInstance(Uri dataUri) { - EditKeyFragment frag = new EditKeyFragment(); - - Bundle args = new Bundle(); - args.putParcelable(ARG_DATA_URI, dataUri); - - frag.setArguments(args); - - return frag; - } - public static EditKeyFragment newInstance(SaveKeyringParcel saveKeyringParcel) { EditKeyFragment frag = new EditKeyFragment(); @@ -128,11 +73,9 @@ public class EditKeyFragment extends QueueingCryptoOperationFragment { + // if we are working on an Uri, save directly + returnKeyringParcel(); + }, + v -> { + getActivity().setResult(Activity.RESULT_CANCELED); + getActivity().finish(); }); - Uri dataUri = getArguments().getParcelable(ARG_DATA_URI); SaveKeyringParcel saveKeyringParcel = getArguments().getParcelable(ARG_SAVE_KEYRING_PARCEL); - if (dataUri == null && saveKeyringParcel == null) { + if (saveKeyringParcel == null) { Timber.e("Either a key Uri or ARG_SAVE_KEYRING_PARCEL is required!"); getActivity().finish(); return; } initView(); - if (dataUri != null) { - loadData(dataUri); - } else { - loadSaveKeyringParcel(saveKeyringParcel); - } + loadSaveKeyringParcel(saveKeyringParcel); } private void loadSaveKeyringParcel(SaveKeyringParcel saveKeyringParcel) { @@ -192,146 +121,13 @@ public class EditKeyFragment extends QueueingCryptoOperationFragment parent, View view, int position, long id) { - editSubkey(position); - } - }); - - mUserIdsList.setOnItemClickListener(new AdapterView.OnItemClickListener() { - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - editUserId(position); - } - }); - } - - public Loader onCreateLoader(int id, Bundle args) { - - switch (id) { - case LOADER_ID_USER_IDS: { - Uri baseUri = UserPackets.buildUserIdsUri(mDataUri); - return new CursorLoader(getActivity(), baseUri, - UserIdsAdapter.USER_PACKETS_PROJECTION, null, null, null); - } - - case LOADER_ID_SUBKEYS: { - Uri baseUri = KeychainContract.Keys.buildKeysUri(mDataUri); - return new CursorLoader(getActivity(), baseUri, - SubkeysAdapter.SUBKEYS_PROJECTION, null, null, null); - } - - default: - return null; - } - } - - public void onLoadFinished(Loader loader, Cursor data) { - // Swap the new cursor in. (The framework will take care of closing the - // old cursor once we return.) - switch (loader.getId()) { - case LOADER_ID_USER_IDS: - mUserIdsAdapter.swapCursor(data); - break; - - case LOADER_ID_SUBKEYS: - mSubkeysAdapter.swapCursor(data); - break; - - } - } - - /** - * This is called when the last Cursor provided to onLoadFinished() above is about to be closed. - * We need to make sure we are no longer using it. - */ - public void onLoaderReset(Loader loader) { - switch (loader.getId()) { - case LOADER_ID_USER_IDS: - mUserIdsAdapter.swapCursor(null); - break; - case LOADER_ID_SUBKEYS: - mSubkeysAdapter.swapCursor(null); - break; - } + mChangePassphrase.setOnClickListener(v -> changePassphrase()); + mAddUserId.setOnClickListener(v -> addUserId()); + mAddSubkey.setOnClickListener(v -> addSubkey()); } private void changePassphrase() { -// Intent passIntent = new Intent(getActivity(), PassphraseWizardActivity.class); -// passIntent.setAction(PassphraseWizardActivity.CREATE_METHOD); -// startActivityForResult(passIntent, 12); - // Message is received after passphrase is cached Handler returnHandler = new Handler() { @Override public void handleMessage(Message message) { @@ -355,138 +151,6 @@ public class EditKeyFragment extends QueueingCryptoOperationFragment finish, return result to showkey and display there! - Intent intent = new Intent(); - intent.putExtra(OperationResult.EXTRA_RESULT, result); - activity.setResult(Activity.RESULT_OK, intent); - activity.finish(); - - } - } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptModeAsymmetricFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptModeAsymmetricFragment.java index 3620063e4..99caf6076 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptModeAsymmetricFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptModeAsymmetricFragment.java @@ -22,46 +22,43 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import android.arch.lifecycle.LiveData; +import android.arch.lifecycle.ViewModel; +import android.arch.lifecycle.ViewModelProviders; +import android.content.Context; import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ViewAnimator; -import com.tokenautocomplete.TokenCompleteTextView.TokenListener; +import org.sufficientlysecure.materialchips.ChipsInput.SimpleChipsListener; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKeyRing; -import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; -import org.sufficientlysecure.keychain.provider.CachedPublicKeyRing; -import org.sufficientlysecure.keychain.provider.KeyRepository; -import org.sufficientlysecure.keychain.provider.KeyRepository.NotFoundException; -import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; -import org.sufficientlysecure.keychain.ui.adapter.KeyAdapter.KeyItem; +import org.sufficientlysecure.keychain.daos.KeyRepository; +import org.sufficientlysecure.keychain.livedata.GenericLiveData; +import org.sufficientlysecure.keychain.model.SubKey.UnifiedKeyInfo; +import org.sufficientlysecure.keychain.ui.chips.EncryptRecipientChipsInput; +import org.sufficientlysecure.keychain.ui.chips.EncryptRecipientChipsInput.EncryptRecipientChip; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; import org.sufficientlysecure.keychain.ui.util.Notify; import org.sufficientlysecure.keychain.ui.util.Notify.Style; -import org.sufficientlysecure.keychain.ui.widget.EncryptKeyCompletionView; import org.sufficientlysecure.keychain.ui.widget.KeySpinner; -import org.sufficientlysecure.keychain.ui.widget.KeySpinner.OnKeyChangedListener; import org.sufficientlysecure.keychain.util.Passphrase; import timber.log.Timber; public class EncryptModeAsymmetricFragment extends EncryptModeFragment { - - KeyRepository mKeyRepository; - - private KeySpinner mSignKeySpinner; - private EncryptKeyCompletionView mEncryptKeyView; - public static final String ARG_SINGATURE_KEY_ID = "signature_key_id"; public static final String ARG_ENCRYPTION_KEY_IDS = "encryption_key_ids"; + KeyRepository keyRepository; + + private KeySpinner mSignKeySpinner; + private EncryptRecipientChipsInput mEncryptKeyView; - /** - * Creates new instance of this fragment - */ public static EncryptModeAsymmetricFragment newInstance(long signatureKey, long[] encryptionKeyIds) { EncryptModeAsymmetricFragment frag = new EncryptModeAsymmetricFragment(); @@ -73,40 +70,47 @@ public class EncryptModeAsymmetricFragment extends EncryptModeFragment { return frag; } + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + keyRepository = KeyRepository.create(requireContext()); + } + /** * Inflate the layout for this fragment */ @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.encrypt_asymmetric_fragment, container, false); - mSignKeySpinner = view.findViewById(R.id.sign); + mSignKeySpinner = view.findViewById(R.id.sign_key_spinner); mEncryptKeyView = view.findViewById(R.id.recipient_list); - mEncryptKeyView.setThreshold(1); // Start working from first character + + ViewGroup filterableListAnchor = view.findViewById(R.id.anchor_dropdown_encrypt); + mEncryptKeyView.setFilterableListLayout(filterableListAnchor); final ViewAnimator vSignatureIcon = view.findViewById(R.id.result_signature_icon); - mSignKeySpinner.setOnKeyChangedListener(new OnKeyChangedListener() { - @Override - public void onKeyChanged(long masterKeyId) { - int child = masterKeyId != Constants.key.none ? 1 : 0; - if (vSignatureIcon.getDisplayedChild() != child) { - vSignatureIcon.setDisplayedChild(child); - } + mSignKeySpinner.setOnKeyChangedListener(masterKeyId -> { + int child = masterKeyId != Constants.key.none ? 1 : 0; + if (vSignatureIcon.getDisplayedChild() != child) { + vSignatureIcon.setDisplayedChild(child); } }); + mSignKeySpinner.setShowNone(R.string.cert_none); final ViewAnimator vEncryptionIcon = view.findViewById(R.id.result_encryption_icon); - mEncryptKeyView.setTokenListener(new TokenListener() { + mEncryptKeyView.addChipsListener(new SimpleChipsListener() { @Override - public void onTokenAdded(KeyItem o) { + public void onChipAdded(EncryptRecipientChip chipInterface, int newSize) { if (vEncryptionIcon.getDisplayedChild() != 1) { vEncryptionIcon.setDisplayedChild(1); } } @Override - public void onTokenRemoved(KeyItem o) { - int child = mEncryptKeyView.getObjects().isEmpty() ? 0 : 1; + public void onChipRemoved(EncryptRecipientChip chipInterface, int newSize) { + int child = newSize == 0 ? 0 : 1; if (vEncryptionIcon.getDisplayedChild() != child) { vEncryptionIcon.setDisplayedChild(child); } @@ -119,56 +123,58 @@ public class EncryptModeAsymmetricFragment extends EncryptModeFragment { @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - mKeyRepository = KeyRepository.create(getContext()); + + EncryptModeViewModel viewModel = ViewModelProviders.of(this).get(EncryptModeViewModel.class); + viewModel.getSignKeyLiveData(requireContext()).observe(this, mSignKeySpinner::setData); + viewModel.getEncryptRecipientLiveData(requireContext()).observe(this, mEncryptKeyView::setData); // preselect keys given, from state or arguments if (savedInstanceState == null) { - Long signatureKeyId = getArguments().getLong(ARG_SINGATURE_KEY_ID); - if (signatureKeyId == Constants.key.none) { - signatureKeyId = null; - } - long[] encryptionKeyIds = getArguments().getLongArray(ARG_ENCRYPTION_KEY_IDS); - preselectKeys(signatureKeyId, encryptionKeyIds); + Bundle arguments = getArguments(); + preselectKeysFromArguments(arguments); } - } - /** - * If an Intent gives a signatureMasterKeyId and/or encryptionMasterKeyIds, preselect those! - */ - private void preselectKeys(Long signatureKeyId, long[] encryptionKeyIds) { - if (signatureKeyId != null) { - try { - CachedPublicKeyRing keyring = mKeyRepository.getCachedPublicKeyRing( - KeyRings.buildUnifiedKeyRingUri(signatureKeyId)); - if (keyring.hasAnySecret()) { - mSignKeySpinner.setPreSelectedKeyId(signatureKeyId); - } - } catch (PgpKeyNotFoundException e) { - Timber.e(e, "key not found for signing!"); - Notify.create(getActivity(), getString(R.string.error_preselect_sign_key, - KeyFormattingUtils.beautifyKeyId(signatureKeyId)), - Style.ERROR).show(); + private void preselectKeysFromArguments(Bundle arguments) { + long preselectedSignatureKeyId = arguments.getLong(ARG_SINGATURE_KEY_ID); + if (preselectedSignatureKeyId != Constants.key.none) { + mSignKeySpinner.setPreSelectedKeyId(preselectedSignatureKeyId); + } + long[] preselectedEncryptionKeyIds = arguments.getLongArray(ARG_ENCRYPTION_KEY_IDS); + if (preselectedEncryptionKeyIds != null) { + mEncryptKeyView.setPreSelectedKeyIds(preselectedEncryptionKeyIds); + } + } + + public static class EncryptModeViewModel extends ViewModel { + private LiveData> signKeyLiveData; + private LiveData> encryptRecipientLiveData; + + LiveData> getSignKeyLiveData(Context context) { + if (signKeyLiveData == null) { + signKeyLiveData = new GenericLiveData<>(context, () -> { + KeyRepository keyRepository = KeyRepository.create(context); + return keyRepository.getAllUnifiedKeyInfoWithSecret(); + }); } + return signKeyLiveData; } - if (encryptionKeyIds != null) { - for (long preselectedId : encryptionKeyIds) { - try { - CanonicalizedPublicKeyRing ring = - mKeyRepository.getCanonicalizedPublicKeyRing(preselectedId); - mEncryptKeyView.addObject(new KeyItem(ring)); - } catch (NotFoundException e) { - Timber.e(e, "key not found for encryption!"); - Notify.create(getActivity(), getString(R.string.error_preselect_encrypt_key, - KeyFormattingUtils.beautifyKeyId(preselectedId)), - Style.ERROR).show(); - } + LiveData> getEncryptRecipientLiveData(Context context) { + if (encryptRecipientLiveData == null) { + encryptRecipientLiveData = new GenericLiveData<>(context, () -> { + KeyRepository keyRepository = KeyRepository.create(context); + List keyInfos = keyRepository.getAllUnifiedKeyInfo(); + ArrayList result = new ArrayList<>(); + for (UnifiedKeyInfo keyInfo : keyInfos) { + EncryptRecipientChip chip = EncryptRecipientChipsInput.chipFromUnifiedKeyInfo(keyInfo); + result.add(chip); + } + return result; + }); } - // This is to work-around a rendering bug in TokenCompleteTextView - mEncryptKeyView.requestFocus(); + return encryptRecipientLiveData; } - } @Override @@ -184,10 +190,8 @@ public class EncryptModeAsymmetricFragment extends EncryptModeFragment { @Override public long[] getAsymmetricEncryptionKeyIds() { List keyIds = new ArrayList<>(); - for (Object object : mEncryptKeyView.getObjects()) { - if (object instanceof KeyItem) { - keyIds.add(((KeyItem) object).mKeyId); - } + for (EncryptRecipientChip chip : mEncryptKeyView.getSelectedChipList()) { + keyIds.add(chip.keyInfo.master_key_id()); } long[] keyIdsArr = new long[keyIds.size()]; @@ -201,16 +205,12 @@ public class EncryptModeAsymmetricFragment extends EncryptModeFragment { @Override public String[] getAsymmetricEncryptionUserIds() { - List userIds = new ArrayList<>(); - for (Object object : mEncryptKeyView.getObjects()) { - if (object instanceof KeyItem) { - userIds.add(((KeyItem) object).mUserIdFull); - } + for (EncryptRecipientChip chip : mEncryptKeyView.getSelectedChipList()) { + userIds.add(chip.keyInfo.user_id()); } return userIds.toArray(new String[userIds.size()]); - } @Override diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java index acfb1759b..72b97b352 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java @@ -19,22 +19,21 @@ package org.sufficientlysecure.keychain.ui; import java.io.IOException; -import java.util.ArrayList; +import java.util.List; import android.animation.ObjectAnimator; import android.app.Activity; +import android.arch.lifecycle.LiveData; +import android.arch.lifecycle.ViewModelProviders; import android.content.Intent; -import android.database.Cursor; -import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; +import android.support.annotation.WorkerThread; import android.support.v4.app.FragmentActivity; -import android.support.v4.app.LoaderManager; -import android.support.v4.content.CursorLoader; -import android.support.v4.content.Loader; import android.support.v4.view.MenuItemCompat; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; import android.support.v7.widget.SearchView; -import android.text.TextUtils; import android.view.ActionMode; import android.view.LayoutInflater; import android.view.Menu; @@ -45,65 +44,66 @@ import android.view.ViewGroup; import android.widget.Button; import android.widget.ViewAnimator; -import com.futuremind.recyclerviewfastscroll.FastScroller; import com.getbase.floatingactionbutton.FloatingActionButton; import com.getbase.floatingactionbutton.FloatingActionsMenu; -import com.tonicartos.superslim.LayoutManager; +import eu.davidea.fastscroller.FastScroller; +import eu.davidea.flexibleadapter.FlexibleAdapter; +import eu.davidea.flexibleadapter.FlexibleAdapter.OnItemClickListener; +import eu.davidea.flexibleadapter.FlexibleAdapter.OnItemLongClickListener; +import eu.davidea.flexibleadapter.SelectableAdapter.Mode; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.compatibility.ClipboardReflection; -import org.sufficientlysecure.keychain.keyimport.HkpKeyserverAddress; -import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing; +import org.sufficientlysecure.keychain.keysync.KeyserverSyncManager; +import org.sufficientlysecure.keychain.model.SubKey.UnifiedKeyInfo; +import org.sufficientlysecure.keychain.operations.KeySyncParcel; import org.sufficientlysecure.keychain.operations.results.BenchmarkResult; import org.sufficientlysecure.keychain.operations.results.ImportKeyResult; import org.sufficientlysecure.keychain.operations.results.OperationResult; import org.sufficientlysecure.keychain.pgp.PgpHelper; -import org.sufficientlysecure.keychain.provider.KeyRepository; -import org.sufficientlysecure.keychain.provider.KeychainContract; +import org.sufficientlysecure.keychain.daos.KeyRepository; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; -import org.sufficientlysecure.keychain.provider.KeychainDatabase; +import org.sufficientlysecure.keychain.KeychainDatabase; import org.sufficientlysecure.keychain.service.BenchmarkInputParcel; -import org.sufficientlysecure.keychain.service.ImportKeyringParcel; -import org.sufficientlysecure.keychain.ui.adapter.KeySectionedListAdapter; +import org.sufficientlysecure.keychain.ui.adapter.FlexibleKeyDetailsItem; +import org.sufficientlysecure.keychain.ui.adapter.FlexibleKeyDummyItem; +import org.sufficientlysecure.keychain.ui.adapter.FlexibleKeyHeader; +import org.sufficientlysecure.keychain.ui.adapter.FlexibleKeyItem; +import org.sufficientlysecure.keychain.ui.adapter.FlexibleKeyItem.FlexibleSectionableKeyItem; +import org.sufficientlysecure.keychain.ui.adapter.FlexibleKeyItemFactory; import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper; import org.sufficientlysecure.keychain.ui.base.RecyclerFragment; +import org.sufficientlysecure.keychain.ui.keyview.GenericViewModel; import org.sufficientlysecure.keychain.ui.keyview.ViewKeyActivity; +import org.sufficientlysecure.keychain.ui.util.KeyInfoFormatter; import org.sufficientlysecure.keychain.ui.util.Notify; -import org.sufficientlysecure.keychain.ui.util.Notify.ActionListener; import org.sufficientlysecure.keychain.ui.util.Notify.Style; import org.sufficientlysecure.keychain.util.FabContainer; import org.sufficientlysecure.keychain.util.Preferences; import timber.log.Timber; -public class KeyListFragment extends RecyclerFragment - implements SearchView.OnQueryTextListener, - LoaderManager.LoaderCallbacks, FabContainer { +public class KeyListFragment extends RecyclerFragment> + implements SearchView.OnQueryTextListener, OnItemClickListener, OnItemLongClickListener, FabContainer { static final int REQUEST_ACTION = 1; private static final int REQUEST_DELETE = 2; private static final int REQUEST_VIEW_KEY = 3; - // saves the mode object for multiselect, needed for reset at some point private ActionMode mActionMode = null; private Button vSearchButton; private ViewAnimator vSearchContainer; - private String mQuery; private FloatingActionsMenu mFab; - // for CryptoOperationHelper import - private ArrayList mKeyList; - private HkpKeyserverAddress mKeyserver; - private CryptoOperationHelper mImportOpHelper; + private KeyRepository keyRepository; + private FlexibleKeyItemFactory flexibleKeyItemFactory; - // Callbacks related to listview and menu events - private final ActionMode.Callback mActionCallback - = new ActionMode.Callback() { + private final ActionMode.Callback mActionCallback = new ActionMode.Callback() { @Override public boolean onCreateActionMode(ActionMode mode, Menu menu) { - getActivity().getMenuInflater().inflate(R.menu.key_list_multi, menu); + mode.getMenuInflater().inflate(R.menu.key_list_multi, menu); return true; } @@ -116,28 +116,17 @@ public class KeyListFragment extends RecyclerFragment public boolean onActionItemClicked(ActionMode mode, MenuItem item) { switch (item.getItemId()) { case R.id.menu_key_list_multi_encrypt: { - long[] keyIds = getAdapter().getSelectedMasterKeyIds(); - Intent intent = new Intent(getActivity(), EncryptFilesActivity.class); - intent.setAction(EncryptFilesActivity.ACTION_ENCRYPT_DATA); - intent.putExtra(EncryptFilesActivity.EXTRA_ENCRYPTION_KEY_IDS, keyIds); - - startActivityForResult(intent, REQUEST_ACTION); + long[] keyIds = getSelectedMasterKeyIds(); + multiSelectEncrypt(keyIds); mode.finish(); break; } case R.id.menu_key_list_multi_delete: { - long[] keyIds = getAdapter().getSelectedMasterKeyIds(); - boolean hasSecret = getAdapter().isAnySecretKeySelected(); - Intent intent = new Intent(getActivity(), DeleteKeyDialogActivity.class); - intent.putExtra(DeleteKeyDialogActivity.EXTRA_DELETE_MASTER_KEY_IDS, keyIds); - intent.putExtra(DeleteKeyDialogActivity.EXTRA_HAS_SECRET, hasSecret); - if (hasSecret) { - intent.putExtra(DeleteKeyDialogActivity.EXTRA_KEYSERVER, - Preferences.getPreferences(getActivity()).getPreferredKeyserver()); - } - - startActivityForResult(intent, REQUEST_DELETE); + long[] keyIds = getSelectedMasterKeyIds(); + boolean hasSecret = isAnySecretKeySelected(); + multiSelectDelete(keyIds, hasSecret); + mode.finish(); break; } } @@ -148,54 +137,62 @@ public class KeyListFragment extends RecyclerFragment public void onDestroyActionMode(ActionMode mode) { mActionMode = null; if (getAdapter() != null) { - getAdapter().finishSelection(); + getAdapter().clearSelection(); } } }; + private FastScroller fastScroller; + private KeyInfoFormatter keyInfoFormatter; - private final KeySectionedListAdapter.KeyListListener mKeyListener - = new KeySectionedListAdapter.KeyListListener() { - @Override - public void onKeyDummyItemClicked() { - createKey(); + private void multiSelectDelete(long[] keyIds, boolean hasSecret) { + Intent intent = new Intent(getActivity(), DeleteKeyDialogActivity.class); + intent.putExtra(DeleteKeyDialogActivity.EXTRA_DELETE_MASTER_KEY_IDS, keyIds); + intent.putExtra(DeleteKeyDialogActivity.EXTRA_HAS_SECRET, hasSecret); + if (hasSecret) { + intent.putExtra(DeleteKeyDialogActivity.EXTRA_KEYSERVER, + Preferences.getPreferences(getActivity()).getPreferredKeyserver()); } + startActivityForResult(intent, REQUEST_DELETE); + } - @Override - public void onKeyItemClicked(long masterKeyId) { - Intent viewIntent = new Intent(getActivity(), ViewKeyActivity.class); - viewIntent.setData(KeyRings.buildGenericKeyRingUri(masterKeyId)); - startActivityForResult(viewIntent, REQUEST_VIEW_KEY); - } + private void multiSelectEncrypt(long[] keyIds) { + Intent intent = new Intent(getActivity(), EncryptFilesActivity.class); + intent.setAction(EncryptFilesActivity.ACTION_ENCRYPT_DATA); + intent.putExtra(EncryptFilesActivity.EXTRA_ENCRYPTION_KEY_IDS, keyIds); - @Override - public void onSlingerButtonClicked(long masterKeyId) { - Intent safeSlingerIntent = new Intent(getActivity(), SafeSlingerActivity.class); - safeSlingerIntent.putExtra(SafeSlingerActivity.EXTRA_MASTER_KEY_ID, masterKeyId); - startActivityForResult(safeSlingerIntent, REQUEST_ACTION); - } + startActivityForResult(intent, REQUEST_ACTION); + } - @Override - public void onSelectionStateChanged(int selectedCount) { - if (selectedCount < 1) { - if (mActionMode != null) { - mActionMode.finish(); - } - } else { - if (mActionMode == null) { - mActionMode = getActivity().startActionMode(mActionCallback); - } - - String keysSelected = getResources().getQuantityString( - R.plurals.key_list_selected_keys, selectedCount, selectedCount); - mActionMode.setTitle(keysSelected); + private long[] getSelectedMasterKeyIds() { + FlexibleAdapter adapter = getAdapter(); + List selectedPositions = adapter.getSelectedPositions(); + long[] keyIds = new long[selectedPositions.size()]; + for (int i = 0; i < selectedPositions.size(); i++) { + FlexibleKeyDetailsItem selectedItem = adapter.getItem(selectedPositions.get(i), FlexibleKeyDetailsItem.class); + if (selectedItem != null) { + keyIds[i] = selectedItem.keyInfo.master_key_id(); } } - }; + return keyIds; + } + private boolean isAnySecretKeySelected() { + FlexibleAdapter adapter = getAdapter(); + for (int position : adapter.getSelectedPositions()) { + FlexibleKeyDetailsItem item = adapter.getItem(position, FlexibleKeyDetailsItem.class); + if (item != null && item.keyInfo.has_any_secret()) { + return true; + } + } + return false; + } + + public void startSafeSlingerForKey(long masterKeyId) { + Intent safeSlingerIntent = new Intent(getActivity(), SafeSlingerActivity.class); + safeSlingerIntent.putExtra(SafeSlingerActivity.EXTRA_MASTER_KEY_ID, masterKeyId); + startActivityForResult(safeSlingerIntent, REQUEST_ACTION); + } - /** - * Load custom layout - */ @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.key_list_fragment, container, false); @@ -219,6 +216,11 @@ public class KeyListFragment extends RecyclerFragment importFile(); }); + fastScroller = view.findViewById(R.id.fast_scroller); + + vSearchContainer = view.findViewById(R.id.search_container); + vSearchButton = view.findViewById(R.id.search_button); + vSearchButton.setOnClickListener(v -> startSearchForQuery()); return view; } @@ -231,32 +233,59 @@ public class KeyListFragment extends RecyclerFragment super.onActivityCreated(savedInstanceState); // show app name instead of "keys" from nav drawer - final FragmentActivity activity = getActivity(); + FragmentActivity activity = getActivity(); + if (activity == null) { + throw new NullPointerException("Activity must be bound!"); + } activity.setTitle(R.string.app_name); // We have a menu item to show in action bar. setHasOptionsMenu(true); - // Start out with a progress indicator. - hideList(false); + setLayoutManager(new LinearLayoutManager(activity)); - // click on search button (in empty view) starts query for search string - vSearchContainer = activity.findViewById(R.id.search_container); - vSearchButton = activity.findViewById(R.id.search_button); - vSearchButton.setOnClickListener(v -> startSearchForQuery()); + keyRepository = KeyRepository.create(requireContext()); + flexibleKeyItemFactory = new FlexibleKeyItemFactory(requireContext().getResources()); - KeySectionedListAdapter adapter = new KeySectionedListAdapter(getContext(), null); - adapter.setKeyListener(mKeyListener); + GenericViewModel viewModel = ViewModelProviders.of(this).get(GenericViewModel.class); + LiveData> liveData = viewModel.getGenericLiveData(requireContext(), this::loadFlexibleKeyItems); + liveData.observe(this, this::onLoadKeyItems); + } - setAdapter(adapter); - setLayoutManager(new LayoutManager(getActivity())); + @WorkerThread + private List loadFlexibleKeyItems() { + List unifiedKeyInfo = keyRepository.getAllUnifiedKeyInfo(); + return flexibleKeyItemFactory.mapUnifiedKeyInfoToFlexibleKeyItems(unifiedKeyInfo); + } - FastScroller fastScroller = getActivity().findViewById(R.id.fastscroll); - fastScroller.setRecyclerView(getRecyclerView()); + private void onLoadKeyItems(List flexibleKeyItems) { + FlexibleAdapter adapter = getAdapter(); + if (adapter == null) { + adapter = new FlexibleAdapter<>(flexibleKeyItems, this, true); + adapter.setDisplayHeadersAtStartUp(true); + adapter.setStickyHeaders(true); + adapter.setMode(Mode.MULTI); + setAdapter(adapter); + adapter.setFastScroller(fastScroller); + fastScroller.setBubbleTextCreator(this::getBubbleText); + } else { + adapter.updateDataSet(flexibleKeyItems, true); + } + } - // Prepare the loader. Either re-connect with an existing one, - // or start a new one. - getLoaderManager().initLoader(0, null, this); + private String getBubbleText(int position) { + FlexibleKeyItem item = getAdapter().getItem(position); + if (item == null) { + return ""; + } + if (item instanceof FlexibleSectionableKeyItem) { + FlexibleKeyHeader header = ((FlexibleSectionableKeyItem) item).getHeader(); + return header.getSectionTitle(); + } + if (item instanceof FlexibleKeyHeader) { + return ((FlexibleKeyHeader) item).getSectionTitle(); + } + return ""; } @Override @@ -314,53 +343,11 @@ public class KeyListFragment extends RecyclerFragment } Intent searchIntent = new Intent(activity, ImportKeysActivity.class); - searchIntent.putExtra(ImportKeysActivity.EXTRA_QUERY, mQuery); + searchIntent.putExtra(ImportKeysActivity.EXTRA_QUERY, getAdapter().getFilter(String.class)); searchIntent.setAction(ImportKeysActivity.ACTION_IMPORT_KEY_FROM_KEYSERVER); startActivity(searchIntent); } - @Override - public Loader onCreateLoader(int id, Bundle args) { - // This is called when a new Loader needs to be created. This - // sample only has one Loader, so we don't care about the ID. - Uri uri; - if (!TextUtils.isEmpty(mQuery)) { - uri = KeyRings.buildUnifiedKeyRingsFindByUserIdUri(mQuery); - } else { - uri = KeyRings.buildUnifiedKeyRingsUri(); - } - - // Now create and return a CursorLoader that will take care of - // creating a Cursor for the data being displayed. - return new CursorLoader(getActivity(), uri, - KeySectionedListAdapter.KeyListCursor.PROJECTION, null, null, - KeySectionedListAdapter.KeyListCursor.ORDER); - } - - @Override - public void onLoadFinished(Loader loader, Cursor data) { - // Swap the new cursor in. (The framework will take care of closing the - // old cursor once we return.) - getAdapter().setSearchQuery(mQuery); - getAdapter().swapCursor(KeySectionedListAdapter.KeyListCursor.wrap(data)); - - // end action mode, if any - if (mActionMode != null) { - mActionMode.finish(); - } - - // The list should now be shown. - showList(isResumed()); - } - - @Override - public void onLoaderReset(Loader loader) { - // This is called when the last Cursor provided to onLoadFinished() - // above is about to be closed. We need to make sure we are no - // longer using it. - getAdapter().swapCursor(null); - } - @Override public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) { inflater.inflate(R.menu.key_list, menu); @@ -370,6 +357,7 @@ public class KeyListFragment extends RecyclerFragment menu.findItem(R.id.menu_key_list_debug_read).setVisible(true); menu.findItem(R.id.menu_key_list_debug_write).setVisible(true); menu.findItem(R.id.menu_key_list_debug_first_time).setVisible(true); + menu.findItem(R.id.menu_key_list_debug_bgsync).setVisible(true); } // Get the searchview @@ -383,19 +371,13 @@ public class KeyListFragment extends RecyclerFragment MenuItemCompat.setOnActionExpandListener(searchItem, new MenuItemCompat.OnActionExpandListener() { @Override public boolean onMenuItemActionExpand(MenuItem item) { - - // disable swipe-to-refresh - // mSwipeRefreshLayout.setIsLocked(true); return true; } @Override public boolean onMenuItemActionCollapse(MenuItem item) { - mQuery = null; - getLoaderManager().restartLoader(0, null, KeyListFragment.this); - - // enable swipe-to-refresh - // mSwipeRefreshLayout.setIsLocked(false); + getAdapter().setFilter(null); + getAdapter().filterItems(); return true; } }); @@ -403,6 +385,64 @@ public class KeyListFragment extends RecyclerFragment super.onCreateOptionsMenu(menu, inflater); } + @Override + public boolean onItemClick(View view, int position) { + FlexibleKeyItem item = getAdapter().getItem(position); + if (item == null) { + return false; + } + + if (item instanceof FlexibleKeyDummyItem) { + createKey(); + return false; + } + + if (!(item instanceof FlexibleKeyDetailsItem)) { + return false; + } + + if (mActionMode != null && position != RecyclerView.NO_POSITION) { + toggleSelection(position); + return true; + } + + long masterKeyId = ((FlexibleKeyDetailsItem) item).keyInfo.master_key_id(); + Intent viewIntent = ViewKeyActivity.getViewKeyActivityIntent(requireActivity(), masterKeyId); + startActivityForResult(viewIntent, REQUEST_VIEW_KEY); + return false; + } + + @Override + public void onItemLongClick(int position) { + if (getAdapter().getItem(position) instanceof FlexibleKeyDetailsItem) { + if (mActionMode == null) { + FragmentActivity activity = getActivity(); + if (activity != null) { + mActionMode = activity.startActionMode(mActionCallback); + } + } + toggleSelection(position); + } + } + + private void toggleSelection(int position) { + getAdapter().toggleSelection(position); + + int count = getAdapter().getSelectedItemCount(); + + if (count == 0) { + mActionMode.finish(); + } else { + setContextTitle(count); + } + } + + private void setContextTitle(int selectedCount) { + String keysSelected = getResources().getQuantityString( + R.plurals.key_list_selected_keys, selectedCount, selectedCount); + mActionMode.setTitle(keysSelected); + } + @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { @@ -418,7 +458,7 @@ public class KeyListFragment extends RecyclerFragment try { KeychainDatabase.debugBackup(getActivity(), true); Notify.create(getActivity(), "Restored debug_backup.db", Notify.Style.OK).show(); - getActivity().getContentResolver().notifyChange(KeychainContract.KeyRings.CONTENT_URI, null); + getActivity().getContentResolver().notifyChange(KeyRings.CONTENT_URI, null); } catch (IOException e) { Timber.e(e, "IO Error"); Notify.create(getActivity(), "IO Error " + e.getMessage(), Notify.Style.ERROR).show(); @@ -444,6 +484,10 @@ public class KeyListFragment extends RecyclerFragment getActivity().finish(); return true; } + case R.id.menu_key_list_debug_bgsync: { + KeyserverSyncManager.debugRunSyncNow(); + return true; + } case R.id.menu_key_list_debug_bench: { benchmark(); return true; @@ -460,20 +504,12 @@ public class KeyListFragment extends RecyclerFragment } @Override - public boolean onQueryTextChange(String s) { - Timber.d("onQueryTextChange s: %s", s); - // Called when the action bar search text has changed. Update the - // search filter, and restart the loader to do a new query with this - // filter. - // If the nav drawer is opened, onQueryTextChange("") is executed. - // This hack prevents restarting the loader. - if (!s.equals(mQuery)) { - mQuery = s; - getLoaderManager().restartLoader(0, null, this); - } + public boolean onQueryTextChange(String searchText) { + getAdapter().setFilter(searchText); + getAdapter().filterItems(300); - if (s.length() > 2) { - vSearchButton.setText(getString(R.string.btn_search_for_query, mQuery)); + if (searchText.length() > 2) { + vSearchButton.setText(getString(R.string.btn_search_for_query, searchText)); vSearchContainer.setDisplayedChild(1); vSearchContainer.setVisibility(View.VISIBLE); } else { @@ -508,45 +544,12 @@ public class KeyListFragment extends RecyclerFragment } private void updateAllKeys() { - Activity activity = getActivity(); - if (activity == null) { - return; - } - - KeyRepository keyRepository = - KeyRepository.create(getContext()); - Cursor cursor = keyRepository.getContentResolver().query( - KeyRings.buildUnifiedKeyRingsUri(), new String[]{ - KeyRings.FINGERPRINT - }, null, null, null - ); - - if (cursor == null) { - Notify.create(activity, R.string.error_loading_keys, Notify.Style.ERROR).show(); - return; - } - - ArrayList keyList = new ArrayList<>(); - try { - while (cursor.moveToNext()) { - byte[] fingerprint = cursor.getBlob(0); //fingerprint column is 0 - ParcelableKeyRing keyEntry = ParcelableKeyRing.createFromReference(fingerprint, null, null, null); - keyList.add(keyEntry); - } - mKeyList = keyList; - } finally { - cursor.close(); - } - - // search config - mKeyserver = Preferences.getPreferences(getActivity()).getPreferredKeyserver(); - - CryptoOperationHelper.Callback callback - = new CryptoOperationHelper.Callback() { + CryptoOperationHelper.Callback callback + = new CryptoOperationHelper.Callback() { @Override - public ImportKeyringParcel createOperationInput() { - return ImportKeyringParcel.createImportKeyringParcel(mKeyList, mKeyserver); + public KeySyncParcel createOperationInput() { + return KeySyncParcel.createRefreshAll(); } @Override @@ -569,9 +572,9 @@ public class KeyListFragment extends RecyclerFragment } }; - mImportOpHelper = new CryptoOperationHelper<>(1, this, callback, R.string.progress_updating); - mImportOpHelper.setProgressCancellable(true); - mImportOpHelper.cryptoOperation(); + CryptoOperationHelper opHelper = new CryptoOperationHelper<>(3, this, callback, R.string.progress_importing); + opHelper.setProgressCancellable(true); + opHelper.cryptoOperation(); } private void benchmark() { @@ -609,10 +612,6 @@ public class KeyListFragment extends RecyclerFragment @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { - if (mImportOpHelper != null) { - mImportOpHelper.handleActivityResult(requestCode, resultCode, data); - } - switch (requestCode) { case REQUEST_DELETE: { if (mActionMode != null) { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/LogDisplayFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/LogDisplayFragment.java index 9173861e4..da0ebf658 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/LogDisplayFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/LogDisplayFragment.java @@ -17,28 +17,28 @@ package org.sufficientlysecure.keychain.ui; + +import java.io.IOException; +import java.io.OutputStream; + import android.app.Activity; import android.content.Intent; import android.net.Uri; import android.os.Bundle; +import android.support.v7.widget.LinearLayoutManager; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; -import com.tonicartos.superslim.LayoutManager; - import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.operations.results.OperationResult; import org.sufficientlysecure.keychain.operations.results.OperationResult.SubLogEntryParcel; import org.sufficientlysecure.keychain.provider.TemporaryFileProvider; import org.sufficientlysecure.keychain.ui.adapter.NestedLogAdapter; +import org.sufficientlysecure.keychain.ui.base.RecyclerFragment; import org.sufficientlysecure.keychain.ui.dialog.ShareLogDialogFragment; import org.sufficientlysecure.keychain.ui.util.Notify; import org.sufficientlysecure.keychain.ui.util.Notify.Style; -import org.sufficientlysecure.keychain.ui.base.RecyclerFragment; - -import java.io.IOException; -import java.io.OutputStream; public class LogDisplayFragment extends RecyclerFragment @@ -79,7 +79,7 @@ public class LogDisplayFragment extends RecyclerFragment adapter.setListener(this); setAdapter(adapter); - setLayoutManager(new LayoutManager(getContext())); + setLayoutManager(new LinearLayoutManager(getContext())); } @Override diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/MainActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/MainActivity.java index fa901d2aa..9471c644d 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/MainActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/MainActivity.java @@ -46,7 +46,7 @@ import org.sufficientlysecure.keychain.ui.transfer.view.TransferNotAvailableFrag import org.sufficientlysecure.keychain.util.FabContainer; import org.sufficientlysecure.keychain.util.Preferences; -public class MainActivity extends BaseSecurityTokenActivity implements FabContainer, OnBackStackChangedListener { +public class MainActivity extends BaseSecurityTokenActivity implements FabContainer { static final int ID_KEYS = 1; static final int ID_ENCRYPT_DECRYPT = 2; @@ -145,8 +145,6 @@ public class MainActivity extends BaseSecurityTokenActivity implements FabContai return; } - getSupportFragmentManager().addOnBackStackChangedListener(this); - // all further initialization steps are saved as instance state if (savedInstanceState != null) { return; @@ -159,7 +157,6 @@ public class MainActivity extends BaseSecurityTokenActivity implements FabContai result.createNotify(this).show(); } - // always initialize keys fragment to the bottom of the backstack onKeysSelected(); if (data != null && data.hasExtra(EXTRA_INIT_FRAG)) { @@ -200,46 +197,41 @@ public class MainActivity extends BaseSecurityTokenActivity implements FabContai } } - private void setFragment(Fragment fragment, boolean addToBackStack) { + private void setFragment(Fragment frag) { FragmentManager fragmentManager = getSupportFragmentManager(); - fragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE); FragmentTransaction ft = fragmentManager.beginTransaction(); - ft.replace(R.id.main_fragment_container, fragment); - if (addToBackStack) { - ft.addToBackStack(null); - } + ft.replace(R.id.main_fragment_container, frag); ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); ft.commit(); - } public void onKeysSelected() { mToolbar.setTitle(R.string.app_name); mDrawer.setSelection(ID_KEYS, false); Fragment frag = new KeyListFragment(); - setFragment(frag, false); + setFragment(frag); } private void onEnDecryptSelected() { mToolbar.setTitle(R.string.nav_encrypt_decrypt); mDrawer.setSelection(ID_ENCRYPT_DECRYPT, false); Fragment frag = new EncryptDecryptFragment(); - setFragment(frag, true); + setFragment(frag); } private void onAppsSelected() { mToolbar.setTitle(R.string.nav_apps); mDrawer.setSelection(ID_APPS, false); Fragment frag = new AppsListFragment(); - setFragment(frag, true); + setFragment(frag); } private void onBackupSelected() { mToolbar.setTitle(R.string.nav_backup); mDrawer.setSelection(ID_BACKUP, false); Fragment frag = new BackupRestoreFragment(); - setFragment(frag, true); + setFragment(frag); } private void onTransferSelected() { @@ -247,10 +239,10 @@ public class MainActivity extends BaseSecurityTokenActivity implements FabContai mDrawer.setSelection(ID_TRANSFER, false); if (Build.VERSION.SDK_INT < VERSION_CODES.LOLLIPOP) { Fragment frag = new TransferNotAvailableFragment(); - setFragment(frag, true); + setFragment(frag); } else { Fragment frag = new TransferFragment(); - setFragment(frag, true); + setFragment(frag); } } @@ -267,7 +259,12 @@ public class MainActivity extends BaseSecurityTokenActivity implements FabContai if (mDrawer != null && mDrawer.isDrawerOpen()) { mDrawer.closeDrawer(); } else { - super.onBackPressed(); + FragmentManager fragmentManager = getSupportFragmentManager(); + if (fragmentManager.findFragmentById(R.id.main_fragment_container) instanceof KeyListFragment) { + super.onBackPressed(); + } else { + onKeysSelected(); + } } } @@ -289,35 +286,4 @@ public class MainActivity extends BaseSecurityTokenActivity implements FabContai } } - - @Override - public void onBackStackChanged() { - FragmentManager fragmentManager = getSupportFragmentManager(); - if (fragmentManager == null) { - return; - } - Fragment frag = fragmentManager.findFragmentById(R.id.main_fragment_container); - if (frag == null) { - return; - } - - // make sure the selected icon is the one shown at this point - if (frag instanceof KeyListFragment) { - mToolbar.setTitle(R.string.app_name); - mDrawer.setSelection(mDrawer.getPosition(ID_KEYS), false); - } else if (frag instanceof EncryptDecryptFragment) { - mToolbar.setTitle(R.string.nav_encrypt_decrypt); - mDrawer.setSelection(mDrawer.getPosition(ID_ENCRYPT_DECRYPT), false); - } else if (frag instanceof AppsListFragment) { - mToolbar.setTitle(R.string.nav_apps); - mDrawer.setSelection(mDrawer.getPosition(ID_APPS), false); - } else if (frag instanceof BackupRestoreFragment) { - mToolbar.setTitle(R.string.nav_backup); - mDrawer.setSelection(mDrawer.getPosition(ID_BACKUP), false); - } else if (frag instanceof TransferFragment) { - mToolbar.setTitle(R.string.nav_transfer); - mDrawer.setSelection(mDrawer.getPosition(ID_TRANSFER), false); - } - } - } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/MultiUserIdsFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/MultiUserIdsFragment.java index dc4a4de3f..80ffbdf06 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/MultiUserIdsFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/MultiUserIdsFragment.java @@ -17,67 +17,47 @@ package org.sufficientlysecure.keychain.ui; -import android.database.Cursor; + +import java.util.ArrayList; +import java.util.List; + +import android.arch.lifecycle.LiveData; import android.database.MatrixCursor; -import android.net.Uri; import android.os.Bundle; import android.os.Parcel; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.app.Fragment; -import android.support.v4.app.LoaderManager; -import android.support.v4.content.CursorLoader; -import android.support.v4.content.Loader; +import android.support.v4.app.FragmentActivity; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ListView; import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.provider.KeychainContract; -import org.sufficientlysecure.keychain.provider.KeychainDatabase; +import org.sufficientlysecure.keychain.livedata.GenericLiveData; +import org.sufficientlysecure.keychain.model.UserPacket.UserId; +import org.sufficientlysecure.keychain.daos.KeyRepository; import org.sufficientlysecure.keychain.service.CertifyActionsParcel; import org.sufficientlysecure.keychain.ui.adapter.MultiUserIdsAdapter; import timber.log.Timber; -import java.util.ArrayList; -public class MultiUserIdsFragment extends Fragment implements LoaderManager.LoaderCallbacks{ +public class MultiUserIdsFragment extends Fragment { public static final String ARG_CHECK_STATES = "check_states"; public static final String EXTRA_KEY_IDS = "extra_key_ids"; private boolean checkboxVisibility = true; - ListView mUserIds; - private MultiUserIdsAdapter mUserIdsAdapter; + ListView userIds; + private MultiUserIdsAdapter userIdsAdapter; - private long[] mPubMasterKeyIds; - - public static final String[] USER_IDS_PROJECTION = new String[]{ - KeychainContract.UserPackets._ID, - KeychainContract.UserPackets.MASTER_KEY_ID, - KeychainContract.UserPackets.USER_ID, - KeychainContract.UserPackets.IS_PRIMARY, - KeychainContract.UserPackets.IS_REVOKED, - KeychainContract.UserPackets.NAME, - KeychainContract.UserPackets.EMAIL, - KeychainContract.UserPackets.COMMENT, - }; - private static final int INDEX_MASTER_KEY_ID = 1; - private static final int INDEX_USER_ID = 2; - @SuppressWarnings("unused") - private static final int INDEX_IS_PRIMARY = 3; - @SuppressWarnings("unused") - private static final int INDEX_IS_REVOKED = 4; - private static final int INDEX_NAME = 5; - private static final int INDEX_EMAIL = 6; - private static final int INDEX_COMMENT = 7; + private long[] pubMasterKeyIds; @Nullable @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.multi_user_ids_fragment, null); - - mUserIds = view.findViewById(R.id.view_key_user_ids); - + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.multi_user_ids_fragment, container, false); + userIds = view.findViewById(R.id.view_key_user_ids); return view; } @@ -85,10 +65,11 @@ public class MultiUserIdsFragment extends Fragment implements LoaderManager.Load public void onActivityCreated(@Nullable Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - mPubMasterKeyIds = getActivity().getIntent().getLongArrayExtra(EXTRA_KEY_IDS); - if (mPubMasterKeyIds == null) { + FragmentActivity activity = requireActivity(); + pubMasterKeyIds = activity.getIntent().getLongArrayExtra(EXTRA_KEY_IDS); + if (pubMasterKeyIds == null) { Timber.e("List of key ids to certify missing!"); - getActivity().finish(); + activity.finish(); return; } @@ -97,18 +78,22 @@ public class MultiUserIdsFragment extends Fragment implements LoaderManager.Load checkedStates = (ArrayList) savedInstanceState.getSerializable(ARG_CHECK_STATES); } - mUserIdsAdapter = new MultiUserIdsAdapter(getActivity(), null, 0, checkedStates, checkboxVisibility); - mUserIds.setAdapter(mUserIdsAdapter); - mUserIds.setDividerHeight(0); + userIdsAdapter = new MultiUserIdsAdapter(activity, null, 0, checkedStates, checkboxVisibility); + userIds.setAdapter(userIdsAdapter); + userIds.setDividerHeight(0); + + KeyRepository keyRepository = KeyRepository.create(activity); + LiveData> userIdLiveData = + new GenericLiveData<>(getContext(), () -> keyRepository.getUserIds(pubMasterKeyIds)); + userIdLiveData.observe(this, this::onUserIdsLoaded); - getLoaderManager().initLoader(0, null, this); } @Override - public void onSaveInstanceState(Bundle outState) { + public void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); - ArrayList states = mUserIdsAdapter.getCheckStates(); + ArrayList states = userIdsAdapter.getCheckStates(); // no proper parceling method available :( outState.putSerializable(ARG_CHECK_STATES, states); } @@ -118,40 +103,10 @@ public class MultiUserIdsFragment extends Fragment implements LoaderManager.Load throw new AssertionError("Item selection not allowed"); } - return mUserIdsAdapter.getSelectedCertifyActions(); + return userIdsAdapter.getSelectedCertifyActions(); } - @Override - public Loader onCreateLoader(int id, Bundle args) { - Uri uri = KeychainContract.UserPackets.buildUserIdsUri(); - - String selection, ids[]; - { - // generate placeholders and string selection args - ids = new String[mPubMasterKeyIds.length]; - StringBuilder placeholders = new StringBuilder("?"); - for (int i = 0; i < mPubMasterKeyIds.length; i++) { - ids[i] = Long.toString(mPubMasterKeyIds[i]); - if (i != 0) { - placeholders.append(",?"); - } - } - // put together selection string - selection = KeychainContract.UserPackets.IS_REVOKED + " = 0" + " AND " - + KeychainDatabase.Tables.USER_PACKETS + "." + KeychainContract.UserPackets.MASTER_KEY_ID - + " IN (" + placeholders + ")"; - } - - return new CursorLoader(getActivity(), uri, - USER_IDS_PROJECTION, selection, ids, - KeychainDatabase.Tables.USER_PACKETS + "." + KeychainContract.UserPackets.MASTER_KEY_ID + " ASC" - + ", " + KeychainDatabase.Tables.USER_PACKETS + "." + KeychainContract.UserPackets.USER_ID + " ASC" - ); - } - - @Override - public void onLoadFinished(Loader loader, Cursor data) { - + private void onUserIdsLoaded(List userIds) { MatrixCursor matrix = new MatrixCursor(new String[]{ "_id", "user_data", "grouped" }) { @@ -160,28 +115,25 @@ public class MultiUserIdsFragment extends Fragment implements LoaderManager.Load return super.getBlob(column); } }; - data.moveToFirst(); long lastMasterKeyId = 0; String lastName = ""; ArrayList uids = new ArrayList<>(); boolean header = true; + boolean isFirst = true; // Iterate over all rows - while (!data.isAfterLast()) { - long masterKeyId = data.getLong(INDEX_MASTER_KEY_ID); - String userId = data.getString(INDEX_USER_ID); - String name = data.getString(INDEX_NAME); - + for (UserId userId : userIds) { // Two cases: - boolean grouped = masterKeyId == lastMasterKeyId; - boolean subGrouped = data.isFirst() || grouped && lastName != null && lastName.equals(name); + boolean grouped = userId.master_key_id() == lastMasterKeyId; + boolean subGrouped = isFirst || grouped && lastName != null && lastName.equals(userId.name()); + isFirst = false; // Remember for next loop - lastName = name; + lastName = userId.name(); - Timber.d(Long.toString(masterKeyId, 16) + (grouped ? "grouped" : "not grouped")); + Timber.d(Long.toString(userId.master_key_id(), 16) + (grouped ? "grouped" : "not grouped")); if (!subGrouped) { // 1. This name should NOT be grouped with the previous, so we flush the buffer @@ -203,17 +155,13 @@ public class MultiUserIdsFragment extends Fragment implements LoaderManager.Load } // 2. This name should be grouped with the previous, just add to buffer - uids.add(userId); - lastMasterKeyId = masterKeyId; + uids.add(userId.user_id()); + lastMasterKeyId = userId.master_key_id(); // If this one wasn't grouped, the next one's gotta be a header if (!grouped) { header = true; } - - // Regardless of the outcome, move to next entry - data.moveToNext(); - } // If there is anything left in the buffer, flush it one last time @@ -230,12 +178,7 @@ public class MultiUserIdsFragment extends Fragment implements LoaderManager.Load } - mUserIdsAdapter.swapCursor(matrix); - } - - @Override - public void onLoaderReset(Loader loader) { - mUserIdsAdapter.swapCursor(null); + userIdsAdapter.swapCursor(matrix); } public void setCheckboxVisibility(boolean checkboxVisibility) { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/OrbotRequiredDialogActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/OrbotRequiredDialogActivity.java index c1565fc9d..f96107619 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/OrbotRequiredDialogActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/OrbotRequiredDialogActivity.java @@ -17,7 +17,11 @@ package org.sufficientlysecure.keychain.ui; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; import android.app.ProgressDialog; +import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.os.Handler; @@ -25,14 +29,17 @@ import android.os.Message; import android.os.Messenger; import android.os.RemoteException; import android.support.v4.app.FragmentActivity; +import android.support.v4.app.NotificationCompat; import android.view.ContextThemeWrapper; +import org.sufficientlysecure.keychain.Constants.NotificationIds; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.compatibility.DialogFragmentWorkaround; import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import org.sufficientlysecure.keychain.ui.util.ThemeChanger; import org.sufficientlysecure.keychain.util.ParcelableProxy; import org.sufficientlysecure.keychain.network.orbot.OrbotHelper; +import org.sufficientlysecure.keychain.util.ResourceUtils; import timber.log.Timber; @@ -169,4 +176,36 @@ public class OrbotRequiredDialogActivity extends FragmentActivity } } } + + public static void showOrbotRequiredNotification(Context context) { + NotificationManager manager = (NotificationManager) context.getSystemService(NOTIFICATION_SERVICE); + if (manager != null) { + manager.notify(NotificationIds.KEYSERVER_SYNC_FAIL_ORBOT, createOrbotNotification(context)); + } + } + + private static Notification createOrbotNotification(Context context) { + NotificationCompat.Builder builder = new NotificationCompat.Builder(context); + builder.setSmallIcon(R.drawable.ic_stat_notify_24dp) + .setLargeIcon(ResourceUtils.getDrawableAsNotificationBitmap(context, R.mipmap.ic_launcher)) + .setContentTitle(context.getString(R.string.keyserver_sync_orbot_notif_title)) + .setContentText(context.getString(R.string.keyserver_sync_orbot_notif_msg)) + .setAutoCancel(true); + + Intent startOrbotIntent = new Intent(context, OrbotRequiredDialogActivity.class); + startOrbotIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startOrbotIntent.putExtra(OrbotRequiredDialogActivity.EXTRA_START_ORBOT, true); + PendingIntent startOrbotPi = PendingIntent.getActivity( + context, 0, startOrbotIntent, PendingIntent.FLAG_CANCEL_CURRENT + ); + + builder.addAction(R.drawable.ic_stat_tor, + context.getString(R.string.keyserver_sync_orbot_notif_start), + startOrbotPi + ); + builder.setContentIntent(startOrbotPi); + + return builder.build(); + } + } \ No newline at end of file diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PassphraseDialogActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PassphraseDialogActivity.java index bd0f84764..1f646294c 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PassphraseDialogActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PassphraseDialogActivity.java @@ -52,16 +52,14 @@ import android.widget.ViewAnimator; import org.openintents.openpgp.util.OpenPgpUtils; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.model.SubKey.UnifiedKeyInfo; import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey; import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey.SecretKeyType; import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKeyRing; import org.sufficientlysecure.keychain.pgp.KeyRing; import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; -import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; -import org.sufficientlysecure.keychain.provider.CachedPublicKeyRing; -import org.sufficientlysecure.keychain.provider.KeyRepository; -import org.sufficientlysecure.keychain.provider.KeyRepository.NotFoundException; -import org.sufficientlysecure.keychain.provider.KeychainContract; +import org.sufficientlysecure.keychain.daos.KeyRepository; +import org.sufficientlysecure.keychain.daos.KeyRepository.NotFoundException; import org.sufficientlysecure.keychain.service.PassphraseCacheService; import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; @@ -115,10 +113,9 @@ public class PassphraseDialogActivity extends FragmentActivity { // handle empty passphrases by directly returning an empty crypto input parcel try { - CachedPublicKeyRing pubRing = - KeyRepository.create(this).getCachedPublicKeyRing(requiredInput.getMasterKeyId()); + KeyRepository keyRepository = KeyRepository.create(this); // use empty passphrase for empty passphrase - if (pubRing.getSecretKeyType(requiredInput.getSubKeyId()) == SecretKeyType.PASSPHRASE_EMPTY) { + if (keyRepository.getSecretKeyType(requiredInput.getSubKeyId()) == SecretKeyType.PASSPHRASE_EMPTY) { // also return passphrase back to activity Intent returnIntent = new Intent(); cryptoInputParcel = cryptoInputParcel.withPassphrase(new Passphrase(""), requiredInput.getSubKeyId()); @@ -285,14 +282,16 @@ public class PassphraseDialogActivity extends FragmentActivity { } else { long subKeyId = subKeyIds[0]; - KeyRepository helper = - KeyRepository.create(getContext()); - CachedPublicKeyRing cachedPublicKeyRing = helper.getCachedPublicKeyRing( - KeychainContract.KeyRings.buildUnifiedKeyRingsFindBySubkeyUri(subKeyId)); + KeyRepository keyRepository = KeyRepository.create(getContext()); + Long masterKeyId = keyRepository.getMasterKeyIdBySubkeyId(subKeyId); + UnifiedKeyInfo unifiedKeyInfo = keyRepository.getUnifiedKeyInfo(masterKeyId); + if (unifiedKeyInfo == null) { + throw new NotFoundException(); + } // yes the inner try/catch block is necessary, otherwise the final variable // above can't be statically verified to have been set in all cases because // the catch clause doesn't return. - String mainUserId = cachedPublicKeyRing.getPrimaryUserIdWithFallback(); + String mainUserId = unifiedKeyInfo.user_id(); OpenPgpUtils.UserId mainUserIdSplit = KeyRing.splitUserId(mainUserId); if (mainUserIdSplit.name != null) { userId = mainUserIdSplit.name; @@ -300,7 +299,7 @@ public class PassphraseDialogActivity extends FragmentActivity { userId = getString(R.string.user_id_no_name); } - keyType = cachedPublicKeyRing.getSecretKeyType(subKeyId); + keyType = keyRepository.getSecretKeyType(subKeyId); switch (keyType) { case PASSPHRASE: message = getString(R.string.passphrase_for, userId); @@ -317,14 +316,10 @@ public class PassphraseDialogActivity extends FragmentActivity { throw new AssertionError("Unhandled SecretKeyType (should not happen)"); } } - } catch (PgpKeyNotFoundException | KeyRepository.NotFoundException e) { + } catch (NotFoundException e) { alert.setTitle(R.string.title_key_not_found); alert.setMessage(getString(R.string.key_not_found, mRequiredInput.getSubKeyId())); - alert.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - dismiss(); - } - }); + alert.setPositiveButton(android.R.string.ok, (dialog, which) -> dismiss()); alert.setCancelable(false); return alert.create(); } @@ -534,11 +529,13 @@ public class PassphraseDialogActivity extends FragmentActivity { CanonicalizedSecretKey canonicalizedSecretKey = null; for (long subKeyId : mRequiredInput.getSubKeyIds()) { - CanonicalizedSecretKeyRing secretKeyRing = - KeyRepository.create(getContext()).getCanonicalizedSecretKeyRing( - KeychainContract.KeyRings.buildUnifiedKeyRingsFindBySubkeyUri(subKeyId)); - CanonicalizedSecretKey secretKeyToUnlock = - secretKeyRing.getSecretKey(subKeyId); + KeyRepository keyRepository = KeyRepository.create(getContext()); + Long masterKeyId = keyRepository.getMasterKeyIdBySubkeyId(subKeyId); + if (masterKeyId == null) { + continue; + } + CanonicalizedSecretKeyRing secretKeyRing = keyRepository.getCanonicalizedSecretKeyRing(masterKeyId); + CanonicalizedSecretKey secretKeyToUnlock = secretKeyRing.getSecretKey(subKeyId); // this is the operation may take a very long time (100ms to several seconds!) boolean unlockSucceeded = secretKeyToUnlock.unlock(passphrase); @@ -596,13 +593,9 @@ public class PassphraseDialogActivity extends FragmentActivity { } else { Timber.d("Caching entered passphrase"); - try { - PassphraseCacheService.addCachedPassphrase(getActivity(), - unlockedKey.getRing().getMasterKeyId(), unlockedKey.getKeyId(), passphrase, - unlockedKey.getRing().getPrimaryUserIdWithFallback(), timeToLiveSeconds); - } catch (PgpKeyNotFoundException e) { - Timber.e(e, "adding of a passphrase failed"); - } + PassphraseCacheService.addCachedPassphrase(getActivity(), + unlockedKey.getRing().getMasterKeyId(), unlockedKey.getKeyId(), passphrase, + unlockedKey.getRing().getPrimaryUserIdWithFallback(), timeToLiveSeconds); } finishCaching(passphrase, unlockedKey.getKeyId()); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/QrCodeViewActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/QrCodeViewActivity.java index 5e94bace3..80c757563 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/QrCodeViewActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/QrCodeViewActivity.java @@ -17,95 +17,73 @@ package org.sufficientlysecure.keychain.ui; + +import android.arch.lifecycle.ViewModelProviders; import android.graphics.Bitmap; import android.net.Uri; import android.os.Bundle; import android.support.v4.app.ActivityCompat; import android.support.v7.widget.CardView; -import android.view.View; -import android.view.ViewTreeObserver.OnGlobalLayoutListener; import android.widget.ImageView; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; -import org.sufficientlysecure.keychain.provider.KeyRepository; +import org.sufficientlysecure.keychain.model.SubKey.UnifiedKeyInfo; import org.sufficientlysecure.keychain.ui.base.BaseActivity; +import org.sufficientlysecure.keychain.ui.keyview.UnifiedKeyInfoViewModel; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; import org.sufficientlysecure.keychain.ui.util.Notify; import org.sufficientlysecure.keychain.ui.util.Notify.Style; import org.sufficientlysecure.keychain.ui.util.QrCodeUtils; -import timber.log.Timber; public class QrCodeViewActivity extends BaseActivity { + public static final String EXTRA_MASTER_KEY_ID = "master_key_id"; - private ImageView mQrCode; - private CardView mQrCodeLayout; + private ImageView qrCodeImageView; + private Bitmap qrCode; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - // Inflate a "Done" custom action bar - setFullScreenDialogClose( - new View.OnClickListener() { - @Override - public void onClick(View v) { - // "Done" - ActivityCompat.finishAfterTransition(QrCodeViewActivity.this); - } - } - ); + setFullScreenDialogClose(v -> ActivityCompat.finishAfterTransition(QrCodeViewActivity.this)); - Uri dataUri = getIntent().getData(); - if (dataUri == null) { - Timber.e("Data missing. Should be Uri of key!"); + qrCodeImageView = findViewById(R.id.qr_code_image); + CardView mQrCodeLayout = findViewById(R.id.qr_code_image_layout); + + mQrCodeLayout.setOnClickListener(v -> ActivityCompat.finishAfterTransition(QrCodeViewActivity.this)); + + if (!getIntent().hasExtra(EXTRA_MASTER_KEY_ID)) { + throw new IllegalArgumentException("Missing required extra master_key_id"); + } + + UnifiedKeyInfoViewModel viewModel = ViewModelProviders.of(this).get(UnifiedKeyInfoViewModel.class); + viewModel.setMasterKeyId(getIntent().getLongExtra(EXTRA_MASTER_KEY_ID, 0L)); + viewModel.getUnifiedKeyInfoLiveData(getApplicationContext()).observe(this, this::onLoadUnifiedKeyInfo); + + qrCodeImageView.getViewTreeObserver().addOnGlobalLayoutListener(() -> { + if (qrCode != null) { + Bitmap scaled = Bitmap.createScaledBitmap(qrCode, qrCodeImageView.getWidth(), qrCodeImageView.getWidth(), false); + qrCodeImageView.setImageBitmap(scaled); + } + }); + } + + private void onLoadUnifiedKeyInfo(UnifiedKeyInfo unifiedKeyInfo) { + if (unifiedKeyInfo == null) { + Notify.create(this, R.string.error_key_not_found, Style.ERROR).show(); ActivityCompat.finishAfterTransition(QrCodeViewActivity.this); return; } - mQrCode = findViewById(R.id.qr_code_image); - mQrCodeLayout = findViewById(R.id.qr_code_image_layout); + Uri uri = new Uri.Builder() + .scheme(Constants.FINGERPRINT_SCHEME) + .opaquePart(KeyFormattingUtils.convertFingerprintToHex(unifiedKeyInfo.fingerprint())) + .build(); + qrCode = QrCodeUtils.getQRCodeBitmap(uri, 0); - mQrCodeLayout.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - ActivityCompat.finishAfterTransition(QrCodeViewActivity.this); - } - }); - - KeyRepository keyRepository = KeyRepository.create(this); - try { - byte[] blob = keyRepository.getCachedPublicKeyRing(dataUri).getFingerprint(); - if (blob == null) { - Timber.e("key not found!"); - Notify.create(this, R.string.error_key_not_found, Style.ERROR).show(); - ActivityCompat.finishAfterTransition(QrCodeViewActivity.this); - } - - Uri uri = new Uri.Builder() - .scheme(Constants.FINGERPRINT_SCHEME) - .opaquePart(KeyFormattingUtils.convertFingerprintToHex(blob)) - .build(); - // create a minimal size qr code, we can keep this in ram no problem - final Bitmap qrCode = QrCodeUtils.getQRCodeBitmap(uri, 0); - - mQrCode.getViewTreeObserver().addOnGlobalLayoutListener( - new OnGlobalLayoutListener() { - @Override - public void onGlobalLayout() { - // create actual bitmap in display dimensions - Bitmap scaled = Bitmap.createScaledBitmap(qrCode, - mQrCode.getWidth(), mQrCode.getWidth(), false); - mQrCode.setImageBitmap(scaled); - } - }); - } catch (PgpKeyNotFoundException e) { - Timber.e(e, "key not found!"); - Notify.create(this, R.string.error_key_not_found, Style.ERROR).show(); - ActivityCompat.finishAfterTransition(QrCodeViewActivity.this); - } + qrCodeImageView.requestLayout(); } @Override diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SafeSlingerActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SafeSlingerActivity.java index 5da61584f..26e970955 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SafeSlingerActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SafeSlingerActivity.java @@ -24,7 +24,6 @@ import java.util.ArrayList; import android.annotation.TargetApi; import android.content.Intent; import android.graphics.PorterDuff; -import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.view.View; @@ -40,9 +39,8 @@ import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing; import org.sufficientlysecure.keychain.operations.ImportOperation; import org.sufficientlysecure.keychain.operations.results.ImportKeyResult; import org.sufficientlysecure.keychain.operations.results.OperationResult; -import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; -import org.sufficientlysecure.keychain.provider.KeyRepository; -import org.sufficientlysecure.keychain.provider.KeychainContract; +import org.sufficientlysecure.keychain.daos.KeyRepository; +import org.sufficientlysecure.keychain.daos.KeyRepository.NotFoundException; import org.sufficientlysecure.keychain.service.ImportKeyringParcel; import org.sufficientlysecure.keychain.ui.base.BaseActivity; import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper; @@ -67,12 +65,14 @@ public class SafeSlingerActivity extends BaseActivity private ArrayList mKeyList; private HkpKeyserverAddress mKeyserver; private CryptoOperationHelper mOperationHelper; + private KeyRepository keyRepository; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + keyRepository = KeyRepository.create(this); mMasterKeyId = getIntent().getLongExtra(EXTRA_MASTER_KEY_ID, 0); NumberPicker picker = findViewById(R.id.safe_slinger_picker); @@ -104,10 +104,8 @@ public class SafeSlingerActivity extends BaseActivity } private void startExchange(long masterKeyId, int number) { - // retrieve public key blob and start SafeSlinger - Uri uri = KeychainContract.KeyRingData.buildPublicKeyRingUri(masterKeyId); try { - byte[] keyBlob = KeyRepository.create(this).getCachedPublicKeyRing(uri).getEncoded(); + byte[] keyBlob = keyRepository.loadPublicKeyRingData(masterKeyId); Intent slingerIntent = new Intent(this, ExchangeActivity.class); @@ -115,8 +113,8 @@ public class SafeSlingerActivity extends BaseActivity slingerIntent.putExtra(ExchangeConfig.extra.USER_DATA, keyBlob); slingerIntent.putExtra(ExchangeConfig.extra.HOST_NAME, Constants.SAFESLINGER_SERVER); startActivityForResult(slingerIntent, REQUEST_CODE_SAFE_SLINGER); - } catch (PgpKeyNotFoundException e) { - Timber.e(e, "personal key not found"); + } catch (NotFoundException e) { + Timber.e(e, "key for transfer not found"); } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SecurityTokenOperationActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SecurityTokenOperationActivity.java index 496b4adfd..698d8b515 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SecurityTokenOperationActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SecurityTokenOperationActivity.java @@ -36,8 +36,7 @@ import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKeyRing; import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey; import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKeyRing; -import org.sufficientlysecure.keychain.provider.KeyRepository; -import org.sufficientlysecure.keychain.provider.KeychainContract; +import org.sufficientlysecure.keychain.daos.KeyRepository; import org.sufficientlysecure.keychain.securitytoken.operations.ModifyPinTokenOp; import org.sufficientlysecure.keychain.securitytoken.SecurityTokenConnection; import org.sufficientlysecure.keychain.securitytoken.SecurityTokenInfo; @@ -199,12 +198,11 @@ public class SecurityTokenOperationActivity extends BaseSecurityTokenActivity { throw new IOException(getString(R.string.error_wrong_security_token)); } - KeyRepository keyRepository = - KeyRepository.create(this); + KeyRepository keyRepository = KeyRepository.create(this); CanonicalizedPublicKeyRing publicKeyRing; try { - publicKeyRing = keyRepository.getCanonicalizedPublicKeyRing( - KeychainContract.KeyRings.buildUnifiedKeyRingsFindBySubkeyUri(mRequiredInput.getMasterKeyId())); + Long masterKeyId = keyRepository.getMasterKeyIdBySubkeyId(mRequiredInput.getMasterKeyId()); + publicKeyRing = keyRepository.getCanonicalizedPublicKeyRing(masterKeyId); } catch (KeyRepository.NotFoundException e) { throw new IOException("Couldn't find subkey for key to token operation."); } @@ -263,9 +261,8 @@ public class SecurityTokenOperationActivity extends BaseSecurityTokenActivity { KeyRepository.create(this); CanonicalizedSecretKeyRing secretKeyRing; try { - secretKeyRing = keyRepository.getCanonicalizedSecretKeyRing( - KeychainContract.KeyRings.buildUnifiedKeyRingsFindBySubkeyUri(mRequiredInput.getMasterKeyId()) - ); + Long masterKeyId = keyRepository.getMasterKeyIdBySubkeyId(mRequiredInput.getMasterKeyId()); + secretKeyRing = keyRepository.getCanonicalizedSecretKeyRing(masterKeyId); } catch (KeyRepository.NotFoundException e) { throw new IOException("Couldn't find subkey for key to token operation."); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsActivity.java index 4bcbba382..cc064aa26 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsActivity.java @@ -18,6 +18,11 @@ package org.sufficientlysecure.keychain.ui; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.util.ArrayList; +import java.util.List; + import android.Manifest; import android.accounts.Account; import android.accounts.AccountManager; @@ -48,20 +53,16 @@ import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.KeychainApplication; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.compatibility.AppCompatPreferenceActivity; +import org.sufficientlysecure.keychain.keyimport.HkpKeyserverAddress; +import org.sufficientlysecure.keychain.keysync.KeyserverSyncManager; +import org.sufficientlysecure.keychain.network.orbot.OrbotHelper; import org.sufficientlysecure.keychain.service.ContactSyncAdapterService; import org.sufficientlysecure.keychain.ui.base.BaseActivity; import org.sufficientlysecure.keychain.ui.util.Notify; import org.sufficientlysecure.keychain.ui.util.ThemeChanger; -import org.sufficientlysecure.keychain.keyimport.HkpKeyserverAddress; import org.sufficientlysecure.keychain.util.Preferences; -import org.sufficientlysecure.keychain.network.orbot.OrbotHelper; import timber.log.Timber; -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.util.ArrayList; -import java.util.List; - public class SettingsActivity extends AppCompatPreferenceActivity { public static final int REQUEST_CODE_KEYSERVER_PREF = 0x00007005; @@ -398,6 +399,7 @@ public class SettingsActivity extends AppCompatPreferenceActivity { * This fragment shows the keyserver/wifi-only-sync/contacts sync preferences */ public static class SyncPrefsFragment extends PresetPreferenceFragment { + boolean syncPrefChanged = false; @Override public void onCreate(Bundle savedInstanceState) { @@ -405,6 +407,22 @@ public class SettingsActivity extends AppCompatPreferenceActivity { // Load the preferences from an XML resource addPreferencesFromResource(R.xml.sync_preferences); + + findPreference(Constants.Pref.SYNC_KEYSERVER).setOnPreferenceChangeListener( + (preference, newValue) -> { + syncPrefChanged = true; + return true; + }); + } + + @Override + public void onPause() { + super.onPause(); + + if (syncPrefChanged) { + KeyserverSyncManager.updateKeyserverSyncSchedule(getActivity(), true); + syncPrefChanged = false; + } } @Override @@ -413,12 +431,6 @@ public class SettingsActivity extends AppCompatPreferenceActivity { // this needs to be done in onResume since the user can change sync values from Android // settings and we need to reflect that change when the user navigates back final Account account = KeychainApplication.createAccountIfNecessary(getActivity()); - // for keyserver sync - initializeSyncCheckBox( - (SwitchPreference) findPreference(Constants.Pref.SYNC_KEYSERVER), - account, - Constants.PROVIDER_AUTHORITY - ); // for contacts sync initializeSyncCheckBox( (SwitchPreference) findPreference(Constants.Pref.SYNC_CONTACTS), diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsKeyserverFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsKeyserverFragment.java index 5b20987fc..1e1f1a86d 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsKeyserverFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsKeyserverFragment.java @@ -43,7 +43,7 @@ import android.widget.TextView; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.keyimport.HkpKeyserverAddress; -import org.sufficientlysecure.keychain.provider.LastUpdateInteractor; +import org.sufficientlysecure.keychain.daos.KeyMetadataDao; import org.sufficientlysecure.keychain.ui.dialog.AddEditKeyserverDialogFragment; import org.sufficientlysecure.keychain.ui.util.FormattingUtils; import org.sufficientlysecure.keychain.ui.util.Notify; @@ -63,7 +63,7 @@ public class SettingsKeyserverFragment extends Fragment implements RecyclerItemC private List mKeyservers; private KeyserverListAdapter mAdapter; - private LastUpdateInteractor lastUpdateInteractor; + private KeyMetadataDao keyMetadataDao; public static SettingsKeyserverFragment newInstance(ArrayList keyservers) { Bundle args = new Bundle(); @@ -78,7 +78,7 @@ public class SettingsKeyserverFragment extends Fragment implements RecyclerItemC @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - lastUpdateInteractor = LastUpdateInteractor.create(getContext()); + keyMetadataDao = KeyMetadataDao.create(getContext()); return inflater.inflate(R.layout.settings_keyserver_fragment, null); } @@ -230,7 +230,7 @@ public class SettingsKeyserverFragment extends Fragment implements RecyclerItemC Preferences.getPreferences(getActivity()).setKeyServers(mKeyserversMutable); mKeyservers = Collections.unmodifiableList(new ArrayList<>(mKeyserversMutable)); - lastUpdateInteractor.resetAllLastUpdatedTimes(); + keyMetadataDao.resetAllLastUpdatedTimes(); } @Override diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/UploadKeyActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/UploadKeyActivity.java index 17382fecc..b3c8f6397 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/UploadKeyActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/UploadKeyActivity.java @@ -18,37 +18,30 @@ package org.sufficientlysecure.keychain.ui; +import java.util.ArrayList; + import android.content.Intent; -import android.net.Uri; import android.os.Bundle; -import android.support.v4.app.NavUtils; -import android.view.MenuItem; import android.view.View; -import android.view.View.OnClickListener; import android.widget.ArrayAdapter; import android.widget.Spinner; import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.keyimport.HkpKeyserverAddress; import org.sufficientlysecure.keychain.operations.results.UploadResult; -import org.sufficientlysecure.keychain.provider.KeychainContract; import org.sufficientlysecure.keychain.service.UploadKeyringParcel; import org.sufficientlysecure.keychain.ui.base.BaseActivity; import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper; -import org.sufficientlysecure.keychain.keyimport.HkpKeyserverAddress; import org.sufficientlysecure.keychain.util.Preferences; -import timber.log.Timber; - -import java.util.ArrayList; /** * Sends the selected public key to a keyserver */ public class UploadKeyActivity extends BaseActivity implements CryptoOperationHelper.Callback { - private View mUploadButton; - private Spinner mKeyServerSpinner; + public static final String EXTRA_KEY_IDS = "extra_key_ids"; - private Uri mDataUri; + private Spinner mKeyServerSpinner; // CryptoOperationHelper.Callback vars private HkpKeyserverAddress mKeyserver; @@ -58,7 +51,7 @@ public class UploadKeyActivity extends BaseActivity protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - mUploadButton = findViewById(R.id.upload_key_action_upload); + View uploadButton = findViewById(R.id.upload_key_action_upload); mKeyServerSpinner = findViewById(R.id.upload_key_keyserver); MultiUserIdsFragment mMultiUserIdsFragment = (MultiUserIdsFragment) @@ -73,23 +66,10 @@ public class UploadKeyActivity extends BaseActivity if (adapter.getCount() > 0) { mKeyServerSpinner.setSelection(0); } else { - mUploadButton.setEnabled(false); - } - - mUploadButton.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - uploadKey(); - } - }); - - mDataUri = getIntent().getData(); - if (mDataUri == null) { - Timber.e("Intent data missing. Should be Uri of key!"); - finish(); - return; + uploadButton.setEnabled(false); } + uploadButton.setOnClickListener(v -> uploadKey()); } private String[] getKeyserversArray() { @@ -126,23 +106,9 @@ public class UploadKeyActivity extends BaseActivity mUploadOpHelper.cryptoOperation(); } - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: { - Intent viewIntent = NavUtils.getParentActivityIntent(this); - viewIntent.setData(KeychainContract.KeyRings.buildGenericKeyRingUri(mDataUri)); - NavUtils.navigateUpTo(this, viewIntent); - return true; - } - } - return super.onOptionsItemSelected(item); - } - @Override public UploadKeyringParcel createOperationInput() { - long[] masterKeyIds = getIntent().getLongArrayExtra(MultiUserIdsFragment.EXTRA_KEY_IDS); - + long[] masterKeyIds = getIntent().getLongArrayExtra(EXTRA_KEY_IDS); return UploadKeyringParcel.createWithKeyId(mKeyserver, masterKeyIds[0]); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewCertActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewCertActivity.java deleted file mode 100644 index d52a8f277..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewCertActivity.java +++ /dev/null @@ -1,221 +0,0 @@ -/* - * Copyright (C) 2017 Schürmann & Breitmoser GbR - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sufficientlysecure.keychain.ui; - - -import java.util.Date; - -import android.content.Intent; -import android.database.Cursor; -import android.net.Uri; -import android.os.Bundle; -import android.support.v4.app.LoaderManager; -import android.support.v4.app.NavUtils; -import android.support.v4.content.CursorLoader; -import android.support.v4.content.Loader; -import android.support.v7.app.ActionBar; -import android.text.format.DateFormat; -import android.view.MenuItem; -import android.view.View; -import android.widget.TextView; - -import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.pgp.WrappedSignature; -import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; -import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; -import org.sufficientlysecure.keychain.provider.KeychainContract.Certs; -import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; -import org.sufficientlysecure.keychain.provider.KeyRepository; -import org.sufficientlysecure.keychain.ui.base.BaseActivity; -import org.sufficientlysecure.keychain.ui.keyview.ViewKeyActivity; -import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; -import timber.log.Timber; - - -public class ViewCertActivity extends BaseActivity - implements LoaderManager.LoaderCallbacks { - - // These are the rows that we will retrieve. - static final String[] PROJECTION = new String[]{ - Certs.MASTER_KEY_ID, - Certs.USER_ID, - Certs.TYPE, - Certs.CREATION, - Certs.KEY_ID_CERTIFIER, - Certs.SIGNER_UID, - Certs.DATA, - }; - private static final int INDEX_MASTER_KEY_ID = 0; - private static final int INDEX_USER_ID = 1; - private static final int INDEX_TYPE = 2; - private static final int INDEX_CREATION = 3; - private static final int INDEX_KEY_ID_CERTIFIER = 4; - private static final int INDEX_SIGNER_UID = 5; - private static final int INDEX_DATA = 6; - - private Uri mDataUri; - - private long mCertifierKeyId; - - private TextView mSigneeKey, mSigneeUid, mAlgorithm, mType, mReason, mCreation; - private TextView mCertifierKey, mCertifierUid, mStatus; - private View mRowReason; - private View mViewCertifierButton; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - ActionBar actionBar = getSupportActionBar(); - actionBar.setDisplayHomeAsUpEnabled(true); - - mSigneeKey = findViewById(R.id.signee_key); - mSigneeUid = findViewById(R.id.signee_uid); - mAlgorithm = findViewById(R.id.algorithm); - mType = findViewById(R.id.signature_type); - mReason = findViewById(R.id.reason); - mCreation = findViewById(R.id.creation); - - mCertifierKey = findViewById(R.id.signer_key_id); - mCertifierUid = findViewById(R.id.signer_uid); - - mRowReason = findViewById(R.id.row_reason); - - mViewCertifierButton = findViewById(R.id.view_cert_view_cert_key); - - mDataUri = getIntent().getData(); - if (mDataUri == null) { - Timber.e("Intent data missing. Should be Uri of key!"); - finish(); - return; - } - - getSupportLoaderManager().initLoader(0, null, this); - } - - @Override - protected void initLayout() { - setContentView(R.layout.view_cert_activity); - } - - @Override - public Loader onCreateLoader(int id, Bundle args) { - // Now create and return a CursorLoader that will take care of - // creating a Cursor for the data being displayed. - return new CursorLoader(this, mDataUri, PROJECTION, null, null, null); - } - - @Override - public void onLoadFinished(Loader loader, Cursor data) { - if (data.moveToFirst()) { - mSigneeKey.setText(KeyFormattingUtils.beautifyKeyId(data.getLong(INDEX_MASTER_KEY_ID))); - - String signeeUid = data.getString(INDEX_USER_ID); - mSigneeUid.setText(signeeUid); - - Date creationDate = new Date(data.getLong(INDEX_CREATION) * 1000); - mCreation.setText(DateFormat.getDateFormat(getApplicationContext()).format(creationDate)); - - mCertifierKeyId = data.getLong(INDEX_KEY_ID_CERTIFIER); - mCertifierKey.setText(KeyFormattingUtils.beautifyKeyId(mCertifierKeyId)); - - String certifierUid = data.getString(INDEX_SIGNER_UID); - if (certifierUid != null) { - mCertifierUid.setText(certifierUid); - } else { - mCertifierUid.setText(R.string.unknown_uid); - } - - WrappedSignature sig = WrappedSignature.fromBytes(data.getBlob(INDEX_DATA)); - - String algorithmStr = KeyFormattingUtils.getAlgorithmInfo(this, sig.getKeyAlgorithm(), null, null); - mAlgorithm.setText(algorithmStr); - - mRowReason.setVisibility(View.GONE); - switch (data.getInt(INDEX_TYPE)) { - case WrappedSignature.DEFAULT_CERTIFICATION: - mType.setText(R.string.cert_default); - break; - case WrappedSignature.NO_CERTIFICATION: - mType.setText(R.string.cert_none); - break; - case WrappedSignature.CASUAL_CERTIFICATION: - mType.setText(R.string.cert_casual); - break; - case WrappedSignature.POSITIVE_CERTIFICATION: - mType.setText(R.string.cert_positive); - break; - case WrappedSignature.CERTIFICATION_REVOCATION: { - mType.setText(R.string.cert_revoke); - if (sig.isRevocation()) { - try { - String reason = sig.getRevocationReason(); - if (reason != null) { - mReason.setText(reason); - } else { - mReason.setText(R.string.none); - } - } catch(PgpGeneralException e) { - mReason.setText(R.string.none); - } - } - mRowReason.setVisibility(View.VISIBLE); - break; - } - } - } - - // can't do this before the data is initialized - mViewCertifierButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - Intent viewIntent = new Intent(ViewCertActivity.this, ViewKeyActivity.class); - - try { - KeyRepository keyRepository = - KeyRepository.create(ViewCertActivity.this); - long signerMasterKeyId = keyRepository.getCachedPublicKeyRing( - KeyRings.buildUnifiedKeyRingsFindBySubkeyUri(mCertifierKeyId)).getMasterKeyId(); - viewIntent.setData(KeyRings.buildGenericKeyRingUri(signerMasterKeyId)); - startActivity(viewIntent); - } catch (PgpKeyNotFoundException e) { - // TODO notify user of this, maybe offer download? - Timber.e(e, "key not found!"); - } - } - }); - } - - @Override - public void onLoaderReset(Loader loader) { - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: { - Intent viewIntent = NavUtils.getParentActivityIntent(this); - viewIntent.setData(KeyRings.buildGenericKeyRingUri(mDataUri)); - NavUtils.navigateUpTo(this, viewIntent); - return true; - } - } - return super.onOptionsItemSelected(item); - } - -} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvActivity.java index bc5233711..71eb650c7 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvActivity.java @@ -18,16 +18,19 @@ package org.sufficientlysecure.keychain.ui; +import java.util.List; + import android.animation.Animator; import android.animation.AnimatorListenerAdapter; +import android.arch.lifecycle.LiveData; +import android.arch.lifecycle.Transformations; +import android.arch.lifecycle.ViewModel; +import android.arch.lifecycle.ViewModelProviders; +import android.content.Context; import android.content.Intent; -import android.database.Cursor; -import android.net.Uri; import android.os.Bundle; -import android.provider.ContactsContract; -import android.support.v4.app.LoaderManager.LoaderCallbacks; -import android.support.v4.content.CursorLoader; -import android.support.v4.content.Loader; +import android.support.annotation.StringRes; +import android.support.v4.app.Fragment; import android.support.v4.view.ViewPager; import android.support.v4.view.ViewPager.OnPageChangeListener; import android.view.ActionMode; @@ -37,83 +40,154 @@ import android.view.MenuItem; import android.view.View; import android.view.ViewPropertyAnimator; import android.view.animation.OvershootInterpolator; -import android.widget.Toast; import com.astuetz.PagerSlidingTabStrip; import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.livedata.GenericLiveData; +import org.sufficientlysecure.keychain.model.SubKey; +import org.sufficientlysecure.keychain.model.SubKey.UnifiedKeyInfo; +import org.sufficientlysecure.keychain.model.UserPacket.UserId; import org.sufficientlysecure.keychain.operations.results.OperationResult; -import org.sufficientlysecure.keychain.provider.KeyRepository; -import org.sufficientlysecure.keychain.provider.KeychainContract; +import org.sufficientlysecure.keychain.daos.KeyRepository; import org.sufficientlysecure.keychain.ui.adapter.PagerTabStripAdapter; import org.sufficientlysecure.keychain.ui.base.BaseActivity; import org.sufficientlysecure.keychain.ui.keyview.ViewKeyActivity; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; -import org.sufficientlysecure.keychain.util.ContactHelper; -import timber.log.Timber; -public class ViewKeyAdvActivity extends BaseActivity implements - LoaderCallbacks, OnPageChangeListener { - - KeyRepository mKeyRepository; - - protected Uri mDataUri; - +public class ViewKeyAdvActivity extends BaseActivity implements OnPageChangeListener { + public static final String EXTRA_MASTER_KEY_ID = "master_key_id"; public static final String EXTRA_SELECTED_TAB = "selected_tab"; - public static final int TAB_START = 0; - public static final int TAB_SHARE = 1; - public static final int TAB_IDENTITIES = 2; - public static final int TAB_SUBKEYS = 3; - public static final int TAB_CERTS = 4; + + KeyRepository keyRepository; // view private ViewPager mViewPager; private PagerSlidingTabStrip mSlidingTabLayout; - private static final int LOADER_ID_UNIFIED = 0; private ActionMode mActionMode; - private boolean mHasSecret; - private PagerTabStripAdapter mTabAdapter; + private boolean hasSecret; private boolean mActionIconShown; - private boolean[] mTabsWithActionMode; + + enum ViewKeyAdvTab { + START(ViewKeyAdvStartFragment.class, R.string.key_view_tab_start, false), + SHARE(ViewKeyAdvShareFragment.class, R.string.key_view_tab_share, false), + IDENTITIES(ViewKeyAdvUserIdsFragment.class, R.string.section_user_ids, true), + SUBKEYS(ViewKeyAdvSubkeysFragment.class, R.string.key_view_tab_keys, true); + + public final Class fragmentClass; + public final int titleRes; + public final boolean hasActionMode; + + ViewKeyAdvTab(Class fragmentClass, @StringRes int titleRes, boolean hasActionMode) { + this.titleRes = titleRes; + this.fragmentClass = fragmentClass; + this.hasActionMode = hasActionMode; + } + } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setFullScreenDialogClose(new View.OnClickListener() { - @Override - public void onClick(View v) { - finish(); - } - }); + setFullScreenDialogClose(v -> finish()); - mKeyRepository = KeyRepository.create(this); + keyRepository = KeyRepository.create(this); mViewPager = findViewById(R.id.pager); mSlidingTabLayout = findViewById(R.id.sliding_tab_layout); - mDataUri = getIntent().getData(); - if (mDataUri == null) { - Timber.e("Data missing. Should be uri of key!"); - finish(); + if (!getIntent().hasExtra(EXTRA_MASTER_KEY_ID)) { + throw new IllegalArgumentException("Missing required extra master_key_id"); + } + + ViewKeyAdvViewModel viewModel = ViewModelProviders.of(this).get(ViewKeyAdvViewModel.class); + viewModel.setMasterKeyId(getIntent().getLongExtra(EXTRA_MASTER_KEY_ID, 0L)); + viewModel.getUnifiedKeyInfoLiveData(getApplicationContext()).observe(this, this::onLoadUnifiedKeyInfo); + + initTabs(); + } + + public static class ViewKeyAdvViewModel extends ViewModel { + private Long masterKeyId; + private LiveData unifiedKeyInfoLiveData; + private LiveData> subKeyLiveData; + private LiveData> userIdsLiveData; + + void setMasterKeyId(long masterKeyId) { + if (this.masterKeyId != null) { + throw new IllegalStateException("cannot change masterKeyId once set!"); + } + this.masterKeyId = masterKeyId; + } + + LiveData getUnifiedKeyInfoLiveData(Context context) { + if (masterKeyId == null) { + throw new IllegalStateException("masterKeyId must be set to retrieve this!"); + } + if (unifiedKeyInfoLiveData == null) { + KeyRepository keyRepository = KeyRepository.create(context); + unifiedKeyInfoLiveData = new GenericLiveData<>(context, masterKeyId, + () -> keyRepository.getUnifiedKeyInfo(masterKeyId)); + } + return unifiedKeyInfoLiveData; + } + + LiveData> getSubkeyLiveData(Context context) { + if (subKeyLiveData == null) { + KeyRepository keyRepository = KeyRepository.create(context); + subKeyLiveData = Transformations.switchMap(getUnifiedKeyInfoLiveData(context), + (unifiedKeyInfo) -> unifiedKeyInfo == null ? null : new GenericLiveData<>(context, + () -> keyRepository.getSubKeysByMasterKeyId(unifiedKeyInfo.master_key_id()))); + } + return subKeyLiveData; + } + + LiveData> getUserIdLiveData(Context context) { + if (userIdsLiveData == null) { + KeyRepository keyRepository = KeyRepository.create(context); + userIdsLiveData = Transformations.switchMap(getUnifiedKeyInfoLiveData(context), + (unifiedKeyInfo) -> unifiedKeyInfo == null ? null : new GenericLiveData<>(context, + () -> keyRepository.getUserIds(unifiedKeyInfo.master_key_id()))); + } + return userIdsLiveData; + } + } + + public void onLoadUnifiedKeyInfo(UnifiedKeyInfo unifiedKeyInfo) { + if (unifiedKeyInfo == null) { return; } - if (mDataUri.getHost().equals(ContactsContract.AUTHORITY)) { - mDataUri = new ContactHelper(this).dataUriFromContactUri(mDataUri); - if (mDataUri == null) { - Timber.e("Contact Data missing. Should be uri of key!"); - Toast.makeText(this, R.string.error_contacts_key_id_missing, Toast.LENGTH_LONG).show(); - finish(); - return; - } + + if (unifiedKeyInfo.name() != null) { + setTitle(unifiedKeyInfo.name()); + } else { + setTitle(R.string.user_id_no_name); } - // Prepare the loaders. Either re-connect with an existing ones, - // or start new ones. - getSupportLoaderManager().initLoader(LOADER_ID_UNIFIED, null, this); + String formattedKeyId = KeyFormattingUtils.beautifyKeyIdWithPrefix(unifiedKeyInfo.master_key_id()); + mToolbar.setSubtitle(formattedKeyId); - initTabs(mDataUri); + hasSecret = unifiedKeyInfo.has_any_secret(); + + // Note: order is important + int color; + if (unifiedKeyInfo.is_revoked() || unifiedKeyInfo.is_expired()) { + color = getResources().getColor(R.color.key_flag_red); + } else if (unifiedKeyInfo.has_any_secret()) { + color = getResources().getColor(R.color.android_green_light); + } else { + if (unifiedKeyInfo.is_verified()) { + color = getResources().getColor(R.color.android_green_light); + } else { + color = getResources().getColor(R.color.key_flag_orange); + } + } + mToolbar.setBackgroundColor(color); + mStatusBar.setBackgroundColor(ViewKeyActivity.getStatusBarBackgroundColor(color)); + mSlidingTabLayout.setBackgroundColor(color); + + invalidateOptionsMenu(); } @Override @@ -121,40 +195,13 @@ public class ViewKeyAdvActivity extends BaseActivity implements setContentView(R.layout.view_key_adv_activity); } - private void initTabs(Uri dataUri) { - mTabAdapter = new PagerTabStripAdapter(this); - mViewPager.setAdapter(mTabAdapter); + private void initTabs() { + PagerTabStripAdapter tabAdapter = new PagerTabStripAdapter(this); + mViewPager.setAdapter(tabAdapter); - // keep track which of these are action mode enabled! - mTabsWithActionMode = new boolean[5]; - - mTabAdapter.addTab(ViewKeyAdvStartFragment.class, - null, getString(R.string.key_view_tab_start)); - mTabsWithActionMode[0] = false; - - Bundle shareBundle = new Bundle(); - shareBundle.putParcelable(ViewKeyAdvShareFragment.ARG_DATA_URI, dataUri); - mTabAdapter.addTab(ViewKeyAdvShareFragment.class, - shareBundle, getString(R.string.key_view_tab_share)); - mTabsWithActionMode[1] = false; - - Bundle userIdsBundle = new Bundle(); - userIdsBundle.putParcelable(ViewKeyAdvUserIdsFragment.ARG_DATA_URI, dataUri); - mTabAdapter.addTab(ViewKeyAdvUserIdsFragment.class, - userIdsBundle, getString(R.string.section_user_ids)); - mTabsWithActionMode[2] = true; - - Bundle keysBundle = new Bundle(); - keysBundle.putParcelable(ViewKeyAdvSubkeysFragment.ARG_DATA_URI, dataUri); - mTabAdapter.addTab(ViewKeyAdvSubkeysFragment.class, - keysBundle, getString(R.string.key_view_tab_keys)); - mTabsWithActionMode[3] = true; - - Bundle certsBundle = new Bundle(); - certsBundle.putParcelable(ViewKeyAdvCertsFragment.ARG_DATA_URI, dataUri); - mTabAdapter.addTab(ViewKeyAdvCertsFragment.class, - certsBundle, getString(R.string.key_view_tab_certs)); - mTabsWithActionMode[4] = false; + for (ViewKeyAdvTab tab : ViewKeyAdvTab.values()) { + tabAdapter.addTab(tab.fragmentClass, null, getString(tab.titleRes)); + } // update layout after operations mSlidingTabLayout.setViewPager(mViewPager); @@ -162,108 +209,8 @@ public class ViewKeyAdvActivity extends BaseActivity implements // switch to tab selected by extra Intent intent = getIntent(); - int switchToTab = intent.getIntExtra(EXTRA_SELECTED_TAB, TAB_START); + int switchToTab = intent.getIntExtra(EXTRA_SELECTED_TAB, 0); mViewPager.setCurrentItem(switchToTab); - - } - - // These are the rows that we will retrieve. - static final String[] PROJECTION = new String[]{ - KeychainContract.KeyRings._ID, - KeychainContract.KeyRings.MASTER_KEY_ID, - KeychainContract.KeyRings.USER_ID, - KeychainContract.KeyRings.IS_REVOKED, - KeychainContract.KeyRings.IS_EXPIRED, - KeychainContract.KeyRings.VERIFIED, - KeychainContract.KeyRings.HAS_ANY_SECRET, - KeychainContract.KeyRings.FINGERPRINT, - KeychainContract.KeyRings.NAME, - KeychainContract.KeyRings.EMAIL, - KeychainContract.KeyRings.COMMENT, - }; - - static final int INDEX_MASTER_KEY_ID = 1; - static final int INDEX_USER_ID = 2; - static final int INDEX_IS_REVOKED = 3; - static final int INDEX_IS_EXPIRED = 4; - static final int INDEX_VERIFIED = 5; - static final int INDEX_HAS_ANY_SECRET = 6; - static final int INDEX_FINGERPRINT = 7; - static final int INDEX_NAME = 8; - static final int INDEX_EMAIL = 9; - static final int INDEX_COMMENT = 10; - - @Override - public Loader onCreateLoader(int id, Bundle args) { - switch (id) { - case LOADER_ID_UNIFIED: { - Uri baseUri = KeychainContract.KeyRings.buildUnifiedKeyRingUri(mDataUri); - return new CursorLoader(this, baseUri, PROJECTION, null, null, null); - } - - default: - return null; - } - } - - @Override - public void onLoadFinished(Loader loader, Cursor data) { - // Avoid NullPointerExceptions... - if (data == null || data.getCount() == 0) { - return; - } - // Swap the new cursor in. (The framework will take care of closing the - // old cursor once we return.) - switch (loader.getId()) { - case LOADER_ID_UNIFIED: { - if (data.moveToFirst()) { - // get name, email, and comment from USER_ID - String name = data.getString(INDEX_NAME); - - if (name != null) { - setTitle(name); - } else { - setTitle(R.string.user_id_no_name); - } - - byte[] fingerprint = data.getBlob(INDEX_FINGERPRINT); - - // get key id from MASTER_KEY_ID - long masterKeyId = data.getLong(INDEX_MASTER_KEY_ID); - String formattedKeyId = KeyFormattingUtils.beautifyKeyIdWithPrefix(masterKeyId); - getSupportActionBar().setSubtitle(formattedKeyId); - - mHasSecret = data.getInt(INDEX_HAS_ANY_SECRET) != 0; - boolean isRevoked = data.getInt(INDEX_IS_REVOKED) > 0; - boolean isExpired = data.getInt(INDEX_IS_EXPIRED) != 0; - boolean isVerified = data.getInt(INDEX_VERIFIED) > 0; - - // Note: order is important - int color; - if (isRevoked || isExpired) { - color = getResources().getColor(R.color.key_flag_red); - } else if (mHasSecret) { - color = getResources().getColor(R.color.android_green_light); - } else { - if (isVerified) { - color = getResources().getColor(R.color.android_green_light); - } else { - color = getResources().getColor(R.color.key_flag_orange); - } - } - mToolbar.setBackgroundColor(color); - mStatusBar.setBackgroundColor(ViewKeyActivity.getStatusBarBackgroundColor(color)); - mSlidingTabLayout.setBackgroundColor(color); - - break; - } - } - } - } - - @Override - public void onLoaderReset(Loader loader) { - } @Override @@ -279,8 +226,7 @@ public class ViewKeyAdvActivity extends BaseActivity implements @Override public boolean onCreateOptionsMenu(Menu menu) { - - if (!mHasSecret) { + if (!hasSecret) { return false; } @@ -288,7 +234,7 @@ public class ViewKeyAdvActivity extends BaseActivity implements getMenuInflater().inflate(R.menu.action_mode_edit, menu); final MenuItem vActionModeItem = menu.findItem(R.id.menu_action_mode_edit); - boolean isCurrentActionFragment = mTabsWithActionMode[mViewPager.getCurrentItem()]; + boolean isCurrentActionFragment = ViewKeyAdvTab.values()[mViewPager.getCurrentItem()].hasActionMode; // if the state is as it should be, never mind if (isCurrentActionFragment == mActionIconShown) { @@ -304,7 +250,6 @@ public class ViewKeyAdvActivity extends BaseActivity implements } private void animateMenuItem(final MenuItem vEditSubkeys, final boolean animateShow) { - View actionView = LayoutInflater.from(this).inflate(R.layout.edit_icon, null); vEditSubkeys.setActionView(actionView); actionView.setTranslationX(animateShow ? 150 : 0); @@ -323,7 +268,6 @@ public class ViewKeyAdvActivity extends BaseActivity implements } }); animator.start(); - } @Override diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvCertsFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvCertsFragment.java deleted file mode 100644 index 1a11c1bce..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvCertsFragment.java +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright (C) 2017 Schürmann & Breitmoser GbR - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sufficientlysecure.keychain.ui; - -import android.content.Intent; -import android.database.Cursor; -import android.net.Uri; -import android.os.Bundle; -import android.support.v4.app.LoaderManager; -import android.support.v4.content.CursorLoader; -import android.support.v4.content.Loader; -import android.view.View; -import android.view.ViewGroup; -import android.view.LayoutInflater; - -import com.tonicartos.superslim.LayoutManager; - -import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.provider.KeychainContract; -import org.sufficientlysecure.keychain.ui.adapter.CertSectionedListAdapter; -import org.sufficientlysecure.keychain.ui.base.RecyclerFragment; -import timber.log.Timber; - - -public class ViewKeyAdvCertsFragment extends RecyclerFragment - implements LoaderManager.LoaderCallbacks, CertSectionedListAdapter.CertListListener { - - public static final String ARG_DATA_URI = "data_uri"; - private Uri mDataUriCerts; - - /** - * Creates new instance of this fragment - */ - public static ViewKeyAdvCertsFragment newInstance(Uri dataUri) { - ViewKeyAdvCertsFragment frag = new ViewKeyAdvCertsFragment(); - - Bundle args = new Bundle(); - args.putParcelable(ARG_DATA_URI, dataUri); - - frag.setArguments(args); - return frag; - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - return inflater.inflate(R.layout.view_key_adv_certs_fragment, container, false); - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - hideList(false); - - Uri dataUri = getArguments().getParcelable(ARG_DATA_URI); - if (dataUri == null) { - Timber.e("Data missing. Should be Uri of key!"); - getActivity().finish(); - return; - } else { - mDataUriCerts = KeychainContract.Certs.buildCertsUri(dataUri); - } - - CertSectionedListAdapter adapter = new CertSectionedListAdapter(getActivity(), null); - adapter.setCertListListener(this); - - setAdapter(adapter); - setLayoutManager(new LayoutManager(getActivity())); - - getLoaderManager().initLoader(0, null, this); - } - - public Loader onCreateLoader(int id, Bundle args) { - // Now create and return a CursorLoader that will take care of - // creating a Cursor for the data being displayed. - return new CursorLoader(getActivity(), mDataUriCerts, - CertSectionedListAdapter.CertCursor.CERTS_PROJECTION, null, null, - CertSectionedListAdapter.CertCursor.CERTS_SORT_ORDER); - } - - public void onLoadFinished(Loader loader, Cursor data) { - // Avoid NullPointerExceptions, if we get an empty result set. - if (data.getCount() == 0) { - return; - } - - // Swap the new cursor in. (The framework will take care of closing the - // old cursor once we return.) - getAdapter().swapCursor(CertSectionedListAdapter.CertCursor.wrap(data)); - - if (isResumed()) { - showList(true); - } else { - showList(false); - } - } - - /** - * This is called when the last Cursor provided to onLoadFinished() above is about to be closed. - * We need to make sure we are no longer using it. - */ - public void onLoaderReset(Loader loader) { - getAdapter().swapCursor(null); - } - - @Override - public void onClick(long masterKeyId, long signerKeyId, long rank) { - if(masterKeyId != 0L) { - Intent viewIntent = new Intent(getActivity(), ViewCertActivity.class); - viewIntent.setData(KeychainContract.Certs.buildCertsSpecificUri( - masterKeyId, rank, signerKeyId)); - startActivity(viewIntent); - } - } -} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvShareFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvShareFragment.java index f83ecfc88..9c80c17d6 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvShareFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvShareFragment.java @@ -18,46 +18,49 @@ package org.sufficientlysecure.keychain.ui; +import java.io.BufferedWriter; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.security.NoSuchAlgorithmException; + import android.app.Activity; import android.app.ActivityOptions; +import android.arch.lifecycle.LiveData; +import android.arch.lifecycle.Transformations; +import android.arch.lifecycle.ViewModelProviders; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; import android.content.Intent; -import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.PorterDuff; import android.net.Uri; -import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.os.ParcelFileDescriptor; +import android.support.annotation.NonNull; import android.support.v4.app.ActivityCompat; -import android.support.v4.app.LoaderManager; -import android.support.v4.content.CursorLoader; -import android.support.v4.content.Loader; +import android.support.v4.app.Fragment; import android.support.v7.widget.CardView; import android.view.LayoutInflater; import android.view.View; -import android.view.View.OnLayoutChangeListener; import android.view.ViewGroup; import android.view.animation.AlphaAnimation; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.TextView; -import org.openintents.openpgp.util.OpenPgpUtils; + import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.livedata.GenericLiveData; +import org.sufficientlysecure.keychain.model.SubKey.UnifiedKeyInfo; import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKey; -import org.sufficientlysecure.keychain.pgp.KeyRing; import org.sufficientlysecure.keychain.pgp.SshPublicKey; import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; -import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; -import org.sufficientlysecure.keychain.provider.CachedPublicKeyRing; -import org.sufficientlysecure.keychain.provider.KeyRepository; -import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; +import org.sufficientlysecure.keychain.daos.KeyRepository; import org.sufficientlysecure.keychain.provider.TemporaryFileProvider; -import org.sufficientlysecure.keychain.ui.base.LoaderFragment; +import org.sufficientlysecure.keychain.ui.ViewKeyAdvActivity.ViewKeyAdvViewModel; import org.sufficientlysecure.keychain.ui.util.FormattingUtils; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; import org.sufficientlysecure.keychain.ui.util.Notify; @@ -65,33 +68,17 @@ import org.sufficientlysecure.keychain.ui.util.Notify.Style; import org.sufficientlysecure.keychain.ui.util.QrCodeUtils; import timber.log.Timber; -import java.io.BufferedWriter; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.OutputStreamWriter; -import java.security.NoSuchAlgorithmException; - -public class ViewKeyAdvShareFragment extends LoaderFragment implements - LoaderManager.LoaderCallbacks { - - public static final String ARG_DATA_URI = "uri"; - +public class ViewKeyAdvShareFragment extends Fragment { private ImageView mQrCode; private CardView mQrCodeLayout; private TextView mFingerprintView; - private static final int LOADER_ID_UNIFIED = 0; - - private Uri mDataUri; - - private byte[] mFingerprint; - private String mUserId; private Bitmap mQrCodeBitmapCache; + private UnifiedKeyInfo unifiedKeyInfo; @Override - public View onCreateView(LayoutInflater inflater, ViewGroup superContainer, Bundle savedInstanceState) { - View root = super.onCreateView(inflater, superContainer, savedInstanceState); - View view = inflater.inflate(R.layout.view_key_adv_share_fragment, getContainer()); + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup viewGroup, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.view_key_adv_share_fragment, viewGroup, false); mFingerprintView = view.findViewById(R.id.view_key_fingerprint); mQrCode = view.findViewById(R.id.view_key_qr_code); @@ -102,34 +89,24 @@ public class ViewKeyAdvShareFragment extends LoaderFragment implements // just calls requestLayout when it is finished, this way the loader and // background task are disconnected from any layouting the ImageView may // undergo. Please note how these six lines are perfectly right-aligned. - mQrCode.addOnLayoutChangeListener(new OnLayoutChangeListener() { - @Override - public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, - int oldRight, - int oldBottom) { - // bitmap scaling is expensive, avoid doing it if we already have the correct size! - int mCurrentWidth = 0, mCurrentHeight = 0; - if (mQrCodeBitmapCache != null) { - if (mCurrentWidth == mQrCode.getWidth() && mCurrentHeight == mQrCode.getHeight()) { - return; - } - mCurrentWidth = mQrCode.getWidth(); - mCurrentHeight = mQrCode.getHeight(); - // scale the image up to our actual size. we do this in code rather - // than let the ImageView do this because we don't require filtering. - Bitmap scaled = Bitmap.createScaledBitmap(mQrCodeBitmapCache, - mCurrentWidth, mCurrentHeight, false); - mQrCode.setImageBitmap(scaled); + mQrCode.addOnLayoutChangeListener((v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { + // bitmap scaling is expensive, avoid doing it if we already have the correct size! + int mCurrentWidth = 0, mCurrentHeight = 0; + if (mQrCodeBitmapCache != null) { + if (mCurrentWidth == mQrCode.getWidth() && mCurrentHeight == mQrCode.getHeight()) { + return; } + mCurrentWidth = mQrCode.getWidth(); + mCurrentHeight = mQrCode.getHeight(); + // scale the image up to our actual size. we do this in code rather + // than let the ImageView do this because we don't require filtering. + Bitmap scaled = Bitmap.createScaledBitmap(mQrCodeBitmapCache, + mCurrentWidth, mCurrentHeight, false); + mQrCode.setImageBitmap(scaled); } }); mQrCodeLayout = view.findViewById(R.id.view_key_qr_code_layout); - mQrCodeLayout.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - showQrCodeDialog(); - } - }); + mQrCodeLayout.setOnClickListener(v -> showQrCodeDialog()); View vFingerprintShareButton = view.findViewById(R.id.view_key_action_fingerprint_share); View vFingerprintClipboardButton = view.findViewById(R.id.view_key_action_fingerprint_clipboard); @@ -139,106 +116,42 @@ public class ViewKeyAdvShareFragment extends LoaderFragment implements View vKeySshShareButton = view.findViewById(R.id.view_key_action_key_ssh_share); View vKeySshClipboardButton = view.findViewById(R.id.view_key_action_key_ssh_clipboard); View vKeyUploadButton = view.findViewById(R.id.view_key_action_upload); - vKeySafeSlingerButton.setColorFilter(FormattingUtils.getColorFromAttr(getActivity(), R.attr.colorTertiaryText), + vKeySafeSlingerButton.setColorFilter(FormattingUtils.getColorFromAttr(requireContext(), R.attr.colorTertiaryText), PorterDuff.Mode.SRC_IN); - vFingerprintShareButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - shareFingerprint(false); - } - }); - vFingerprintClipboardButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - shareFingerprint(true); - } - }); - vKeyShareButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - shareKey(false, false); - } - }); - vKeyClipboardButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - shareKey(true, false); - } - }); + vFingerprintShareButton.setOnClickListener(v -> shareFingerprint(false)); + vFingerprintClipboardButton.setOnClickListener(v -> shareFingerprint(true)); + vKeyShareButton.setOnClickListener(v -> shareKey(false, false)); + vKeyClipboardButton.setOnClickListener(v -> shareKey(true, false)); - vKeySafeSlingerButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - startSafeSlinger(mDataUri); - } - }); - vKeySshShareButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - shareKey(false, true); - } - }); - vKeySshClipboardButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - shareKey(true, true); - } - }); - vKeyUploadButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - uploadToKeyserver(); - } - }); + vKeySafeSlingerButton.setOnClickListener(v -> startSafeSlinger()); + vKeySshShareButton.setOnClickListener(v -> shareKey(false, true)); + vKeySshClipboardButton.setOnClickListener(v -> shareKey(true, true)); + vKeyUploadButton.setOnClickListener(v -> uploadToKeyserver()); - return root; + return view; } - private void startSafeSlinger(Uri dataUri) { - long keyId = 0; - try { - keyId = KeyRepository.create(getContext()) - .getCachedPublicKeyRing(dataUri) - .extractOrGetMasterKeyId(); - } catch (PgpKeyNotFoundException e) { - Timber.e(e, "key not found!"); - } + private void startSafeSlinger() { Intent safeSlingerIntent = new Intent(getActivity(), SafeSlingerActivity.class); - safeSlingerIntent.putExtra(SafeSlingerActivity.EXTRA_MASTER_KEY_ID, keyId); + safeSlingerIntent.putExtra(SafeSlingerActivity.EXTRA_MASTER_KEY_ID, unifiedKeyInfo.master_key_id()); startActivityForResult(safeSlingerIntent, 0); } - private boolean hasAuthenticationKey() { - KeyRepository keyRepository = KeyRepository.create(getContext()); - long masterKeyId = Constants.key.none; - long authSubKeyId = Constants.key.none; - try { - masterKeyId = keyRepository.getCachedPublicKeyRing(mDataUri).extractOrGetMasterKeyId(); - CachedPublicKeyRing cachedPublicKeyRing = keyRepository.getCachedPublicKeyRing(masterKeyId); - authSubKeyId = cachedPublicKeyRing.getAuthenticationId(); - } catch (PgpKeyNotFoundException e) { - Timber.e(e, "key not found!"); - } - return authSubKeyId != Constants.key.none; - } - private String getShareKeyContent(boolean asSshKey) - throws PgpKeyNotFoundException, KeyRepository.NotFoundException, IOException, PgpGeneralException, - NoSuchAlgorithmException { + throws KeyRepository.NotFoundException, IOException, PgpGeneralException, NoSuchAlgorithmException { - KeyRepository keyRepository = KeyRepository.create(getContext()); + KeyRepository keyRepository = KeyRepository.create(requireContext()); String content; - long masterKeyId = keyRepository.getCachedPublicKeyRing(mDataUri).extractOrGetMasterKeyId(); if (asSshKey) { - long authSubKeyId = keyRepository.getCachedPublicKeyRing(masterKeyId).getAuthenticationId(); - CanonicalizedPublicKey publicKey = keyRepository.getCanonicalizedPublicKeyRing(masterKeyId) + long authSubKeyId = unifiedKeyInfo.has_auth_key_int(); + CanonicalizedPublicKey publicKey = keyRepository.getCanonicalizedPublicKeyRing(unifiedKeyInfo.master_key_id()) .getPublicKey(authSubKeyId); SshPublicKey sshPublicKey = new SshPublicKey(publicKey); content = sshPublicKey.getEncodedKey(); } else { - content = keyRepository.getPublicKeyRingAsArmoredString(masterKeyId); + content = keyRepository.getPublicKeyRingAsArmoredString(unifiedKeyInfo.master_key_id()); } return content; @@ -246,10 +159,10 @@ public class ViewKeyAdvShareFragment extends LoaderFragment implements private void shareKey(boolean toClipboard, boolean asSshKey) { Activity activity = getActivity(); - if (activity == null || mFingerprint == null) { + if (activity == null || unifiedKeyInfo == null) { return; } - if (asSshKey && !hasAuthenticationKey()) { + if (asSshKey && !unifiedKeyInfo.has_auth_key()) { Notify.create(activity, R.string.authentication_subkey_not_found, Style.ERROR).show(); return; } @@ -281,10 +194,11 @@ public class ViewKeyAdvShareFragment extends LoaderFragment implements try { TemporaryFileProvider shareFileProv = new TemporaryFileProvider(); - String filename = KeyFormattingUtils.convertFingerprintToHex(mFingerprint); - OpenPgpUtils.UserId mainUserId = KeyRing.splitUserId(mUserId); - if (mainUserId.name != null) { - filename = mainUserId.name; + String filename; + if (unifiedKeyInfo.name() != null) { + filename = unifiedKeyInfo.name(); + } else { + filename = KeyFormattingUtils.convertFingerprintToHex(unifiedKeyInfo.fingerprint()); } Uri contentUri = TemporaryFileProvider.createFile(activity, filename + Constants.FILE_EXTENSION_ASC); @@ -308,7 +222,7 @@ public class ViewKeyAdvShareFragment extends LoaderFragment implements } catch (PgpGeneralException | IOException | NoSuchAlgorithmException e) { Timber.e(e, "error processing key!"); Notify.create(activity, R.string.error_key_processing, Notify.Style.ERROR).show(); - } catch (PgpKeyNotFoundException | KeyRepository.NotFoundException e) { + } catch (KeyRepository.NotFoundException e) { Timber.e(e, "key not found!"); Notify.create(activity, R.string.error_key_not_found, Notify.Style.ERROR).show(); } @@ -316,12 +230,12 @@ public class ViewKeyAdvShareFragment extends LoaderFragment implements private void shareFingerprint(boolean toClipboard) { Activity activity = getActivity(); - if (activity == null || mFingerprint == null) { + if (activity == null || unifiedKeyInfo == null) { return; } String content; - String fingerprint = KeyFormattingUtils.convertFingerprintToHex(mFingerprint); + String fingerprint = KeyFormattingUtils.convertFingerprintToHex(unifiedKeyInfo.fingerprint()); if (!toClipboard) { content = Constants.FINGERPRINT_SCHEME + ":" + fingerprint; } else { @@ -365,141 +279,60 @@ public class ViewKeyAdvShareFragment extends LoaderFragment implements opts = options.toBundle(); } - qrCodeIntent.setData(mDataUri); - ActivityCompat.startActivity(getActivity(), qrCodeIntent, opts); + qrCodeIntent.putExtra(QrCodeViewActivity.EXTRA_MASTER_KEY_ID, unifiedKeyInfo.master_key_id()); + ActivityCompat.startActivity(requireActivity(), qrCodeIntent, opts); } @Override - public void onViewCreated(View view, Bundle savedInstanceState) { + public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - Uri dataUri = getArguments().getParcelable(ARG_DATA_URI); - if (dataUri == null) { - Timber.e("Data missing. Should be Uri of key!"); - getActivity().finish(); + ViewKeyAdvViewModel viewModel = ViewModelProviders.of(requireActivity()).get(ViewKeyAdvViewModel.class); + LiveData unifiedKeyInfoLiveData = viewModel.getUnifiedKeyInfoLiveData(requireContext()); + unifiedKeyInfoLiveData.observe(this, this::onLoadUnifiedKeyInfo); + + LiveData qrCodeLiveData = Transformations.switchMap(unifiedKeyInfoLiveData, + (unifiedKeyInfo) -> unifiedKeyInfo == null ? null : new GenericLiveData<>(getContext(), + () -> { + String fingerprintHex = KeyFormattingUtils.convertFingerprintToHex(unifiedKeyInfo.fingerprint()); + Uri uri = new Uri.Builder().scheme(Constants.FINGERPRINT_SCHEME).opaquePart(fingerprintHex).build(); + // render with minimal size + return QrCodeUtils.getQRCodeBitmap(uri, 0); + } + )); + qrCodeLiveData.observe(this, this::onLoadQrCode); + } + + public void onLoadUnifiedKeyInfo(UnifiedKeyInfo unifiedKeyInfo) { + if (unifiedKeyInfo == null) { return; } - loadData(dataUri); - } + this.unifiedKeyInfo = unifiedKeyInfo; - private void loadData(Uri dataUri) { - mDataUri = dataUri; - - // Prepare the loaders. Either re-connect with an existing ones, - // or start new ones. - getLoaderManager().initLoader(LOADER_ID_UNIFIED, null, this); - } - - static final String[] UNIFIED_PROJECTION = new String[]{ - KeyRings._ID, KeyRings.FINGERPRINT, KeyRings.USER_ID - }; - - static final int INDEX_UNIFIED_FINGERPRINT = 1; - static final int INDEX_UNIFIED_USER_ID = 2; - - public Loader onCreateLoader(int id, Bundle args) { - setContentShown(false); - switch (id) { - case LOADER_ID_UNIFIED: { - Uri baseUri = KeyRings.buildUnifiedKeyRingUri(mDataUri); - return new CursorLoader(getActivity(), baseUri, UNIFIED_PROJECTION, null, null, null); - } - - default: - return null; - } - } - - public void onLoadFinished(Loader loader, Cursor data) { - // Avoid NullPointerExceptions... - if (data == null || data.getCount() == 0) { - return; - } - // Swap the new cursor in. (The framework will take care of closing the - // old cursor once we return.) - switch (loader.getId()) { - case LOADER_ID_UNIFIED: { - if (data.moveToFirst()) { - - byte[] fingerprintBlob = data.getBlob(INDEX_UNIFIED_FINGERPRINT); - setFingerprint(fingerprintBlob); - - mUserId = data.getString(INDEX_UNIFIED_USER_ID); - - break; - } - } - - } - setContentShown(true); - } - - /** - * This is called when the last Cursor provided to onLoadFinished() above is about to be closed. - * We need to make sure we are no longer using it. - */ - public void onLoaderReset(Loader loader) { - mFingerprint = null; - mQrCodeBitmapCache = null; - } - - /** - * Load QR Code asynchronously and with a fade in animation - */ - private void setFingerprint(byte[] fingerprintBlob) { - mFingerprint = fingerprintBlob; - - final String fingerprint = KeyFormattingUtils.convertFingerprintToHex(fingerprintBlob); + final String fingerprint = KeyFormattingUtils.convertFingerprintToHex(unifiedKeyInfo.fingerprint()); mFingerprintView.setText(KeyFormattingUtils.formatFingerprint(fingerprint)); + } + private void onLoadQrCode(Bitmap qrCode) { if (mQrCodeBitmapCache != null) { return; } - AsyncTask loadTask = - new AsyncTask() { - protected Bitmap doInBackground(Void... unused) { - Uri uri = new Uri.Builder() - .scheme(Constants.FINGERPRINT_SCHEME) - .opaquePart(fingerprint) - .build(); - // render with minimal size - return QrCodeUtils.getQRCodeBitmap(uri, 0); - } + mQrCodeBitmapCache = qrCode; + if (ViewKeyAdvShareFragment.this.isAdded()) { + mQrCode.requestLayout(); - protected void onPostExecute(Bitmap qrCode) { - // cache for later, and if we are attached request re-layout - mQrCodeBitmapCache = qrCode; - - if (ViewKeyAdvShareFragment.this.isAdded()) { - mQrCode.requestLayout(); - - // simple fade-in animation - AlphaAnimation anim = new AlphaAnimation(0.0f, 1.0f); - anim.setDuration(200); - mQrCode.startAnimation(anim); - } - } - }; - - loadTask.execute(); + // simple fade-in animation + AlphaAnimation anim = new AlphaAnimation(0.0f, 1.0f); + anim.setDuration(200); + mQrCode.startAnimation(anim); + } } private void uploadToKeyserver() { - long keyId; - try { - keyId = KeyRepository.create(getContext()) - .getCachedPublicKeyRing(mDataUri) - .extractOrGetMasterKeyId(); - } catch (PgpKeyNotFoundException e) { - Timber.e(e, "key not found!"); - Notify.create(getActivity(), "key not found", Style.ERROR).show(); - return; - } Intent uploadIntent = new Intent(getActivity(), UploadKeyActivity.class); - uploadIntent.setData(mDataUri); - uploadIntent.putExtra(MultiUserIdsFragment.EXTRA_KEY_IDS, new long[]{keyId}); + uploadIntent.putExtra(UploadKeyActivity.EXTRA_KEY_IDS, new long[] { unifiedKeyInfo.master_key_id() }); startActivityForResult(uploadIntent, 0); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvSubkeysFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvSubkeysFragment.java index bff3d5695..c1984e888 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvSubkeysFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvSubkeysFragment.java @@ -18,52 +18,44 @@ package org.sufficientlysecure.keychain.ui; +import java.util.List; + +import android.arch.lifecycle.ViewModelProviders; import android.content.Intent; -import android.database.Cursor; -import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.os.Messenger; +import android.support.annotation.NonNull; +import android.support.v4.app.Fragment; import android.support.v4.app.FragmentActivity; -import android.support.v4.app.LoaderManager; -import android.support.v4.content.CursorLoader; -import android.support.v4.content.Loader; import android.view.ActionMode; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; -import android.widget.AdapterView; import android.widget.ListView; import android.widget.ViewAnimator; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.compatibility.DialogFragmentWorkaround; +import org.sufficientlysecure.keychain.model.SubKey; +import org.sufficientlysecure.keychain.model.SubKey.UnifiedKeyInfo; import org.sufficientlysecure.keychain.operations.results.EditKeyResult; import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey.SecretKeyType; -import org.sufficientlysecure.keychain.provider.KeychainContract; import org.sufficientlysecure.keychain.service.SaveKeyringParcel; import org.sufficientlysecure.keychain.service.SaveKeyringParcel.SubkeyChange; +import org.sufficientlysecure.keychain.ui.ViewKeyAdvActivity.ViewKeyAdvViewModel; import org.sufficientlysecure.keychain.ui.adapter.SubkeysAdapter; import org.sufficientlysecure.keychain.ui.adapter.SubkeysAddedAdapter; import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper; -import org.sufficientlysecure.keychain.ui.base.LoaderFragment; import org.sufficientlysecure.keychain.ui.dialog.AddSubkeyDialogFragment; import org.sufficientlysecure.keychain.ui.dialog.EditSubkeyDialogFragment; import org.sufficientlysecure.keychain.ui.dialog.EditSubkeyExpiryDialogFragment; -import timber.log.Timber; -public class ViewKeyAdvSubkeysFragment extends LoaderFragment implements - LoaderManager.LoaderCallbacks { - - public static final String ARG_DATA_URI = "data_uri"; - - private static final int LOADER_ID_UNIFIED = 0; - private static final int LOADER_ID_SUBKEYS = 1; - +public class ViewKeyAdvSubkeysFragment extends Fragment { private ListView mSubkeysList; private ListView mSubkeysAddedList; private View mSubkeysAddedLayout; @@ -74,28 +66,18 @@ public class ViewKeyAdvSubkeysFragment extends LoaderFragment implements private CryptoOperationHelper mEditKeyHelper; - private Uri mDataUri; - - private long mMasterKeyId; - private byte[] mFingerprint; - private boolean mHasSecret; private SaveKeyringParcel.Builder mEditModeSkpBuilder; + private UnifiedKeyInfo unifiedKeyInfo; @Override - public View onCreateView(LayoutInflater inflater, ViewGroup superContainer, Bundle savedInstanceState) { - View root = super.onCreateView(inflater, superContainer, savedInstanceState); - View view = inflater.inflate(R.layout.view_key_adv_subkeys_fragment, getContainer()); + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup viewGroup, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.view_key_adv_subkeys_fragment, viewGroup, false); mSubkeysList = view.findViewById(R.id.view_key_subkeys); mSubkeysAddedList = view.findViewById(R.id.view_key_subkeys_added); mSubkeysAddedLayout = view.findViewById(R.id.view_key_subkeys_add_layout); - mSubkeysList.setOnItemClickListener(new AdapterView.OnItemClickListener() { - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - editSubkey(position); - } - }); + mSubkeysList.setOnItemClickListener((parent, view1, position, id) -> editSubkey(position)); View footer = new View(getActivity()); int spacing = (int) android.util.TypedValue.applyDimension( @@ -109,30 +91,37 @@ public class ViewKeyAdvSubkeysFragment extends LoaderFragment implements mSubkeysAddedList.addFooterView(footer, null, false); mSubkeyAddFabLayout = view.findViewById(R.id.view_key_subkey_fab_layout); - view.findViewById(R.id.view_key_subkey_fab).setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - addSubkey(); - } - }); + view.findViewById(R.id.view_key_subkey_fab).setOnClickListener(v -> addSubkey()); setHasOptionsMenu(true); - return root; + return view; } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - Uri dataUri = getArguments().getParcelable(ARG_DATA_URI); - if (dataUri == null) { - Timber.e("Data missing. Should be Uri of key!"); - getActivity().finish(); + // Create an empty adapter we will use to display the loaded data. + mSubkeysAdapter = new SubkeysAdapter(requireContext()); + mSubkeysList.setAdapter(mSubkeysAdapter); + + ViewKeyAdvViewModel viewModel = ViewModelProviders.of(requireActivity()).get(ViewKeyAdvViewModel.class); + viewModel.getUnifiedKeyInfoLiveData(requireContext()).observe(this, this::onLoadFinished); + viewModel.getSubkeyLiveData(requireContext()).observe(this, this::onLoadSubKeys); + } + + public void onLoadFinished(UnifiedKeyInfo unifiedKeyInfo) { + // Avoid NullPointerExceptions, if we get an empty result set. + if (unifiedKeyInfo == null) { return; } - loadData(dataUri); + this.unifiedKeyInfo = unifiedKeyInfo; + } + + private void onLoadSubKeys(List subKeys) { + mSubkeysAdapter.setData(subKeys); } @Override @@ -144,89 +133,6 @@ public class ViewKeyAdvSubkeysFragment extends LoaderFragment implements super.onActivityResult(requestCode, resultCode, data); } - private void loadData(Uri dataUri) { - mDataUri = dataUri; - - // Create an empty adapter we will use to display the loaded data. - mSubkeysAdapter = new SubkeysAdapter(getActivity(), null, 0); - mSubkeysList.setAdapter(mSubkeysAdapter); - - // Prepare the loaders. Either re-connect with an existing ones, - // or start new ones. - getLoaderManager().initLoader(LOADER_ID_UNIFIED, null, this); - getLoaderManager().initLoader(LOADER_ID_SUBKEYS, null, this); - } - - // These are the rows that we will retrieve. - static final String[] PROJECTION = new String[]{ - KeychainContract.KeyRings._ID, - KeychainContract.KeyRings.MASTER_KEY_ID, - KeychainContract.KeyRings.HAS_ANY_SECRET, - KeychainContract.KeyRings.FINGERPRINT, - }; - - static final int INDEX_MASTER_KEY_ID = 1; - static final int INDEX_HAS_ANY_SECRET = 2; - static final int INDEX_FINGERPRINT = 3; - - @Override - public Loader onCreateLoader(int id, Bundle args) { - switch (id) { - case LOADER_ID_UNIFIED: { - Uri baseUri = KeychainContract.KeyRings.buildUnifiedKeyRingUri(mDataUri); - return new CursorLoader(getActivity(), baseUri, - PROJECTION, null, null, null); - } - - case LOADER_ID_SUBKEYS: { - setContentShown(false); - - Uri subkeysUri = KeychainContract.Keys.buildKeysUri(mDataUri); - return new CursorLoader(getActivity(), subkeysUri, - SubkeysAdapter.SUBKEYS_PROJECTION, null, null, null); - } - - default: - return null; - } - } - - public void onLoadFinished(Loader loader, Cursor data) { - // Avoid NullPointerExceptions, if we get an empty result set. - if (data.getCount() == 0) { - return; - } - - switch (loader.getId()) { - case LOADER_ID_UNIFIED: { - data.moveToFirst(); - - mMasterKeyId = data.getLong(INDEX_MASTER_KEY_ID); - mHasSecret = data.getInt(INDEX_HAS_ANY_SECRET) != 0; - mFingerprint = data.getBlob(INDEX_FINGERPRINT); - break; - } - case LOADER_ID_SUBKEYS: { - // Swap the new cursor in. (The framework will take care of closing the - // old cursor once we return.) - mSubkeysAdapter.swapCursor(data); - - // TODO: maybe show not before both are loaded! - setContentShown(true); - break; - } - } - - } - - /** - * This is called when the last Cursor provided to onLoadFinished() above is about to be closed. - * We need to make sure we are no longer using it. - */ - public void onLoaderReset(Loader loader) { - mSubkeysAdapter.swapCursor(null); - } - @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { @@ -247,7 +153,7 @@ public class ViewKeyAdvSubkeysFragment extends LoaderFragment implements @Override public boolean onCreateActionMode(ActionMode mode, Menu menu) { - mEditModeSkpBuilder = SaveKeyringParcel.buildChangeKeyringParcel(mMasterKeyId, mFingerprint); + mEditModeSkpBuilder = SaveKeyringParcel.buildChangeKeyringParcel(unifiedKeyInfo.master_key_id(), unifiedKeyInfo.fingerprint()); mSubkeysAddedAdapter = new SubkeysAddedAdapter( getActivity(), mEditModeSkpBuilder.getMutableAddSubKeys(), false); @@ -256,7 +162,6 @@ public class ViewKeyAdvSubkeysFragment extends LoaderFragment implements mSubkeyAddFabLayout.setDisplayedChild(1); mSubkeysAdapter.setEditMode(mEditModeSkpBuilder); - getLoaderManager().restartLoader(LOADER_ID_SUBKEYS, null, ViewKeyAdvSubkeysFragment.this); mode.setTitle(R.string.title_edit_subkeys); mode.getMenuInflater().inflate(R.menu.action_edit_uids, menu); @@ -281,7 +186,6 @@ public class ViewKeyAdvSubkeysFragment extends LoaderFragment implements mSubkeysAdapter.setEditMode(null); mSubkeysAddedLayout.setVisibility(View.GONE); mSubkeyAddFabLayout.setDisplayedChild(0); - getLoaderManager().restartLoader(LOADER_ID_SUBKEYS, null, ViewKeyAdvSubkeysFragment.this); } }); } @@ -297,19 +201,12 @@ public class ViewKeyAdvSubkeysFragment extends LoaderFragment implements AddSubkeyDialogFragment addSubkeyDialogFragment = AddSubkeyDialogFragment.newInstance(willBeMasterKey); addSubkeyDialogFragment - .setOnAlgorithmSelectedListener( - new AddSubkeyDialogFragment.OnAlgorithmSelectedListener() { - @Override - public void onAlgorithmSelected(SaveKeyringParcel.SubkeyAdd newSubkey) { - mSubkeysAddedAdapter.add(newSubkey); - } - } - ); - addSubkeyDialogFragment.show(getActivity().getSupportFragmentManager(), "addSubkeyDialog"); + .setOnAlgorithmSelectedListener(newSubkey -> mSubkeysAddedAdapter.add(newSubkey)); + addSubkeyDialogFragment.show(requireFragmentManager(), "addSubkeyDialog"); } private void editSubkey(final int position) { - final long keyId = mSubkeysAdapter.getKeyId(position); + final SubKey subKey = mSubkeysAdapter.getItem(position); Handler returnHandler = new Handler() { @Override @@ -320,49 +217,47 @@ public class ViewKeyAdvSubkeysFragment extends LoaderFragment implements break; case EditSubkeyDialogFragment.MESSAGE_REVOKE: // toggle - if (mEditModeSkpBuilder.getMutableRevokeSubKeys().contains(keyId)) { - mEditModeSkpBuilder.removeRevokeSubkey(keyId); + if (mEditModeSkpBuilder.getMutableRevokeSubKeys().contains(subKey.key_id())) { + mEditModeSkpBuilder.removeRevokeSubkey(subKey.key_id()); } else { - mEditModeSkpBuilder.addRevokeSubkey(keyId); + mEditModeSkpBuilder.addRevokeSubkey(subKey.key_id()); } break; case EditSubkeyDialogFragment.MESSAGE_STRIP: { - SecretKeyType secretKeyType = mSubkeysAdapter.getSecretKeyType(position); - if (secretKeyType == SecretKeyType.GNU_DUMMY) { + if (subKey.has_secret() == SecretKeyType.GNU_DUMMY) { // Key is already stripped; this is a no-op. break; } - SubkeyChange change = mEditModeSkpBuilder.getSubkeyChange(keyId); + SubkeyChange change = mEditModeSkpBuilder.getSubkeyChange(subKey.key_id()); if (change == null || !change.getDummyStrip()) { - mEditModeSkpBuilder.addOrReplaceSubkeyChange(SubkeyChange.createStripChange(keyId)); + mEditModeSkpBuilder.addOrReplaceSubkeyChange(SubkeyChange.createStripChange(subKey.key_id())); } else { mEditModeSkpBuilder.removeSubkeyChange(change); } break; } } - getLoaderManager().getLoader(LOADER_ID_SUBKEYS).forceLoad(); + mSubkeysAdapter.notifyDataSetChanged(); } }; // Create a new Messenger for the communication back final Messenger messenger = new Messenger(returnHandler); - DialogFragmentWorkaround.INTERFACE.runnableRunDelayed(new Runnable() { - public void run() { - EditSubkeyDialogFragment dialogFragment = - EditSubkeyDialogFragment.newInstance(messenger); + DialogFragmentWorkaround.INTERFACE.runnableRunDelayed(() -> { + EditSubkeyDialogFragment dialogFragment = + EditSubkeyDialogFragment.newInstance(messenger); - dialogFragment.show(getActivity().getSupportFragmentManager(), "editSubkeyDialog"); - } + dialogFragment.show(requireFragmentManager(), "editSubkeyDialog"); }); } private void editSubkeyExpiry(final int position) { - final long keyId = mSubkeysAdapter.getKeyId(position); - final Long creationDate = mSubkeysAdapter.getCreationDate(position); - final Long expiryDate = mSubkeysAdapter.getExpiryDate(position); + SubKey subKey = mSubkeysAdapter.getItem(position); + final long keyId = subKey.key_id(); + final Long creationDate = subKey.creation(); + final Long expiryDate = subKey.expiry(); Handler returnHandler = new Handler() { @Override @@ -375,20 +270,18 @@ public class ViewKeyAdvSubkeysFragment extends LoaderFragment implements SubkeyChange.createFlagsOrExpiryChange(keyId, null, expiry)); break; } - getLoaderManager().getLoader(LOADER_ID_SUBKEYS).forceLoad(); + mSubkeysAdapter.notifyDataSetChanged(); } }; // Create a new Messenger for the communication back final Messenger messenger = new Messenger(returnHandler); - DialogFragmentWorkaround.INTERFACE.runnableRunDelayed(new Runnable() { - public void run() { - EditSubkeyExpiryDialogFragment dialogFragment = - EditSubkeyExpiryDialogFragment.newInstance(messenger, creationDate, expiryDate); + DialogFragmentWorkaround.INTERFACE.runnableRunDelayed(() -> { + EditSubkeyExpiryDialogFragment dialogFragment = + EditSubkeyExpiryDialogFragment.newInstance(messenger, creationDate, expiryDate); - dialogFragment.show(getActivity().getSupportFragmentManager(), "editSubkeyExpiryDialog"); - } + dialogFragment.show(requireFragmentManager(), "editSubkeyExpiryDialog"); }); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvUserIdsFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvUserIdsFragment.java index a0811e8b5..0877d9263 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvUserIdsFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvUserIdsFragment.java @@ -17,52 +17,46 @@ package org.sufficientlysecure.keychain.ui; + +import java.util.List; + +import android.arch.lifecycle.ViewModelProviders; import android.content.Intent; -import android.database.Cursor; -import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.os.Messenger; +import android.support.annotation.NonNull; +import android.support.v4.app.Fragment; import android.support.v4.app.FragmentActivity; -import android.support.v4.app.LoaderManager; -import android.support.v4.content.CursorLoader; -import android.support.v4.content.Loader; import android.view.ActionMode; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; -import android.widget.AdapterView; import android.widget.ListView; import android.widget.ViewAnimator; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.compatibility.DialogFragmentWorkaround; +import org.sufficientlysecure.keychain.model.SubKey.UnifiedKeyInfo; +import org.sufficientlysecure.keychain.model.UserPacket.UserId; import org.sufficientlysecure.keychain.operations.results.EditKeyResult; -import org.sufficientlysecure.keychain.provider.KeychainContract; -import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets; +import org.sufficientlysecure.keychain.pgp.CanonicalizedKeyRing.VerificationStatus; +import org.sufficientlysecure.keychain.provider.KeychainContract.Certs; import org.sufficientlysecure.keychain.service.SaveKeyringParcel; +import org.sufficientlysecure.keychain.ui.ViewKeyAdvActivity.ViewKeyAdvViewModel; import org.sufficientlysecure.keychain.ui.adapter.UserIdsAdapter; import org.sufficientlysecure.keychain.ui.adapter.UserIdsAddedAdapter; import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper; -import org.sufficientlysecure.keychain.ui.base.LoaderFragment; import org.sufficientlysecure.keychain.ui.dialog.AddUserIdDialogFragment; import org.sufficientlysecure.keychain.ui.dialog.EditUserIdDialogFragment; import org.sufficientlysecure.keychain.ui.dialog.SetPassphraseDialogFragment; import org.sufficientlysecure.keychain.ui.dialog.UserIdInfoDialogFragment; -import timber.log.Timber; -public class ViewKeyAdvUserIdsFragment extends LoaderFragment implements - LoaderManager.LoaderCallbacks { - - public static final String ARG_DATA_URI = "uri"; - - private static final int LOADER_ID_UNIFIED = 0; - private static final int LOADER_ID_USER_IDS = 1; - +public class ViewKeyAdvUserIdsFragment extends Fragment { private ListView mUserIds; private ListView mUserIdsAddedList; private View mUserIdsAddedLayout; @@ -73,28 +67,18 @@ public class ViewKeyAdvUserIdsFragment extends LoaderFragment implements private CryptoOperationHelper mEditKeyHelper; - private Uri mDataUri; - - private long mMasterKeyId; - private byte[] mFingerprint; - private boolean mHasSecret; private SaveKeyringParcel.Builder mSkpBuilder; + private UnifiedKeyInfo unifiedKeyInfo; @Override - public View onCreateView(LayoutInflater inflater, ViewGroup superContainer, Bundle savedInstanceState) { - View root = super.onCreateView(inflater, superContainer, savedInstanceState); - View view = inflater.inflate(R.layout.view_key_adv_user_ids_fragment, getContainer()); + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup viewGroup, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.view_key_adv_user_ids_fragment, viewGroup, false); mUserIds = view.findViewById(R.id.view_key_user_ids); mUserIdsAddedList = view.findViewById(R.id.view_key_user_ids_added); mUserIdsAddedLayout = view.findViewById(R.id.view_key_user_ids_add_layout); - mUserIds.setOnItemClickListener(new AdapterView.OnItemClickListener() { - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - showOrEditUserIdInfo(position); - } - }); + mUserIds.setOnItemClickListener((parent, view1, position, id) -> showOrEditUserIdInfo(position)); View footer = new View(getActivity()); int spacing = (int) android.util.TypedValue.applyDimension( @@ -108,16 +92,11 @@ public class ViewKeyAdvUserIdsFragment extends LoaderFragment implements mUserIdsAddedList.addFooterView(footer, null, false); mUserIdAddFabLayout = view.findViewById(R.id.view_key_subkey_fab_layout); - view.findViewById(R.id.view_key_subkey_fab).setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - addUserId(); - } - }); + view.findViewById(R.id.view_key_subkey_fab).setOnClickListener(v -> addUserId()); setHasOptionsMenu(true); - return root; + return view; } private void showOrEditUserIdInfo(final int position) { @@ -160,34 +139,30 @@ public class ViewKeyAdvUserIdsFragment extends LoaderFragment implements } break; } - getLoaderManager().getLoader(LOADER_ID_USER_IDS).forceLoad(); + mUserIdsAdapter.notifyDataSetChanged(); } }; // Create a new Messenger for the communication back final Messenger messenger = new Messenger(returnHandler); - DialogFragmentWorkaround.INTERFACE.runnableRunDelayed(new Runnable() { - public void run() { - EditUserIdDialogFragment dialogFragment = - EditUserIdDialogFragment.newInstance(messenger, isRevoked, isRevokedPending); - dialogFragment.show(getActivity().getSupportFragmentManager(), "editUserIdDialog"); - } + DialogFragmentWorkaround.INTERFACE.runnableRunDelayed(() -> { + EditUserIdDialogFragment dialogFragment = + EditUserIdDialogFragment.newInstance(messenger, isRevoked, isRevokedPending); + dialogFragment.show(requireFragmentManager(), "editUserIdDialog"); }); } private void showUserIdInfo(final int position) { final boolean isRevoked = mUserIdsAdapter.getIsRevoked(position); - final int isVerified = mUserIdsAdapter.getIsVerified(position); + final boolean isVerified = mUserIdsAdapter.getVerificationStatus(position) == VerificationStatus.VERIFIED_SECRET; - DialogFragmentWorkaround.INTERFACE.runnableRunDelayed(new Runnable() { - public void run() { - UserIdInfoDialogFragment dialogFragment = - UserIdInfoDialogFragment.newInstance(isRevoked, isVerified); + DialogFragmentWorkaround.INTERFACE.runnableRunDelayed(() -> { + UserIdInfoDialogFragment dialogFragment = + UserIdInfoDialogFragment.newInstance(isRevoked, isVerified); - dialogFragment.show(getActivity().getSupportFragmentManager(), "userIdInfoDialog"); - } + dialogFragment.show(requireFragmentManager(), "userIdInfoDialog"); }); } @@ -211,21 +186,30 @@ public class ViewKeyAdvUserIdsFragment extends LoaderFragment implements // pre-fill out primary name AddUserIdDialogFragment addUserIdDialog = AddUserIdDialogFragment.newInstance(messenger, ""); - addUserIdDialog.show(getActivity().getSupportFragmentManager(), "addUserIdDialog"); + addUserIdDialog.show(requireFragmentManager(), "addUserIdDialog"); } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - Uri dataUri = getArguments().getParcelable(ARG_DATA_URI); - if (dataUri == null) { - Timber.e("Data missing. Should be Uri of key!"); - getActivity().finish(); + mUserIdsAdapter = new UserIdsAdapter(getActivity(), false); + mUserIds.setAdapter(mUserIdsAdapter); + + ViewKeyAdvViewModel viewModel = ViewModelProviders.of(requireActivity()).get(ViewKeyAdvViewModel.class); + viewModel.getUnifiedKeyInfoLiveData(requireContext()).observe(this, this::onLoadUnifiedKeyInfo); + viewModel.getUserIdLiveData(requireContext()).observe(this, this::onLoadUserIds); + } + + public void onLoadUnifiedKeyInfo(UnifiedKeyInfo unifiedKeyInfo) { + if (unifiedKeyInfo == null) { return; } + this.unifiedKeyInfo = unifiedKeyInfo; + } - loadData(dataUri); + private void onLoadUserIds(List userIds) { + mUserIdsAdapter.setData(userIds); } @Override @@ -237,90 +221,6 @@ public class ViewKeyAdvUserIdsFragment extends LoaderFragment implements super.onActivityResult(requestCode, resultCode, data); } - private void loadData(Uri dataUri) { - mDataUri = dataUri; - - Timber.i("dataUri: " + mDataUri); - - mUserIdsAdapter = new UserIdsAdapter(getActivity(), null, 0); - mUserIds.setAdapter(mUserIdsAdapter); - - // Prepare the loaders. Either re-connect with an existing ones, - // or start new ones. - getLoaderManager().initLoader(LOADER_ID_UNIFIED, null, this); - getLoaderManager().initLoader(LOADER_ID_USER_IDS, null, this); - } - - // These are the rows that we will retrieve. - static final String[] PROJECTION = new String[]{ - KeychainContract.KeyRings._ID, - KeychainContract.KeyRings.MASTER_KEY_ID, - KeychainContract.KeyRings.HAS_ANY_SECRET, - KeychainContract.KeyRings.FINGERPRINT, - }; - - static final int INDEX_MASTER_KEY_ID = 1; - static final int INDEX_HAS_ANY_SECRET = 2; - static final int INDEX_FINGERPRINT = 3; - - public Loader onCreateLoader(int id, Bundle args) { - switch (id) { - case LOADER_ID_UNIFIED: { - Uri baseUri = KeychainContract.KeyRings.buildUnifiedKeyRingUri(mDataUri); - return new CursorLoader(getActivity(), baseUri, - PROJECTION, null, null, null); - } - - case LOADER_ID_USER_IDS: { - setContentShown(false); - - Uri userIdUri = UserPackets.buildUserIdsUri(mDataUri); - return new CursorLoader(getActivity(), userIdUri, - UserIdsAdapter.USER_PACKETS_PROJECTION, null, null, null); - } - - default: - return null; - } - } - - public void onLoadFinished(Loader loader, Cursor data) { - // Avoid NullPointerExceptions, if we get an empty result set. - if (data.getCount() == 0) { - return; - } - - switch (loader.getId()) { - case LOADER_ID_UNIFIED: { - data.moveToFirst(); - - mMasterKeyId = data.getLong(INDEX_MASTER_KEY_ID); - mHasSecret = data.getInt(INDEX_HAS_ANY_SECRET) != 0; - mFingerprint = data.getBlob(INDEX_FINGERPRINT); - break; - } - case LOADER_ID_USER_IDS: { - // Swap the new cursor in. (The framework will take care of closing the - // old cursor once we return.) - mUserIdsAdapter.swapCursor(data); - - setContentShown(true); - break; - } - } - } - - /** - * This is called when the last Cursor provided to onLoadFinished() above is about to be closed. - * We need to make sure we are no longer using it. - */ - public void onLoaderReset(Loader loader) { - if (loader.getId() != LOADER_ID_USER_IDS) { - return; - } - mUserIdsAdapter.swapCursor(null); - } - @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { @@ -340,8 +240,8 @@ public class ViewKeyAdvUserIdsFragment extends LoaderFragment implements activity.startActionMode(new ActionMode.Callback() { @Override public boolean onCreateActionMode(ActionMode mode, Menu menu) { - - mSkpBuilder = SaveKeyringParcel.buildChangeKeyringParcel(mMasterKeyId, mFingerprint); + mSkpBuilder = SaveKeyringParcel.buildChangeKeyringParcel( + unifiedKeyInfo.master_key_id(), unifiedKeyInfo.fingerprint()); mUserIdsAddedAdapter = new UserIdsAddedAdapter(getActivity(), mSkpBuilder.getMutableAddUserIds(), false); @@ -350,7 +250,7 @@ public class ViewKeyAdvUserIdsFragment extends LoaderFragment implements mUserIdAddFabLayout.setDisplayedChild(1); mUserIdsAdapter.setEditMode(mSkpBuilder); - getLoaderManager().restartLoader(LOADER_ID_USER_IDS, null, ViewKeyAdvUserIdsFragment.this); + mUserIdsAdapter.notifyDataSetChanged(); mode.setTitle(R.string.title_edit_identities); mode.getMenuInflater().inflate(R.menu.action_edit_uids, menu); @@ -375,7 +275,7 @@ public class ViewKeyAdvUserIdsFragment extends LoaderFragment implements mUserIdsAdapter.setEditMode(null); mUserIdsAddedLayout.setVisibility(View.GONE); mUserIdAddFabLayout.setDisplayedChild(0); - getLoaderManager().restartLoader(LOADER_ID_USER_IDS, null, ViewKeyAdvUserIdsFragment.this); + mUserIdsAdapter.notifyDataSetChanged(); } }); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyKeybaseFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyKeybaseFragment.java index f483d09c9..70fb46e70 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyKeybaseFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyKeybaseFragment.java @@ -17,15 +17,18 @@ package org.sufficientlysecure.keychain.ui; + +import java.util.ArrayList; +import java.util.Hashtable; +import java.util.List; + +import android.arch.lifecycle.ViewModelProviders; import android.content.Intent; -import android.database.Cursor; import android.graphics.Typeface; -import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; -import android.support.v4.app.LoaderManager; -import android.support.v4.content.CursorLoader; -import android.support.v4.content.Loader; +import android.support.annotation.NonNull; +import android.support.v4.app.Fragment; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.method.LinkMovementMethod; @@ -43,66 +46,43 @@ import com.textuality.keybase.lib.KeybaseException; import com.textuality.keybase.lib.KeybaseQuery; import com.textuality.keybase.lib.Proof; import com.textuality.keybase.lib.User; - import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.model.SubKey.UnifiedKeyInfo; +import org.sufficientlysecure.keychain.network.OkHttpKeybaseClient; +import org.sufficientlysecure.keychain.network.orbot.OrbotHelper; import org.sufficientlysecure.keychain.operations.results.KeybaseVerificationResult; -import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; import org.sufficientlysecure.keychain.service.KeybaseVerificationParcel; import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper; -import org.sufficientlysecure.keychain.ui.base.LoaderFragment; +import org.sufficientlysecure.keychain.ui.keyview.UnifiedKeyInfoViewModel; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; -import org.sufficientlysecure.keychain.network.OkHttpKeybaseClient; import org.sufficientlysecure.keychain.util.ParcelableProxy; import org.sufficientlysecure.keychain.util.Preferences; -import org.sufficientlysecure.keychain.network.orbot.OrbotHelper; -import timber.log.Timber; -import java.util.ArrayList; -import java.util.Hashtable; -import java.util.List; - -public class ViewKeyKeybaseFragment extends LoaderFragment implements - LoaderManager.LoaderCallbacks, +public class ViewKeyKeybaseFragment extends Fragment implements CryptoOperationHelper.Callback { - - public static final String ARG_DATA_URI = "uri"; - private TextView mReportHeader; private TableLayout mProofListing; private LayoutInflater mInflater; private View mProofVerifyHeader; private TextView mProofVerifyDetail; - private static final int LOADER_ID_DATABASE = 1; - - // for retrieving the key we’re working on - private Uri mDataUri; - private Proof mProof; // for CryptoOperationHelper,Callback private String mKeybaseProof; private String mKeybaseFingerprint; - private CryptoOperationHelper - mKeybaseOpHelper; + private CryptoOperationHelper mKeybaseOpHelper; /** * Creates new instance of this fragment */ - public static ViewKeyKeybaseFragment newInstance(Uri dataUri) { - ViewKeyKeybaseFragment frag = new ViewKeyKeybaseFragment(); - Bundle args = new Bundle(); - args.putParcelable(ARG_DATA_URI, dataUri); - - frag.setArguments(args); - - return frag; + public static ViewKeyKeybaseFragment newInstance() { + return new ViewKeyKeybaseFragment(); } @Override - public View onCreateView(LayoutInflater inflater, ViewGroup superContainer, Bundle savedInstanceState) { - View root = super.onCreateView(inflater, superContainer, savedInstanceState); - View view = inflater.inflate(R.layout.view_key_adv_keybase_fragment, getContainer()); + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup viewGroup, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.view_key_adv_keybase_fragment, viewGroup, false); mInflater = inflater; mReportHeader = view.findViewById(R.id.view_key_trust_cloud_narrative); @@ -114,71 +94,23 @@ public class ViewKeyKeybaseFragment extends LoaderFragment implements mProofVerifyHeader.setVisibility(View.GONE); mProofVerifyDetail.setVisibility(View.GONE); - return root; + return view; } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - Uri dataUri = getArguments().getParcelable(ARG_DATA_URI); - if (dataUri == null) { - Timber.e("Data missing. Should be Uri of key!"); - getActivity().finish(); - return; - } - mDataUri = dataUri; - - // retrieve the key from the database - getLoaderManager().initLoader(LOADER_ID_DATABASE, null, this); + UnifiedKeyInfoViewModel viewKeyViewModel = ViewModelProviders.of(requireActivity()).get(UnifiedKeyInfoViewModel.class); + viewKeyViewModel.getUnifiedKeyInfoLiveData(requireContext()).observe(this, this::onLoadUnifiedKeyInfo); } - static final String[] TRUST_PROJECTION = new String[]{ - KeyRings._ID, KeyRings.FINGERPRINT, KeyRings.IS_REVOKED, KeyRings.IS_EXPIRED, - KeyRings.HAS_ANY_SECRET, KeyRings.VERIFIED - }; - static final int INDEX_TRUST_FINGERPRINT = 1; - static final int INDEX_TRUST_IS_REVOKED = 2; - static final int INDEX_TRUST_IS_EXPIRED = 3; - static final int INDEX_UNIFIED_HAS_ANY_SECRET = 4; - static final int INDEX_VERIFIED = 5; - - public Loader onCreateLoader(int id, Bundle args) { - setContentShown(false); - - switch (id) { - case LOADER_ID_DATABASE: { - Uri baseUri = KeyRings.buildUnifiedKeyRingUri(mDataUri); - return new CursorLoader(getActivity(), baseUri, TRUST_PROJECTION, null, null, null); - } - // decided to just use an AsyncTask for keybase, but maybe later - default: - return null; - } - } - - public void onLoadFinished(Loader loader, Cursor data) { - /* TODO better error handling? May cause problems when a key is deleted, - * because the notification triggers faster than the activity closes. - */ - // Avoid NullPointerExceptions... - if (data.getCount() == 0) { + private void onLoadUnifiedKeyInfo(UnifiedKeyInfo unifiedKeyInfo) { + if (unifiedKeyInfo == null) { return; } - - boolean nothingSpecial = true; - - // Swap the new cursor in. (The framework will take care of closing the - // old cursor once we return.) - if (data.moveToFirst()) { - - final byte[] fp = data.getBlob(INDEX_TRUST_FINGERPRINT); - final String fingerprint = KeyFormattingUtils.convertFingerprintToHex(fp); - - startSearch(fingerprint); - } - - setContentShown(true); + String fingerprint = KeyFormattingUtils.convertFingerprintToHex(unifiedKeyInfo.fingerprint()); + startSearch(fingerprint); } private void startSearch(final String fingerprint) { @@ -208,19 +140,11 @@ public class ViewKeyKeybaseFragment extends LoaderFragment implements } } - /** - * This is called when the last Cursor provided to onLoadFinished() above is about to be closed. - * We need to make sure we are no longer using it. - */ - public void onLoaderReset(Loader loader) { - // no-op in this case I think - } - class ResultPage { String mHeader; final List mProofs; - public ResultPage(String header, List proofs) { + ResultPage(String header, List proofs) { mHeader = header; mProofs = proofs; } @@ -232,7 +156,7 @@ public class ViewKeyKeybaseFragment extends LoaderFragment implements private class DescribeKey extends AsyncTask { ParcelableProxy mParcelableProxy; - public DescribeKey(ParcelableProxy parcelableProxy) { + DescribeKey(ParcelableProxy parcelableProxy) { mParcelableProxy = parcelableProxy; } @@ -240,8 +164,8 @@ public class ViewKeyKeybaseFragment extends LoaderFragment implements protected ResultPage doInBackground(String... args) { String fingerprint = args[0]; - final ArrayList proofList = new ArrayList(); - final Hashtable> proofs = new Hashtable>(); + final ArrayList proofList = new ArrayList<>(); + final Hashtable> proofs = new Hashtable<>(); try { KeybaseQuery keybaseQuery = new KeybaseQuery(new OkHttpKeybaseClient()); keybaseQuery.setProxy(mParcelableProxy.getProxy()); @@ -293,7 +217,7 @@ public class ViewKeyKeybaseFragment extends LoaderFragment implements return ssb; } - private SpannableStringBuilder appendProofLinks(SpannableStringBuilder ssb, final String fingerprint, final Proof proof) throws KeybaseException { + private void appendProofLinks(SpannableStringBuilder ssb, final String fingerprint, final Proof proof) throws KeybaseException { int startAt = ssb.length(); String handle = proof.getHandle(); ssb.append(handle); @@ -315,7 +239,6 @@ public class ViewKeyKeybaseFragment extends LoaderFragment implements ssb.setSpan(clicker, startAt, startAt + verify.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); ssb.append("]"); } - return ssb; } @Override @@ -327,7 +250,7 @@ public class ViewKeyKeybaseFragment extends LoaderFragment implements } if (result.mProofs.isEmpty()) { - result.mHeader = getActivity().getString(R.string.key_trust_no_cloud_evidence); + result.mHeader = requireActivity().getString(R.string.key_trust_no_cloud_evidence); } mReportHeader.setVisibility(View.VISIBLE); @@ -382,10 +305,10 @@ public class ViewKeyKeybaseFragment extends LoaderFragment implements } } - private void appendIfOK(Hashtable> table, Integer proofType, Proof proof) throws KeybaseException { + private void appendIfOK(Hashtable> table, Integer proofType, Proof proof) { ArrayList list = table.get(proofType); if (list == null) { - list = new ArrayList(); + list = new ArrayList<>(); table.put(proofType, list); } list.add(proof); @@ -512,7 +435,6 @@ public class ViewKeyKeybaseFragment extends LoaderFragment implements @Override public void onCryptoOperationError(KeybaseVerificationResult result) { - result.createNotify(getActivity()).show(); SpannableStringBuilder ssb = new SpannableStringBuilder(); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/CertSectionedListAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/CertSectionedListAdapter.java deleted file mode 100644 index aa4551e74..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/CertSectionedListAdapter.java +++ /dev/null @@ -1,259 +0,0 @@ -/* - * Copyright (C) 2017 Schürmann & Breitmoser GbR - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sufficientlysecure.keychain.ui.adapter; - -import android.content.Context; -import android.database.Cursor; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.TextView; - - -import org.openintents.openpgp.util.OpenPgpUtils; -import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.pgp.KeyRing; -import org.sufficientlysecure.keychain.pgp.WrappedSignature; -import org.sufficientlysecure.keychain.provider.KeychainContract; -import org.sufficientlysecure.keychain.provider.KeychainDatabase; -import org.sufficientlysecure.keychain.ui.adapter.CertSectionedListAdapter.CertCursor; -import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; -import org.sufficientlysecure.keychain.ui.util.adapter.CursorAdapter; -import org.sufficientlysecure.keychain.ui.util.adapter.SectionCursorAdapter; - -import java.util.ArrayList; -import java.util.Arrays; - -public class CertSectionedListAdapter extends SectionCursorAdapter { - - private CertListListener mListener; - - public CertSectionedListAdapter(Context context, CertCursor cursor) { - super(context, CertCursor.wrap(cursor), 0); - } - - public void setCertListListener(CertListListener listener) { - mListener = listener; - } - - @Override - public long getIdFromCursor(CertCursor cursor) { - return cursor.getKeyId(); - } - - @Override - protected String getSectionFromCursor(CertCursor cursor) throws IllegalStateException { - return cursor.getRawSignerUserId(); - } - - @Override - protected CertSectionViewHolder onCreateSectionViewHolder(ViewGroup parent, int viewType) { - return new CertSectionViewHolder(LayoutInflater.from(parent.getContext()) - .inflate(R.layout.view_key_adv_certs_header, parent, false)); - } - - @Override - protected CertItemViewHolder onCreateItemViewHolder(ViewGroup parent, int viewType) { - return new CertItemViewHolder(LayoutInflater.from(parent.getContext()) - .inflate(R.layout.view_key_adv_certs_item, parent, false)); - } - - @Override - protected void onBindSectionViewHolder(CertSectionViewHolder holder, String section) { - holder.bind(section); - } - - @Override - protected void onBindItemViewHolder(CertItemViewHolder holder, CertCursor cursor) { - holder.bind(cursor); - } - - class CertItemViewHolder extends SectionCursorAdapter.ViewHolder - implements View.OnClickListener { - - private TextView mSignerKeyId; - private TextView mSignerName; - private TextView mSignStatus; - - public CertItemViewHolder(View itemView) { - super(itemView); - - itemView.setClickable(true); - itemView.setOnClickListener(this); - - mSignerName = itemView.findViewById(R.id.signerName); - mSignStatus = itemView.findViewById(R.id.signStatus); - mSignerKeyId = itemView.findViewById(R.id.signerKeyId); - } - - public void bind(CertCursor cursor) { - String signerKeyId = KeyFormattingUtils.beautifyKeyIdWithPrefix( - cursor.getCertifierKeyId()); - - OpenPgpUtils.UserId userId = cursor.getSignerUserId(); - if (userId.name != null) { - mSignerName.setText(userId.name); - } else { - mSignerName.setText(R.string.user_id_no_name); - } - - mSignerKeyId.setText(signerKeyId); - switch (cursor.getType()) { - case WrappedSignature.DEFAULT_CERTIFICATION: // 0x10 - mSignStatus.setText(R.string.cert_default); - break; - case WrappedSignature.NO_CERTIFICATION: // 0x11 - mSignStatus.setText(R.string.cert_none); - break; - case WrappedSignature.CASUAL_CERTIFICATION: // 0x12 - mSignStatus.setText(R.string.cert_casual); - break; - case WrappedSignature.POSITIVE_CERTIFICATION: // 0x13 - mSignStatus.setText(R.string.cert_positive); - break; - case WrappedSignature.CERTIFICATION_REVOCATION: // 0x30 - mSignStatus.setText(R.string.cert_revoke); - break; - } - } - - @Override - public void onClick(View v) { - if(mListener != null) { - int index = getCursorPositionWithoutSections(getAdapterPosition()); - if (moveCursor(index)) { - CertCursor cursor = getCursor(); - mListener.onClick( - cursor.getKeyId(), - cursor.getCertifierKeyId(), - cursor.getRank() - ); - } - } - } - } - - static class CertSectionViewHolder extends SectionCursorAdapter.ViewHolder { - private TextView mHeaderText; - - public CertSectionViewHolder(View itemView) { - super(itemView); - mHeaderText = itemView.findViewById(R.id.stickylist_header_text); - } - - public void bind(String text) { - mHeaderText.setText(text); - } - } - - public static class CertCursor extends CursorAdapter.SimpleCursor { - public static final String[] CERTS_PROJECTION; - static { - ArrayList projection = new ArrayList<>(); - projection.addAll(Arrays.asList(SimpleCursor.PROJECTION)); - projection.addAll(Arrays.asList( - KeychainContract.Certs.MASTER_KEY_ID, - KeychainContract.Certs.VERIFIED, - KeychainContract.Certs.TYPE, - KeychainContract.Certs.RANK, - KeychainContract.Certs.KEY_ID_CERTIFIER, - KeychainContract.Certs.USER_ID, - KeychainContract.Certs.SIGNER_UID - )); - - CERTS_PROJECTION = projection.toArray(new String[projection.size()]); - } - - public static final String CERTS_SORT_ORDER = - KeychainDatabase.Tables.CERTS + "." + KeychainContract.Certs.RANK + " ASC, " - + KeychainContract.Certs.VERIFIED + " DESC, " - + KeychainDatabase.Tables.CERTS + "." + KeychainContract.Certs.TYPE + " DESC, " - + KeychainContract.Certs.SIGNER_UID + " ASC"; - - public static CertCursor wrap(Cursor cursor) { - if(cursor != null) { - return new CertCursor(cursor); - } else { - return null; - } - } - - private CertCursor(Cursor cursor) { - super(cursor); - } - - public long getKeyId() { - int index = getColumnIndexOrThrow(KeychainContract.Certs.MASTER_KEY_ID); - return getLong(index); - } - - public boolean isVerified() { - int index = getColumnIndexOrThrow(KeychainContract.Certs.VERIFIED); - return getInt(index) > 0; - } - - public int getType() { - int index = getColumnIndexOrThrow(KeychainContract.Certs.TYPE); - return getInt(index); - } - - public long getRank() { - int index = getColumnIndexOrThrow(KeychainContract.Certs.RANK); - return getLong(index); - } - - public long getCertifierKeyId() { - int index = getColumnIndexOrThrow(KeychainContract.Certs.KEY_ID_CERTIFIER); - return getLong(index); - } - - public String getRawUserId() { - int index = getColumnIndexOrThrow(KeychainContract.Certs.USER_ID); - return getString(index); - } - - public String getRawSignerUserId() { - int index = getColumnIndexOrThrow(KeychainContract.Certs.SIGNER_UID); - return getString(index); - } - - public String getName() { - int index = getColumnIndexOrThrow(KeychainContract.Certs.NAME); - return getString(index); - } - - public String getEmail() { - int index = getColumnIndexOrThrow(KeychainContract.Certs.EMAIL); - return getString(index); - } - - public String getComment() { - int index = getColumnIndexOrThrow(KeychainContract.Certs.COMMENT); - return getString(index); - } - - public OpenPgpUtils.UserId getSignerUserId() { - return KeyRing.splitUserId(getRawSignerUserId()); - } - } - - public interface CertListListener { - void onClick(long masterKeyId, long signerKeyId, long rank); - } -} \ No newline at end of file diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/FlexibleKeyDetailsItem.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/FlexibleKeyDetailsItem.java new file mode 100644 index 000000000..372bd0830 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/FlexibleKeyDetailsItem.java @@ -0,0 +1,106 @@ +package org.sufficientlysecure.keychain.ui.adapter; + + +import java.util.Arrays; +import java.util.List; + +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; + +import eu.davidea.flexibleadapter.FlexibleAdapter; +import eu.davidea.flexibleadapter.items.IFilterable; +import eu.davidea.flexibleadapter.items.IFlexible; +import eu.davidea.viewholders.FlexibleViewHolder; + +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.model.SubKey.UnifiedKeyInfo; +import org.sufficientlysecure.keychain.ui.adapter.FlexibleKeyDetailsItem.FlexibleKeyItemViewHolder; +import org.sufficientlysecure.keychain.ui.adapter.FlexibleKeyItem.FlexibleSectionableKeyItem; +import org.sufficientlysecure.keychain.ui.util.KeyInfoFormatter; + + +public class FlexibleKeyDetailsItem extends FlexibleSectionableKeyItem + implements IFilterable { + public final UnifiedKeyInfo keyInfo; + + FlexibleKeyDetailsItem(UnifiedKeyInfo keyInfo, FlexibleKeyHeader header) { + super(header); + this.keyInfo = keyInfo; + + setSelectable(true); + } + + @Override + public int getLayoutRes() { + return R.layout.key_list_item; + } + + @Override + public FlexibleKeyItemViewHolder createViewHolder(View view, FlexibleAdapter adapter) { + return new FlexibleKeyItemViewHolder(view, adapter); + } + + @Override + public void bindViewHolder( + FlexibleAdapter adapter, FlexibleKeyItemViewHolder holder, int position, List payloads) { + String highlightString = adapter.getFilter(String.class); + holder.bind(keyInfo, highlightString); + } + + @Override + public boolean equals(Object o) { + if (o instanceof org.sufficientlysecure.keychain.ui.adapter.FlexibleKeyDetailsItem) { + org.sufficientlysecure.keychain.ui.adapter.FlexibleKeyDetailsItem + other = (org.sufficientlysecure.keychain.ui.adapter.FlexibleKeyDetailsItem) o; + return keyInfo.master_key_id() == other.keyInfo.master_key_id(); + } + return false; + } + + @Override + public int hashCode() { + long masterKeyId = keyInfo.master_key_id(); + return (int) (masterKeyId ^ (masterKeyId >>> 32)); + } + + @Override + public boolean filter(String constraint) { + return constraint == null || keyInfo.uidSearchString().contains(constraint); + } + + class FlexibleKeyItemViewHolder extends FlexibleViewHolder { + private final TextView vMainUserId; + private final TextView vMainUserIdRest; + private final TextView vCreationDate; + private final ImageView vStatusIcon; + private final ImageView vTrustIdIcon; + private final KeyInfoFormatter keyInfoFormatter; + + FlexibleKeyItemViewHolder(View itemView, FlexibleAdapter adapter) { + super(itemView, adapter); + + vMainUserId = itemView.findViewById(R.id.key_list_item_name); + vMainUserIdRest = itemView.findViewById(R.id.key_list_item_email); + vStatusIcon = itemView.findViewById(R.id.key_list_item_status_icon); + vCreationDate = itemView.findViewById(R.id.key_list_item_creation); + vTrustIdIcon = itemView.findViewById(R.id.key_list_item_tid_icon); + + keyInfoFormatter = new KeyInfoFormatter(itemView.getContext()); + } + + public void bind(UnifiedKeyInfo keyInfo, String highlightString) { + setEnabled(true); + + keyInfoFormatter.setKeyInfo(keyInfo); + keyInfoFormatter.setHighlightString(highlightString); + keyInfoFormatter.formatUserId(vMainUserId, vMainUserIdRest); + keyInfoFormatter.formatCreationDate(vCreationDate); + keyInfoFormatter.greyInvalidKeys(Arrays.asList(vMainUserId, vMainUserIdRest, vCreationDate)); + keyInfoFormatter.formatStatusIcon(vStatusIcon); + keyInfoFormatter.formatTrustIcon(vTrustIdIcon); + } + + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/FlexibleKeyDummyItem.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/FlexibleKeyDummyItem.java new file mode 100644 index 000000000..a2f707983 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/FlexibleKeyDummyItem.java @@ -0,0 +1,48 @@ +package org.sufficientlysecure.keychain.ui.adapter; + + +import java.util.List; + +import android.view.View; + +import eu.davidea.flexibleadapter.FlexibleAdapter; +import eu.davidea.flexibleadapter.items.IFlexible; +import eu.davidea.viewholders.FlexibleViewHolder; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.ui.adapter.FlexibleKeyDummyItem.FlexibleKeyDummyViewHolder; +import org.sufficientlysecure.keychain.ui.adapter.FlexibleKeyItem.FlexibleSectionableKeyItem; + + +public class FlexibleKeyDummyItem extends FlexibleSectionableKeyItem { + FlexibleKeyDummyItem(FlexibleKeyHeader header) { + super(header); + + setSelectable(false); + } + + @Override + public int getLayoutRes() { + return R.layout.key_list_dummy; + } + + @Override + public boolean equals(Object o) { + return this == o; + } + + @Override + public FlexibleKeyDummyViewHolder createViewHolder(View view, FlexibleAdapter adapter) { + return new FlexibleKeyDummyViewHolder(view, adapter); + } + + @Override + public void bindViewHolder(FlexibleAdapter adapter, FlexibleKeyDummyViewHolder holder, + int position, List payloads) { + } + + class FlexibleKeyDummyViewHolder extends FlexibleViewHolder { + private FlexibleKeyDummyViewHolder(View view, FlexibleAdapter adapter) { + super(view, adapter, true); + } + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/FlexibleKeyHeader.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/FlexibleKeyHeader.java new file mode 100644 index 000000000..2c2327e98 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/FlexibleKeyHeader.java @@ -0,0 +1,67 @@ +package org.sufficientlysecure.keychain.ui.adapter; + + +import java.util.List; + +import android.view.View; +import android.widget.TextView; + +import eu.davidea.flexibleadapter.FlexibleAdapter; +import eu.davidea.flexibleadapter.items.AbstractHeaderItem; +import eu.davidea.flexibleadapter.items.IFlexible; +import eu.davidea.flexibleadapter.items.IHeader; +import eu.davidea.viewholders.FlexibleViewHolder; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.ui.adapter.FlexibleKeyHeader.FlexibleHeaderViewHolder; + + +public class FlexibleKeyHeader extends FlexibleKeyItem + implements IHeader { + private final String sectionTitle; + + FlexibleKeyHeader(String sectionTitle) { + super(); + this.sectionTitle = sectionTitle; + setEnabled(false); + } + + @Override + public boolean equals(Object o) { + if (o instanceof FlexibleKeyHeader) { + FlexibleKeyHeader other = (FlexibleKeyHeader) o; + return sectionTitle.equals(other.sectionTitle); + } + return false; + } + + @Override + public int getLayoutRes() { + return R.layout.key_list_header_public; + } + + public String getSectionTitle() { + return sectionTitle; + } + + @Override + public FlexibleHeaderViewHolder createViewHolder(View view, FlexibleAdapter adapter) { + return new FlexibleHeaderViewHolder(view, adapter); + } + + @Override + public void bindViewHolder(FlexibleAdapter adapter, FlexibleHeaderViewHolder holder, int position, + List payloads) { + holder.text1.setText(sectionTitle); + } + + static class FlexibleHeaderViewHolder extends FlexibleViewHolder { + final TextView text1; + + FlexibleHeaderViewHolder(View view, FlexibleAdapter adapter) { + super(view, adapter, true); + text1 = itemView.findViewById(android.R.id.text1); + } + + + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/FlexibleKeyItem.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/FlexibleKeyItem.java new file mode 100644 index 000000000..3f4c0bd5f --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/FlexibleKeyItem.java @@ -0,0 +1,30 @@ +package org.sufficientlysecure.keychain.ui.adapter; + + +import android.support.v7.widget.RecyclerView; + +import eu.davidea.flexibleadapter.items.AbstractFlexibleItem; +import eu.davidea.flexibleadapter.items.ISectionable; + + +public abstract class FlexibleKeyItem extends AbstractFlexibleItem { + + public static abstract class FlexibleSectionableKeyItem + extends FlexibleKeyItem implements ISectionable { + FlexibleKeyHeader header; + + FlexibleSectionableKeyItem(FlexibleKeyHeader header) { + this.header = header; + } + + @Override + public FlexibleKeyHeader getHeader() { + return header; + } + + @Override + public void setHeader(FlexibleKeyHeader header) { + this.header = header; + } + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/FlexibleKeyItemFactory.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/FlexibleKeyItemFactory.java new file mode 100644 index 000000000..139b7cb20 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/FlexibleKeyItemFactory.java @@ -0,0 +1,72 @@ +package org.sufficientlysecure.keychain.ui.adapter; + + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import android.content.res.Resources; +import android.support.annotation.NonNull; + +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.model.SubKey.UnifiedKeyInfo; + + +public class FlexibleKeyItemFactory { + private Map initialsHeaderMap = new HashMap<>(); + private FlexibleKeyHeader myKeysHeader; + + public FlexibleKeyItemFactory(Resources resources) { + String myKeysHeaderText = resources.getString(R.string.my_keys); + myKeysHeader = new FlexibleKeyHeader(myKeysHeaderText); + } + + public List mapUnifiedKeyInfoToFlexibleKeyItems(List unifiedKeyInfos) { + List result = new ArrayList<>(); + if (unifiedKeyInfos == null) { + return result; + } + if (unifiedKeyInfos.isEmpty() || !unifiedKeyInfos.get(0).has_any_secret()) { + result.add(new FlexibleKeyDummyItem(myKeysHeader)); + } + for (UnifiedKeyInfo unifiedKeyInfo : unifiedKeyInfos) { + FlexibleKeyHeader header = getFlexibleKeyHeader(unifiedKeyInfo); + FlexibleKeyItem flexibleKeyItem = new FlexibleKeyDetailsItem(unifiedKeyInfo, header); + result.add(flexibleKeyItem); + } + return result; + } + + private FlexibleKeyHeader getFlexibleKeyHeader(UnifiedKeyInfo unifiedKeyInfo) { + if (unifiedKeyInfo.has_any_secret()) { + return myKeysHeader; + } + + String headerText = getHeaderText(unifiedKeyInfo); + + FlexibleKeyHeader header; + if (initialsHeaderMap.containsKey(headerText)) { + header = initialsHeaderMap.get(headerText); + } else { + header = new FlexibleKeyHeader(headerText); + initialsHeaderMap.put(headerText, header); + } + return header; + } + + @NonNull + private String getHeaderText(UnifiedKeyInfo unifiedKeyInfo) { + String headerText = unifiedKeyInfo.name(); + if (headerText == null || headerText.isEmpty()) { + headerText = unifiedKeyInfo.email(); + } + if (headerText == null || headerText.isEmpty()) { + return ""; + } + if (!Character.isLetter(headerText.codePointAt(0))) { + return "#"; + } + return headerText.substring(0, 1).toUpperCase(); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/IdentityAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/IdentityAdapter.java index f3bb61d51..b8bbd43c0 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/IdentityAdapter.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/IdentityAdapter.java @@ -25,22 +25,21 @@ import android.content.Context; import android.graphics.Typeface; import android.os.Build; import android.os.Build.VERSION_CODES; +import android.support.annotation.NonNull; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; -import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.linked.UriAttribute; -import org.sufficientlysecure.keychain.provider.KeychainContract.Certs; import org.sufficientlysecure.keychain.ui.adapter.IdentityAdapter.ViewHolder; +import org.sufficientlysecure.keychain.ui.keyview.loader.IdentityDao.AutocryptPeerInfo; import org.sufficientlysecure.keychain.ui.keyview.loader.IdentityDao.IdentityInfo; import org.sufficientlysecure.keychain.ui.keyview.loader.IdentityDao.LinkedIdInfo; import org.sufficientlysecure.keychain.ui.keyview.loader.IdentityDao.UserIdInfo; -import org.sufficientlysecure.keychain.ui.keyview.loader.IdentityDao.AutocryptPeerInfo; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils.State; import org.sufficientlysecure.keychain.ui.util.SubtleAttentionSeeker; @@ -53,28 +52,28 @@ public class IdentityAdapter extends RecyclerView.Adapter { private final Context context; private final LayoutInflater layoutInflater; - private final boolean isSecret; private final IdentityClickListener identityClickListener; private List data; + private boolean isSecret; - public IdentityAdapter(Context context, boolean isSecret, IdentityClickListener identityClickListener) { + public IdentityAdapter(Context context, IdentityClickListener identityClickListener) { super(); this.layoutInflater = LayoutInflater.from(context); this.context = context; - this.isSecret = isSecret; this.identityClickListener = identityClickListener; } - public void setData(List data) { + public void setData(List data, boolean isSecret) { this.data = data; + this.isSecret = isSecret; notifyDataSetChanged(); } @Override - public void onBindViewHolder(ViewHolder holder, int position) { + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { IdentityInfo info = data.get(position); int viewType = getItemViewType(position); @@ -91,8 +90,9 @@ public class IdentityAdapter extends RecyclerView.Adapter { } } + @NonNull @Override - public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { if (viewType == VIEW_TYPE_USER_ID) { return new UserIdViewHolder( layoutInflater.inflate(R.layout.view_key_identity_user_id, parent, false), identityClickListener); @@ -145,12 +145,9 @@ public class IdentityAdapter extends RecyclerView.Adapter { vTitle = view.findViewById(R.id.linked_id_title); vComment = view.findViewById(R.id.linked_id_comment); - view.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - if (identityClickListener != null) { - identityClickListener.onClickIdentity(getAdapterPosition()); - } + view.setOnClickListener(v -> { + if (identityClickListener != null) { + identityClickListener.onClickIdentity(getAdapterPosition()); } }); } @@ -158,7 +155,7 @@ public class IdentityAdapter extends RecyclerView.Adapter { public void bind(Context context, LinkedIdInfo info, boolean isSecret) { bindVerified(context, info, isSecret); - UriAttribute uriAttribute = info.getUriAttribute(); + UriAttribute uriAttribute = info.getLinkedAttribute(); bind(context, uriAttribute); } @@ -178,19 +175,12 @@ public class IdentityAdapter extends RecyclerView.Adapter { private void bindVerified(Context context, IdentityInfo info, boolean isSecret) { if (!isSecret) { - switch (info.getVerified()) { - case Certs.VERIFIED_SECRET: - KeyFormattingUtils.setStatusImage(context, vVerified, - null, State.VERIFIED, KeyFormattingUtils.DEFAULT_COLOR); - break; - case Certs.VERIFIED_SELF: - KeyFormattingUtils.setStatusImage(context, vVerified, - null, State.UNVERIFIED, KeyFormattingUtils.DEFAULT_COLOR); - break; - default: - KeyFormattingUtils.setStatusImage(context, vVerified, - null, State.INVALID, KeyFormattingUtils.DEFAULT_COLOR); - break; + if (info.isVerified()) { + KeyFormattingUtils.setStatusImage(context, vVerified, + null, State.VERIFIED, KeyFormattingUtils.DEFAULT_COLOR); + } else { + KeyFormattingUtils.setStatusImage(context, vVerified, + null, State.UNVERIFIED, KeyFormattingUtils.DEFAULT_COLOR); } } } @@ -221,19 +211,8 @@ public class IdentityAdapter extends RecyclerView.Adapter { vIcon = view.findViewById(R.id.trust_id_app_icon); vMore = view.findViewById(R.id.user_id_item_more); - view.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - identityClickListener.onClickIdentity(getAdapterPosition()); - } - }); - - vMore.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - identityClickListener.onClickIdentityMore(getAdapterPosition(), v); - } - }); + view.setOnClickListener(v -> identityClickListener.onClickIdentity(getAdapterPosition())); + vMore.setOnClickListener(v -> identityClickListener.onClickIdentityMore(getAdapterPosition(), v)); } public void bind(AutocryptPeerInfo info) { @@ -248,6 +227,8 @@ public class IdentityAdapter extends RecyclerView.Adapter { } vIcon.setImageDrawable(info.getAppIcon()); + + vIcon.setVisibility(View.VISIBLE); vMore.setVisibility(View.VISIBLE); itemView.setClickable(info.getAutocryptPeerIntent() != null); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysAdapter.java index 633300e70..bcb13376c 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysAdapter.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysAdapter.java @@ -18,6 +18,11 @@ package org.sufficientlysecure.keychain.ui.adapter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + import android.content.Intent; import android.databinding.DataBindingUtil; import android.support.v4.app.FragmentActivity; @@ -26,6 +31,7 @@ import android.view.LayoutInflater; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; + import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.databinding.ImportKeysListItemBinding; import org.sufficientlysecure.keychain.keyimport.HkpKeyserverAddress; @@ -34,13 +40,12 @@ import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing; import org.sufficientlysecure.keychain.keyimport.processing.ImportKeysListener; import org.sufficientlysecure.keychain.keyimport.processing.ImportKeysOperationCallback; import org.sufficientlysecure.keychain.keyimport.processing.ImportKeysResultListener; +import org.sufficientlysecure.keychain.model.SubKey.UnifiedKeyInfo; import org.sufficientlysecure.keychain.operations.ImportOperation; import org.sufficientlysecure.keychain.operations.results.ImportKeyResult; import org.sufficientlysecure.keychain.pgp.CanonicalizedKeyRing; -import org.sufficientlysecure.keychain.pgp.KeyRing; -import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; -import org.sufficientlysecure.keychain.provider.KeyRepository; -import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; +import org.sufficientlysecure.keychain.pgp.CanonicalizedKeyRing.VerificationStatus; +import org.sufficientlysecure.keychain.daos.KeyRepository; import org.sufficientlysecure.keychain.service.ImportKeyringParcel; import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper; import org.sufficientlysecure.keychain.ui.keyview.ViewKeyActivity; @@ -49,11 +54,6 @@ import org.sufficientlysecure.keychain.ui.util.Notify; import org.sufficientlysecure.keychain.util.ParcelableFileCache; import timber.log.Timber; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; - public class ImportKeysAdapter extends RecyclerView.Adapter implements ImportKeysResultListener { @@ -87,15 +87,16 @@ public class ImportKeysAdapter extends RecyclerView.Adapter 0; - } catch (KeyRepository.NotFoundException | PgpKeyNotFoundException ignored) { + keyState.mVerified = verified != null && verified != VerificationStatus.UNVERIFIED; + } catch (KeyRepository.NotFoundException ignored) { } mKeyStates[i] = keyState; @@ -182,8 +183,7 @@ public class ImportKeysAdapter extends RecyclerView.Adapter. - */ - -package org.sufficientlysecure.keychain.ui.adapter; - -import android.content.Context; -import android.database.Cursor; -import android.graphics.PorterDuff; -import android.support.v4.widget.CursorAdapter; -import android.text.format.DateUtils; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageButton; -import android.widget.ImageView; -import android.widget.TextView; - -import org.openintents.openpgp.util.OpenPgpUtils; -import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKey; -import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKeyRing; -import org.sufficientlysecure.keychain.pgp.KeyRing; -import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; -import org.sufficientlysecure.keychain.ui.util.FormattingUtils; -import org.sufficientlysecure.keychain.ui.util.Highlighter; -import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; -import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils.State; - -import java.io.Serializable; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Date; -import java.util.List; - -public class KeyAdapter extends CursorAdapter { - - protected String mQuery; - protected LayoutInflater mInflater; - protected Context mContext; - - // These are the rows that we will retrieve. - public static final String[] PROJECTION = new String[]{ - KeyRings._ID, - KeyRings.MASTER_KEY_ID, - KeyRings.USER_ID, - KeyRings.IS_REVOKED, - KeyRings.IS_EXPIRED, - KeyRings.IS_SECURE, - KeyRings.VERIFIED, - KeyRings.HAS_ANY_SECRET, - KeyRings.HAS_DUPLICATE_USER_ID, - KeyRings.FINGERPRINT, - KeyRings.CREATION, - KeyRings.HAS_ENCRYPT, - KeyRings.NAME, - KeyRings.EMAIL, - KeyRings.COMMENT - }; - - public static final int INDEX_MASTER_KEY_ID = 1; - public static final int INDEX_USER_ID = 2; - public static final int INDEX_IS_REVOKED = 3; - public static final int INDEX_IS_EXPIRED = 4; - public static final int INDEX_IS_SECURE = 5; - public static final int INDEX_VERIFIED = 6; - public static final int INDEX_HAS_ANY_SECRET = 7; - public static final int INDEX_HAS_DUPLICATE_USER_ID = 8; - public static final int INDEX_FINGERPRINT = 9; - public static final int INDEX_CREATION = 10; - public static final int INDEX_HAS_ENCRYPT = 11; - public static final int INDEX_NAME = 12; - public static final int INDEX_EMAIL = 13; - public static final int INDEX_COMMENT = 14; - - public KeyAdapter(Context context, Cursor c, int flags) { - super(context, c, flags); - - mContext = context; - mInflater = LayoutInflater.from(context); - } - - public void setSearchQuery(String query) { - mQuery = query; - } - - public static class KeyItemViewHolder { - public View mView; - public View mLayoutData; - public Long mMasterKeyId; - public TextView mMainUserId; - public TextView mMainUserIdRest; - public TextView mCreationDate; - public ImageView mStatus; - public View mSlinger; - public ImageButton mSlingerButton; - - public KeyItem mDisplayedItem; - - public KeyItemViewHolder(View view) { - mView = view; - mLayoutData = view.findViewById(R.id.key_list_item_data); - mMainUserId = view.findViewById(R.id.key_list_item_name); - mMainUserIdRest = view.findViewById(R.id.key_list_item_email); - mStatus = view.findViewById(R.id.key_list_item_status_icon); - mSlinger = view.findViewById(R.id.key_list_item_slinger_view); - mSlingerButton = view.findViewById(R.id.key_list_item_slinger_button); - mCreationDate = view.findViewById(R.id.key_list_item_creation); - } - - public void setData(Context context, KeyItem item, Highlighter highlighter, boolean enabled) { - mDisplayedItem = item; - - { // set name and stuff, common to both key types - OpenPgpUtils.UserId userIdSplit = item.mUserId; - if (userIdSplit.name != null) { - mMainUserId.setText(highlighter.highlight(userIdSplit.name)); - } else { - mMainUserId.setText(R.string.user_id_no_name); - } - if (userIdSplit.email != null) { - mMainUserIdRest.setText(highlighter.highlight(userIdSplit.email)); - mMainUserIdRest.setVisibility(View.VISIBLE); - } else { - mMainUserIdRest.setVisibility(View.GONE); - } - } - - // sort of a hack: if this item isn't enabled, we make it clickable - // to intercept its click events. either way, no listener! - mView.setClickable(!enabled); - - { // set edit button and status, specific by key type - - mMasterKeyId = item.mKeyId; - - int textColor; - - // Note: order is important! - if (item.mIsRevoked) { - KeyFormattingUtils - .setStatusImage(context, mStatus, null, State.REVOKED, R.color.key_flag_gray); - mStatus.setVisibility(View.VISIBLE); - mSlinger.setVisibility(View.GONE); - textColor = context.getResources().getColor(R.color.key_flag_gray); - } else if (item.mIsExpired) { - KeyFormattingUtils.setStatusImage(context, mStatus, null, State.EXPIRED, R.color.key_flag_gray); - mStatus.setVisibility(View.VISIBLE); - mSlinger.setVisibility(View.GONE); - textColor = context.getResources().getColor(R.color.key_flag_gray); - } else if (!item.mIsSecure) { - KeyFormattingUtils.setStatusImage(context, mStatus, null, State.INSECURE, R.color.key_flag_gray); - mStatus.setVisibility(View.VISIBLE); - mSlinger.setVisibility(View.GONE); - textColor = context.getResources().getColor(R.color.key_flag_gray); - } else if (item.mIsSecret) { - mStatus.setVisibility(View.GONE); - if (mSlingerButton.hasOnClickListeners()) { - mSlingerButton.setColorFilter( - FormattingUtils.getColorFromAttr(context, R.attr.colorTertiaryText), - PorterDuff.Mode.SRC_IN); - mSlinger.setVisibility(View.VISIBLE); - } else { - mSlinger.setVisibility(View.GONE); - } - textColor = FormattingUtils.getColorFromAttr(context, R.attr.colorText); - } else { - // this is a public key - show if it's verified - if (item.mIsVerified) { - KeyFormattingUtils.setStatusImage(context, mStatus, State.VERIFIED); - mStatus.setVisibility(View.VISIBLE); - } else { - KeyFormattingUtils.setStatusImage(context, mStatus, State.UNVERIFIED); - mStatus.setVisibility(View.VISIBLE); - } - mSlinger.setVisibility(View.GONE); - textColor = FormattingUtils.getColorFromAttr(context, R.attr.colorText); - } - - if (!enabled) { - textColor = context.getResources().getColor(R.color.key_flag_gray); - } - - mMainUserId.setTextColor(textColor); - mMainUserIdRest.setTextColor(textColor); - - if (item.mHasDuplicate) { - String dateTime = DateUtils.formatDateTime(context, - item.mCreation.getTime(), - DateUtils.FORMAT_SHOW_DATE - | DateUtils.FORMAT_SHOW_TIME - | DateUtils.FORMAT_SHOW_YEAR - | DateUtils.FORMAT_ABBREV_MONTH); - mCreationDate.setText(context.getString(R.string.label_key_created, - dateTime)); - mCreationDate.setTextColor(textColor); - mCreationDate.setVisibility(View.VISIBLE); - } else { - mCreationDate.setVisibility(View.GONE); - } - } - } - } - - public boolean isEnabled(Cursor cursor) { - return true; - } - - @Override - public View newView(Context context, Cursor cursor, ViewGroup parent) { - View view = mInflater.inflate(R.layout.key_list_item, parent, false); - KeyItemViewHolder holder = new KeyItemViewHolder(view); - view.setTag(holder); - return view; - } - - @Override - public void bindView(View view, Context context, Cursor cursor) { - Highlighter highlighter = new Highlighter(context, mQuery); - KeyItem item = new KeyItem(cursor); - boolean isEnabled = isEnabled(cursor); - - KeyItemViewHolder h = (KeyItemViewHolder) view.getTag(); - h.setData(context, item, highlighter, isEnabled); - } - - public boolean isSecretAvailable(int id) { - if (!mCursor.moveToPosition(id)) { - throw new IllegalStateException("couldn't move cursor to position " + id); - } - - return mCursor.getInt(INDEX_HAS_ANY_SECRET) != 0; - } - - public long getMasterKeyId(int id) { - if (!mCursor.moveToPosition(id)) { - throw new IllegalStateException("couldn't move cursor to position " + id); - } - - return mCursor.getLong(INDEX_MASTER_KEY_ID); - } - - @Override - public KeyItem getItem(int position) { - Cursor c = getCursor(); - if (c.isClosed() || !c.moveToPosition(position)) { - return null; - } - return new KeyItem(c); - } - - @Override - public long getItemId(int position) { - Cursor cursor = getCursor(); - // prevent a crash on rapid cursor changes - if (cursor != null && getCursor().isClosed()) { - return 0L; - } - return super.getItemId(position); - } - - // must be serializable for TokenCompleTextView state - public static class KeyItem implements Serializable { - - public final String mUserIdFull; - public final OpenPgpUtils.UserId mUserId; - public final String mName; - public final String mEmail; - public final String mComment; - public final long mKeyId; - public final boolean mHasDuplicate; - public final boolean mHasEncrypt; - public final Date mCreation; - public final String mFingerprint; - public final boolean mIsSecret, mIsRevoked, mIsExpired, mIsSecure, mIsVerified; - - private KeyItem(Cursor cursor) { - String userId = cursor.getString(INDEX_USER_ID); - mUserId = KeyRing.splitUserId(userId); - mName = cursor.getString(INDEX_NAME); - mEmail = cursor.getString(INDEX_EMAIL); - mComment = cursor.getString(INDEX_COMMENT); - mUserIdFull = userId; - mKeyId = cursor.getLong(INDEX_MASTER_KEY_ID); - mHasDuplicate = cursor.getLong(INDEX_HAS_DUPLICATE_USER_ID) > 0; - mHasEncrypt = cursor.getInt(INDEX_HAS_ENCRYPT) != 0; - mCreation = new Date(cursor.getLong(INDEX_CREATION) * 1000); - mFingerprint = KeyFormattingUtils.convertFingerprintToHex( - 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; - mIsSecure = cursor.getInt(INDEX_IS_SECURE) > 0; - mIsVerified = cursor.getInt(INDEX_VERIFIED) > 0; - } - - public KeyItem(CanonicalizedPublicKeyRing ring) { - CanonicalizedPublicKey key = ring.getPublicKey(); - String userId = key.getPrimaryUserIdWithFallback(); - mUserId = KeyRing.splitUserId(userId); - mName = mUserId.name; - mEmail = mUserId.email; - mComment = mUserId.comment; - mUserIdFull = userId; - mKeyId = ring.getMasterKeyId(); - mHasDuplicate = false; - mHasEncrypt = key.getKeyRing().getEncryptIds().size() > 0; - mCreation = key.getCreationTime(); - mFingerprint = KeyFormattingUtils.convertFingerprintToHex( - ring.getFingerprint()); - mIsRevoked = key.isRevoked(); - mIsExpired = key.isExpired(); - mIsSecure = key.isSecure(); - - // these two are actually "don't know"s - mIsSecret = false; - mIsVerified = false; - } - - public String getReadableName() { - if (mName != null) { - return mName; - } else { - return mEmail; - } - } - } - - public static String[] getProjectionWith(String[] projection) { - List list = new ArrayList<>(); - list.addAll(Arrays.asList(PROJECTION)); - list.addAll(Arrays.asList(projection)); - return list.toArray(new String[list.size()]); - } - -} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeyChoiceAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeyChoiceAdapter.java new file mode 100644 index 000000000..e90334274 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeyChoiceAdapter.java @@ -0,0 +1,279 @@ +package org.sufficientlysecure.keychain.ui.adapter; + + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +import android.content.Context; +import android.support.annotation.Nullable; +import android.support.annotation.StringRes; +import android.support.v7.widget.RecyclerView; +import android.view.View; +import android.widget.CheckBox; +import android.widget.RadioButton; +import android.widget.TextView; +import android.widget.Toast; + +import eu.davidea.flexibleadapter.FlexibleAdapter; +import eu.davidea.flexibleadapter.items.AbstractFlexibleItem; +import eu.davidea.flexibleadapter.items.IFlexible; +import eu.davidea.viewholders.FlexibleViewHolder; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.model.SubKey.UnifiedKeyInfo; +import org.sufficientlysecure.keychain.ui.adapter.KeyChoiceAdapter.KeyChoiceItem; +import org.sufficientlysecure.keychain.ui.util.KeyInfoFormatter; + + +public class KeyChoiceAdapter extends FlexibleAdapter { + @Nullable + private final OnKeyClickListener onKeyClickListener; + @Nullable + private final KeyDisabledPredicate keyDisabledPredicate; + @Nullable + private Integer activeItem; + private KeyInfoFormatter keyInfoFormatter; + + public static KeyChoiceAdapter createSingleClickableAdapter(Context context, List items, + OnKeyClickListener onKeyClickListener, + KeyDisabledPredicate keyDisabledPredicate) { + return new KeyChoiceAdapter(context, items, Objects.requireNonNull(onKeyClickListener), Mode.IDLE, + keyDisabledPredicate + ); + } + + public static KeyChoiceAdapter createSingleChoiceAdapter(Context context, List items, + KeyDisabledPredicate keyDisabledPredicate) { + return new KeyChoiceAdapter(context, items, null, Mode.SINGLE, keyDisabledPredicate); + } + + public static KeyChoiceAdapter createMultiChoiceAdapter(Context context, List items, + KeyDisabledPredicate keyDisabledPredicate) { + return new KeyChoiceAdapter(context, items, null, Mode.MULTI, keyDisabledPredicate); + } + + private KeyChoiceAdapter(Context context, List items, + @Nullable OnKeyClickListener onKeyClickListener, int idle, + @Nullable KeyDisabledPredicate keyDisabledPredicate) { + super(null, null, true); + setMode(idle); + addListener((OnItemClickListener) (view, position) -> onClickItem(position)); + updateDataSet(getKeyChoiceItems(items, keyDisabledPredicate), false); + this.keyInfoFormatter = new KeyInfoFormatter(context); + this.onKeyClickListener = onKeyClickListener; + this.keyDisabledPredicate = keyDisabledPredicate; + } + + @Nullable + private ArrayList getKeyChoiceItems(@Nullable List items, + @Nullable KeyDisabledPredicate keyDisabledPredicate) { + if (items == null) { + return null; + } + ArrayList choiceItems = new ArrayList<>(); + for (UnifiedKeyInfo keyInfo : items) { + Integer disabledString = keyDisabledPredicate != null ? keyDisabledPredicate.getDisabledString(keyInfo) : null; + KeyChoiceItem keyChoiceItem = new KeyChoiceItem(keyInfo, disabledString); + choiceItems.add(keyChoiceItem); + } + return choiceItems; + } + + private boolean onClickItem(int position) { + KeyChoiceItem item = getItem(position); + if (item != null && item.disabledStringRes != null) { + Toast.makeText(getRecyclerView().getContext(), item.disabledStringRes, Toast.LENGTH_SHORT).show(); + return false; + } + + if (getMode() == Mode.MULTI) { + toggleSelection(position); + notifyItemChanged(position); + return true; + } else if (getMode() == Mode.SINGLE) { + setActiveItem(position); + return true; + } + + Objects.requireNonNull(onKeyClickListener).onKeyClick(item.keyInfo); + return false; + } + + public void setActiveItem(Integer newActiveItem) { + if (getMode() != Mode.SINGLE) { + throw new IllegalStateException("Cannot get active item in single select mode!"); + } + + clearSelection(); + + Integer prevActiveItem = this.activeItem; + this.activeItem = newActiveItem; + + if (prevActiveItem != null) { + notifyItemChanged(prevActiveItem); + } + if (newActiveItem != null) { + toggleSelection(newActiveItem); + notifyItemChanged(newActiveItem); + } + } + + public UnifiedKeyInfo getActiveItem() { + if (getMode() != Mode.SINGLE) { + throw new IllegalStateException("Cannot get active item in single select mode!"); + } + if (activeItem == null) { + return null; + } + + KeyChoiceItem item = getItem(activeItem); + return item == null ? null : item.keyInfo; + } + + public void setUnifiedKeyInfoItems(List keyInfos) { + List keyChoiceItems = getKeyChoiceItems(keyInfos, keyDisabledPredicate); + updateDataSet(keyChoiceItems); + } + + @Override + public long getItemId(int position) { + KeyChoiceItem item = getItem(position); + if (item == null) { + return RecyclerView.NO_ID; + } + return item.getMasterKeyId(); + } + + public void setSelectionByIds(Set checkedIds) { + if (getMode() != Mode.MULTI) { + throw new IllegalStateException("Cannot get active item in single select mode!"); + } + + clearSelection(); + for (int position = 0; position < getItemCount(); position++) { + long itemId = getItemId(position); + if (checkedIds.contains(itemId)) { + addSelection(position); + } + } + } + + public Set getSelectionIds() { + if (getMode() != Mode.MULTI) { + throw new IllegalStateException("Cannot get active item in single select mode!"); + } + + Set result = new HashSet<>(); + for (int position : getSelectedPositions()) { + long itemId = getItemId(position); + result.add(itemId); + } + return result; + } + + public class KeyChoiceItem extends AbstractFlexibleItem { + private UnifiedKeyInfo keyInfo; + @StringRes + private Integer disabledStringRes; + + KeyChoiceItem(UnifiedKeyInfo keyInfo, @StringRes Integer disabledStringRes) { + this.keyInfo = keyInfo; + this.disabledStringRes = disabledStringRes; + setSelectable(true); + } + + @Override + public int getLayoutRes() { + return R.layout.key_choice_item; + } + + @Override + public KeyChoiceViewHolder createViewHolder(View view, FlexibleAdapter adapter) { + return new KeyChoiceViewHolder(view, adapter); + } + + @Override + public void bindViewHolder(FlexibleAdapter adapter, KeyChoiceViewHolder holder, int position, + List payloads) { + boolean isActive = adapter.isSelected(position); + boolean isEnabled = disabledStringRes == null; + holder.bind(keyInfo, adapter.getMode(), isActive, isEnabled); + } + + @Override + public boolean equals(Object o) { + return (o instanceof KeyChoiceItem) && + ((KeyChoiceItem) o).keyInfo.master_key_id() == keyInfo.master_key_id(); + } + + @Override + public int hashCode() { + long masterKeyId = keyInfo.master_key_id(); + return (int) (masterKeyId ^ (masterKeyId >>> 32)); + } + + public long getMasterKeyId() { + return keyInfo.master_key_id(); + } + } + + public class KeyChoiceViewHolder extends FlexibleViewHolder { + private final TextView vName; + private final TextView vCreation; + private final CheckBox vCheckbox; + private final RadioButton vRadio; + + KeyChoiceViewHolder(View itemView, FlexibleAdapter adapter) { + super(itemView, adapter); + + vName = itemView.findViewById(R.id.text_keychoice_name); + vCreation = itemView.findViewById(R.id.text_keychoice_creation); + vCheckbox = itemView.findViewById(R.id.checkbox_keychoice); + vRadio = itemView.findViewById(R.id.radio_keychoice); + } + + void bind(UnifiedKeyInfo keyInfo, int choiceMode, boolean isActive, boolean isEnabled) { + keyInfoFormatter.setKeyInfo(keyInfo); + + vName.setText(keyInfo.user_id()); + + keyInfoFormatter.formatCreationDate(vCreation); + + switch (choiceMode) { + case Mode.IDLE: { + vRadio.setVisibility(View.GONE); + vCheckbox.setVisibility(View.GONE); + break; + } + case Mode.SINGLE: { + vRadio.setVisibility(View.VISIBLE); + vRadio.setChecked(isActive); + vCheckbox.setVisibility(View.GONE); + break; + } + case Mode.MULTI: { + vCheckbox.setVisibility(View.VISIBLE); + vCheckbox.setChecked(isActive); + vRadio.setVisibility(View.GONE); + break; + } + } + + vCheckbox.setEnabled(isEnabled); + vRadio.setEnabled(isEnabled); + vName.setEnabled(isEnabled); + vCreation.setEnabled(isEnabled); + } + } + + public interface OnKeyClickListener { + void onKeyClick(UnifiedKeyInfo keyInfo); + } + + public interface KeyDisabledPredicate { + @StringRes + Integer getDisabledString(UnifiedKeyInfo keyInfo); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeyCursorAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeyCursorAdapter.java deleted file mode 100644 index c46a11740..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeyCursorAdapter.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (C) 2017 Schürmann & Breitmoser GbR - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sufficientlysecure.keychain.ui.adapter; - -import android.content.Context; -import android.database.Cursor; -import android.support.v7.widget.RecyclerView; - -import org.sufficientlysecure.keychain.ui.util.adapter.CursorAdapter; - -public abstract class KeyCursorAdapter - extends CursorAdapter { - - private String mQuery; - - public KeyCursorAdapter(Context context, C cursor){ - super(context, cursor, 0); - setHasStableIds(true); - } - - public void setQuery(String query) { - mQuery = query; - } - - @Override - public int getItemViewType(int position) { - moveCursorOrThrow(position); - return getItemViewType(getCursor()); - } - - @Override - public long getIdFromCursor(C keyCursor) { - return keyCursor.getKeyId(); - } - - @Override - public void onBindViewHolder(VH holder, int position) { - moveCursorOrThrow(position); - onBindViewHolder(holder, getCursor(), mQuery); - } - - public int getItemViewType(C keyCursor) { return 0; } - public abstract void onBindViewHolder(VH holder, C cursor, String query); -} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeySectionedListAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeySectionedListAdapter.java deleted file mode 100644 index c82bc9500..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeySectionedListAdapter.java +++ /dev/null @@ -1,691 +0,0 @@ -/* - * Copyright (C) 2017 Schürmann & Breitmoser GbR - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sufficientlysecure.keychain.ui.adapter; - -import android.content.Context; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; -import android.database.Cursor; -import android.database.MatrixCursor; -import android.database.MergeCursor; -import android.graphics.PorterDuff; -import android.graphics.drawable.Drawable; -import android.support.annotation.NonNull; -import android.support.v4.content.ContextCompat; -import android.text.TextUtils; -import android.text.format.DateUtils; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageButton; -import android.widget.ImageView; -import android.widget.TextView; - -import com.futuremind.recyclerviewfastscroll.SectionTitleProvider; - -import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.provider.KeychainContract; -import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; -import org.sufficientlysecure.keychain.ui.util.FormattingUtils; -import org.sufficientlysecure.keychain.ui.util.Highlighter; -import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; -import org.sufficientlysecure.keychain.ui.util.adapter.CursorAdapter; -import org.sufficientlysecure.keychain.ui.util.adapter.SectionCursorAdapter; -import timber.log.Timber; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; - -public class KeySectionedListAdapter extends SectionCursorAdapter implements SectionTitleProvider { - - private static final short VIEW_ITEM_TYPE_KEY = 0x0; - private static final short VIEW_ITEM_TYPE_DUMMY = 0x1; - - private static final short VIEW_SECTION_TYPE_PRIVATE = 0x0; - private static final short VIEW_SECTION_TYPE_PUBLIC = 0x1; - - private static final long JUST_NOW_TIMESPAN = DateUtils.MINUTE_IN_MILLIS * 5; - - private String mQuery; - private List mSelected; - private KeyListListener mListener; - - private boolean mHasDummy = false; - - public KeySectionedListAdapter(Context context, Cursor cursor) { - super(context, KeyListCursor.wrap(cursor, KeyListCursor.class), 0); - - mQuery = ""; - mSelected = new ArrayList<>(); - } - - public void setSearchQuery(String query) { - mQuery = query; - } - - - @Override - public void onContentChanged() { - mHasDummy = false; - mSelected.clear(); - - if (mListener != null) { - mListener.onSelectionStateChanged(0); - } - - super.onContentChanged(); - } - - @Override - public KeyListCursor swapCursor(KeyListCursor cursor) { - if (cursor != null && (mQuery == null || TextUtils.isEmpty(mQuery))) { - boolean isSecret = cursor.moveToFirst() && cursor.isSecret(); - - if (!isSecret) { - MatrixCursor headerCursor = new MatrixCursor(KeyListCursor.PROJECTION); - Long[] row = new Long[KeyListCursor.PROJECTION.length]; - row[cursor.getColumnIndex(KeychainContract.KeyRings.HAS_ANY_SECRET)] = 1L; - row[cursor.getColumnIndex(KeychainContract.KeyRings.MASTER_KEY_ID)] = 0L; - headerCursor.addRow(row); - - Cursor[] toMerge = { - headerCursor, - cursor.getWrappedCursor() - }; - - cursor = KeyListCursor.wrap(new MergeCursor(toMerge)); - } - } - - return super.swapCursor(cursor); - } - - public void setKeyListener(KeyListListener listener) { - mListener = listener; - } - - private int getSelectedCount() { - return mSelected.size(); - } - - private void selectPosition(int position) { - mSelected.add(position); - notifyItemChanged(position); - } - - private void deselectPosition(int position) { - mSelected.remove(Integer.valueOf(position)); - notifyItemChanged(position); - } - - private boolean isSelected(int position) { - return mSelected.contains(position); - } - - public long[] getSelectedMasterKeyIds() { - long[] keys = new long[mSelected.size()]; - for (int i = 0; i < keys.length; i++) { - int index = getCursorPositionWithoutSections(mSelected.get(i)); - if (!moveCursor(index)) { - return keys; - } - - keys[i] = getIdFromCursor(getCursor()); - } - - return keys; - } - - public boolean isAnySecretKeySelected() { - for (int i = 0; i < mSelected.size(); i++) { - int index = getCursorPositionWithoutSections(mSelected.get(i)); - if (!moveCursor(index)) { - return false; - } - - if (getCursor().isSecret()) { - return true; - } - } - - return false; - } - - - /** - * Returns the number of database entries displayed. - * - * @return The item count - */ - public int getCount() { - if (getCursor() != null) { - return getCursor().getCount() - (mHasDummy ? 1 : 0); - } else { - return 0; - } - } - - @Override - public long getIdFromCursor(KeyListCursor cursor) { - return cursor.getKeyId(); - } - - @Override - protected Character getSectionFromCursor(KeyListCursor cursor) throws IllegalStateException { - if (cursor.isSecret()) { - if (cursor.getKeyId() == 0L) { - mHasDummy = true; - } - - return '#'; - } else { - String name = cursor.getName(); - if (name != null) { - return Character.toUpperCase(name.charAt(0)); - } else { - return '?'; - } - } - } - - @Override - protected short getSectionHeaderViewType(int sectionIndex) { - return (sectionIndex < 1) ? - VIEW_SECTION_TYPE_PRIVATE : - VIEW_SECTION_TYPE_PUBLIC; - } - - @Override - protected short getSectionItemViewType(int position) { - if (moveCursor(position)) { - KeyListCursor c = getCursor(); - - if (c.isSecret() && c.getKeyId() == 0L) { - return VIEW_ITEM_TYPE_DUMMY; - } - } else { - Timber.w("Unable to determine key view type. " - + "Reason: Could not move cursor over dataset."); - } - - return VIEW_ITEM_TYPE_KEY; - } - - @Override - protected KeyHeaderViewHolder onCreateSectionViewHolder(ViewGroup parent, int viewType) { - switch (viewType) { - case VIEW_SECTION_TYPE_PUBLIC: - return new KeyHeaderViewHolder(LayoutInflater.from(parent.getContext()) - .inflate(R.layout.key_list_header_public, parent, false)); - - case VIEW_SECTION_TYPE_PRIVATE: - return new KeyHeaderViewHolder(LayoutInflater.from(parent.getContext()) - .inflate(R.layout.key_list_header_private, parent, false)); - - default: - return null; - } - } - - @Override - protected ViewHolder onCreateItemViewHolder(ViewGroup parent, int viewType) { - switch (viewType) { - case VIEW_ITEM_TYPE_KEY: - return new KeyItemViewHolder(LayoutInflater.from(parent.getContext()) - .inflate(R.layout.key_list_item, parent, false)); - - case VIEW_ITEM_TYPE_DUMMY: - return new KeyDummyViewHolder(LayoutInflater.from(parent.getContext()) - .inflate(R.layout.key_list_dummy, parent, false)); - - default: - return null; - } - - } - - @Override - protected void onBindSectionViewHolder(KeyHeaderViewHolder holder, Character section) { - switch (holder.getItemViewTypeWithoutSections()) { - case VIEW_SECTION_TYPE_PUBLIC: { - String title = section.equals('?') ? - getContext().getString(R.string.user_id_no_name) : - String.valueOf(section); - - holder.bind(title); - break; - } - - case VIEW_SECTION_TYPE_PRIVATE: { - int count = getCount(); - String title = getContext().getResources() - .getQuantityString(R.plurals.n_keys, count, count); - holder.bind(title); - break; - } - - } - } - - @Override - protected void onBindItemViewHolder(ViewHolder holder, KeyListCursor cursor) { - if (holder.getItemViewTypeWithoutSections() == VIEW_ITEM_TYPE_KEY) { - Highlighter highlighter = new Highlighter(getContext(), mQuery); - ((KeyItemViewHolder) holder).bindKey(cursor, highlighter); - } - } - - public void finishSelection() { - Integer[] selected = mSelected.toArray( - new Integer[mSelected.size()] - ); - - mSelected.clear(); - - for (Integer aSelected : selected) { - notifyItemChanged(aSelected); - } - } - - @Override - public String getSectionTitle(int position) { - // this String will be shown in a bubble for specified position - if (moveCursor(getCursorPositionWithoutSections(position))) { - KeyListCursor cursor = getCursor(); - - if (cursor.isSecret()) { - if (cursor.getKeyId() == 0L) { - mHasDummy = true; - } - - return "My"; - } else { - String name = cursor.getName(); - if (name != null) { - return name.substring(0, 1).toUpperCase(); - } else { - return null; - } - } - } else { - Timber.w("Unable to determine section title. " - + "Reason: Could not move cursor over dataset."); - return null; - } - } - - private class KeyDummyViewHolder extends SectionCursorAdapter.ViewHolder - implements View.OnClickListener { - - KeyDummyViewHolder(View itemView) { - super(itemView); - - itemView.setClickable(true); - itemView.setOnClickListener(this); - itemView.setEnabled(getSelectedCount() == 0); - } - - @Override - public void onClick(View view) { - if (mListener != null) { - mListener.onKeyDummyItemClicked(); - } - } - } - - public class KeyItemViewHolder extends SectionCursorAdapter.ViewHolder - implements View.OnClickListener, View.OnLongClickListener { - - private final ImageView mTrustIdIcon; - private final TextView mMainUserId; - private final TextView mMainUserIdRest; - private final TextView mCreationDate; - private final ImageView mStatus; - private final View mSlinger; - private final ImageButton mSlingerButton; - - KeyItemViewHolder(View itemView) { - super(itemView); - - mMainUserId = itemView.findViewById(R.id.key_list_item_name); - mMainUserIdRest = itemView.findViewById(R.id.key_list_item_email); - mStatus = itemView.findViewById(R.id.key_list_item_status_icon); - mSlinger = itemView.findViewById(R.id.key_list_item_slinger_view); - mSlingerButton = itemView.findViewById(R.id.key_list_item_slinger_button); - mCreationDate = itemView.findViewById(R.id.key_list_item_creation); - mTrustIdIcon = itemView.findViewById(R.id.key_list_item_tid_icon); - - itemView.setClickable(true); - itemView.setLongClickable(true); - itemView.setOnClickListener(this); - itemView.setOnLongClickListener(this); - - mSlingerButton.setClickable(true); - mSlingerButton.setOnClickListener(this); - } - - void bindKey(KeyListCursor keyItem, Highlighter highlighter) { - itemView.setSelected(isSelected(getAdapterPosition())); - Context context = itemView.getContext(); - - { // set name and stuff, common to both key types - String name = keyItem.getName(); - String email = keyItem.getEmail(); - if (name == null) { - if (email != null) { - mMainUserId.setText(highlighter.highlight(email)); - mMainUserIdRest.setVisibility(View.GONE); - } else { - mMainUserId.setText(R.string.user_id_no_name); - } - } else { - mMainUserId.setText(highlighter.highlight(name)); - if (email != null) { - mMainUserIdRest.setText(highlighter.highlight(email)); - mMainUserIdRest.setVisibility(View.VISIBLE); - } else { - mMainUserIdRest.setVisibility(View.GONE); - } - } - } - - { // set edit button and status, specific by key type. Note: order is important! - int textColor; - if (keyItem.isRevoked()) { - KeyFormattingUtils.setStatusImage( - context, - mStatus, - null, - KeyFormattingUtils.State.REVOKED, - R.color.key_flag_gray - ); - - mStatus.setVisibility(View.VISIBLE); - mSlinger.setVisibility(View.GONE); - textColor = ContextCompat.getColor(context, R.color.key_flag_gray); - } else if (keyItem.isExpired()) { - KeyFormattingUtils.setStatusImage( - context, - mStatus, - null, - KeyFormattingUtils.State.EXPIRED, - R.color.key_flag_gray - ); - - mStatus.setVisibility(View.VISIBLE); - mSlinger.setVisibility(View.GONE); - textColor = ContextCompat.getColor(context, R.color.key_flag_gray); - } else if (!keyItem.isSecure()) { - KeyFormattingUtils.setStatusImage( - context, - mStatus, - null, - KeyFormattingUtils.State.INSECURE, - R.color.key_flag_gray - ); - - mStatus.setVisibility(View.VISIBLE); - mSlinger.setVisibility(View.GONE); - textColor = ContextCompat.getColor(context, R.color.key_flag_gray); - } else if (keyItem.isSecret()) { - mStatus.setVisibility(View.GONE); - if (mSlingerButton.hasOnClickListeners()) { - mSlingerButton.setColorFilter( - FormattingUtils.getColorFromAttr(context, R.attr.colorTertiaryText), - PorterDuff.Mode.SRC_IN - ); - - mSlinger.setVisibility(View.VISIBLE); - } else { - mSlinger.setVisibility(View.GONE); - } - textColor = FormattingUtils.getColorFromAttr(context, R.attr.colorText); - } else { - // this is a public key - show if it's verified - if (keyItem.isVerified()) { - KeyFormattingUtils.setStatusImage( - context, - mStatus, - KeyFormattingUtils.State.VERIFIED - ); - - mStatus.setVisibility(View.VISIBLE); - } else { - KeyFormattingUtils.setStatusImage( - context, - mStatus, - KeyFormattingUtils.State.UNVERIFIED - ); - - mStatus.setVisibility(View.VISIBLE); - } - mSlinger.setVisibility(View.GONE); - textColor = FormattingUtils.getColorFromAttr(context, R.attr.colorText); - } - - mMainUserId.setTextColor(textColor); - mMainUserIdRest.setTextColor(textColor); - - if (keyItem.hasDuplicate() || keyItem.isSecret()) { - mCreationDate.setText(getSecretKeyReadableTime(context, keyItem)); - mCreationDate.setTextColor(textColor); - mCreationDate.setVisibility(View.VISIBLE); - } else { - mCreationDate.setVisibility(View.GONE); - } - } - - { // set icons - List packageNames = keyItem.getAutocryptPeerIdPackages(); - - if (!keyItem.isSecret() && !packageNames.isEmpty()) { - String packageName = packageNames.get(0); - Drawable drawable = getDrawableForPackageName(packageName); - if (drawable != null) { - mTrustIdIcon.setImageDrawable(drawable); - mTrustIdIcon.setVisibility(View.VISIBLE); - } else { - mTrustIdIcon.setVisibility(View.GONE); - } - } else { - mTrustIdIcon.setVisibility(View.GONE); - } - } - } - - @NonNull - private String getSecretKeyReadableTime(Context context, KeyListCursor keyItem) { - long creationMillis = keyItem.getCreationTime(); - - boolean allowRelativeTimestamp = keyItem.hasDuplicate(); - if (allowRelativeTimestamp) { - long creationAgeMillis = System.currentTimeMillis() - creationMillis; - if (creationAgeMillis < JUST_NOW_TIMESPAN) { - return context.getString(R.string.label_key_created_just_now); - } - } - - String dateTime = DateUtils.formatDateTime(context, - creationMillis, - DateUtils.FORMAT_SHOW_DATE - | DateUtils.FORMAT_SHOW_TIME - | DateUtils.FORMAT_SHOW_YEAR - | DateUtils.FORMAT_ABBREV_MONTH); - return context.getString(R.string.label_key_created, dateTime); - } - - @Override - public void onClick(View v) { - int pos = getAdapterPosition(); - switch (v.getId()) { - case R.id.key_list_item_slinger_button: - if (mListener != null) { - mListener.onSlingerButtonClicked(getItemId()); - } - break; - - default: - if (getSelectedCount() == 0) { - if (mListener != null) { - mListener.onKeyItemClicked(getItemId()); - } - } else { - if (isSelected(pos)) { - deselectPosition(pos); - } else { - selectPosition(pos); - } - - if (mListener != null) { - mListener.onSelectionStateChanged(getSelectedCount()); - } - } - break; - } - - } - - @Override - public boolean onLongClick(View v) { - System.out.println("Long Click!"); - if (getSelectedCount() == 0) { - selectPosition(getAdapterPosition()); - - if (mListener != null) { - mListener.onSelectionStateChanged(getSelectedCount()); - } - return true; - } - - return false; - } - } - - static class KeyHeaderViewHolder extends SectionCursorAdapter.ViewHolder { - private TextView mText1; - - public KeyHeaderViewHolder(View itemView) { - super(itemView); - mText1 = itemView.findViewById(android.R.id.text1); - } - - public void bind(String title) { - mText1.setText(title); - } - } - - public static class KeyListCursor extends CursorAdapter.KeyCursor { - public static final String ORDER = KeychainContract.KeyRings.HAS_ANY_SECRET - + " DESC, " + KeychainContract.KeyRings.USER_ID + " COLLATE NOCASE ASC"; - - public static final String[] PROJECTION; - - static { - ArrayList arr = new ArrayList<>(); - arr.addAll(Arrays.asList(KeyCursor.PROJECTION)); - arr.addAll(Arrays.asList( - KeychainContract.KeyRings.VERIFIED, - KeychainContract.KeyRings.HAS_ANY_SECRET, - KeychainContract.KeyRings.FINGERPRINT, - KeychainContract.KeyRings.HAS_ENCRYPT, - KeychainContract.KeyRings.API_KNOWN_TO_PACKAGE_NAMES - )); - - PROJECTION = arr.toArray(new String[arr.size()]); - } - - public static KeyListCursor wrap(Cursor cursor) { - if (cursor != null) { - return new KeyListCursor(cursor); - } else { - return null; - } - } - - private KeyListCursor(Cursor cursor) { - super(cursor); - } - - public boolean hasEncrypt() { - int index = getColumnIndexOrThrow(KeychainContract.KeyRings.HAS_ENCRYPT); - return getInt(index) != 0; - } - - public byte[] getRawFingerprint() { - int index = getColumnIndexOrThrow(KeychainContract.KeyRings.FINGERPRINT); - return getBlob(index); - } - - public String getFingerprint() { - return KeyFormattingUtils.convertFingerprintToHex(getRawFingerprint()); - } - - public boolean isSecret() { - int index = getColumnIndexOrThrow(KeychainContract.KeyRings.HAS_ANY_SECRET); - return getInt(index) != 0; - } - - public boolean isVerified() { - int index = getColumnIndexOrThrow(KeychainContract.KeyRings.VERIFIED); - return getInt(index) > 0; - } - - public List getAutocryptPeerIdPackages() { - int index = getColumnIndexOrThrow(KeyRings.API_KNOWN_TO_PACKAGE_NAMES); - String packageNames = getString(index); - if (packageNames == null) { - return Collections.EMPTY_LIST; - } - return Arrays.asList(packageNames.split(",")); - } - } - - public interface KeyListListener { - void onKeyDummyItemClicked(); - - void onKeyItemClicked(long masterKeyId); - - void onSlingerButtonClicked(long masterKeyId); - - void onSelectionStateChanged(int selectedCount); - } - - private HashMap appIconCache = new HashMap<>(); - - private Drawable getDrawableForPackageName(String packageName) { - if (appIconCache.containsKey(packageName)) { - return appIconCache.get(packageName); - } - - PackageManager pm = getContext().getPackageManager(); - try { - ApplicationInfo ai = pm.getApplicationInfo(packageName, 0); - - Drawable appIcon = pm.getApplicationIcon(ai); - appIconCache.put(packageName, appIcon); - - return appIcon; - } catch (PackageManager.NameNotFoundException e) { - return null; - } - } -} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeySelectableAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeySelectableAdapter.java deleted file mode 100644 index 1ee67f2ec..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeySelectableAdapter.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright (C) 2017 Schürmann & Breitmoser GbR - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sufficientlysecure.keychain.ui.adapter; - -import android.content.Context; -import android.database.Cursor; -import android.view.View; -import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.AdapterView.OnItemClickListener; -import android.widget.CheckBox; - -import org.sufficientlysecure.keychain.R; - -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; - -public class KeySelectableAdapter extends KeyAdapter implements OnItemClickListener { - - private HashSet mSelectedItems = new HashSet<>(); - - public KeySelectableAdapter(Context context, Cursor c, int flags, Set initialChecked) { - super(context, c, flags); - if (initialChecked != null) { - mSelectedItems.addAll(initialChecked); - } - } - - public static class KeySelectableItemViewHolder extends KeyItemViewHolder { - - public CheckBox mCheckbox; - - public KeySelectableItemViewHolder(View view) { - super(view); - mCheckbox = view.findViewById(R.id.selected); - } - - public void setCheckedState(boolean checked) { - mCheckbox.setChecked(checked); - } - - } - - @Override - public View newView(Context context, Cursor cursor, ViewGroup parent) { - View view = mInflater.inflate(R.layout.key_list_selectable_item, parent, false); - KeySelectableItemViewHolder holder = new KeySelectableItemViewHolder(view); - view.setTag(holder); - return view; - } - - @Override - public void bindView(View view, Context context, Cursor cursor) { - super.bindView(view, context, cursor); - - KeySelectableItemViewHolder h = (KeySelectableItemViewHolder) view.getTag(); - h.setCheckedState(mSelectedItems.contains(h.mDisplayedItem.mKeyId)); - - } - - public void setCheckedStates(Set checked) { - mSelectedItems.clear(); - mSelectedItems.addAll(checked); - notifyDataSetChanged(); - } - - public Set getSelectedMasterKeyIds() { - return Collections.unmodifiableSet(mSelectedItems); - } - - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - long masterKeyId = getMasterKeyId(position); - if (mSelectedItems.contains(masterKeyId)) { - mSelectedItems.remove(masterKeyId); - } else { - mSelectedItems.add(masterKeyId); - } - notifyDataSetChanged(); - } - -} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/NestedLogAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/NestedLogAdapter.java index 756b19ae3..af50a1dbd 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/NestedLogAdapter.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/NestedLogAdapter.java @@ -17,6 +17,10 @@ package org.sufficientlysecure.keychain.ui.adapter; + +import java.util.ArrayList; +import java.util.List; + import android.content.Context; import android.graphics.Color; import android.support.v4.content.ContextCompat; @@ -29,15 +33,10 @@ import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; -import com.tonicartos.superslim.LayoutManager; - import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.operations.results.OperationResult; import org.sufficientlysecure.keychain.ui.util.FormattingUtils; -import java.util.ArrayList; -import java.util.List; - public class NestedLogAdapter extends RecyclerView.Adapter { private static final int ENTRY_TYPE_REGULAR = 0; private static final int ENTRY_TYPE_SUBLOG = 1; @@ -135,14 +134,7 @@ public class NestedLogAdapter extends RecyclerView.Adapter data; private SaveKeyringParcel.Builder mSkpBuilder; - private boolean mHasAnySecret; private ColorStateList mDefaultTextColor; - public static final String[] SUBKEYS_PROJECTION = new String[]{ - Keys._ID, - Keys.KEY_ID, - Keys.RANK, - Keys.ALGORITHM, - Keys.KEY_SIZE, - Keys.KEY_CURVE_OID, - Keys.HAS_SECRET, - Keys.CAN_CERTIFY, - Keys.CAN_ENCRYPT, - Keys.CAN_SIGN, - Keys.CAN_AUTHENTICATE, - Keys.IS_REVOKED, - Keys.IS_SECURE, - Keys.CREATION, - Keys.EXPIRY, - Keys.FINGERPRINT - }; - private static final int INDEX_ID = 0; - private static final int INDEX_KEY_ID = 1; - private static final int INDEX_RANK = 2; - private static final int INDEX_ALGORITHM = 3; - private static final int INDEX_KEY_SIZE = 4; - private static final int INDEX_KEY_CURVE_OID = 5; - private static final int INDEX_HAS_SECRET = 6; - private static final int INDEX_CAN_CERTIFY = 7; - private static final int INDEX_CAN_ENCRYPT = 8; - private static final int INDEX_CAN_SIGN = 9; - private static final int INDEX_CAN_AUTHENTICATE = 10; - private static final int INDEX_IS_REVOKED = 11; - private static final int INDEX_IS_SECURE = 12; - private static final int INDEX_CREATION = 13; - private static final int INDEX_EXPIRY = 14; - private static final int INDEX_FINGERPRINT = 15; - - public SubkeysAdapter(Context context, Cursor c, int flags) { - super(context, c, flags); - - mInflater = LayoutInflater.from(context); + public SubkeysAdapter(Context context) { + this.context = context; + layoutInflater = LayoutInflater.from(context); } - public long getKeyId(int position) { - mCursor.moveToPosition(position); - return mCursor.getLong(INDEX_KEY_ID); + public void setData(List data) { + this.data = data; + notifyDataSetChanged(); } - public long getCreationDate(int position) { - mCursor.moveToPosition(position); - return mCursor.getLong(INDEX_CREATION); + @Override + public int getCount() { + return data != null ? data.size() : 0; } - public Long getExpiryDate(int position) { - mCursor.moveToPosition(position); - if (mCursor.isNull(INDEX_EXPIRY)) { - return null; + @Override + public SubKey getItem(int position) { + return data.get(position); + } + + @Override + public long getItemId(int position) { + return data.get(position).key_id(); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + View view; + if (convertView != null) { + view = convertView; } else { - return mCursor.getLong(INDEX_EXPIRY); - } - } - - public int getAlgorithm(int position) { - mCursor.moveToPosition(position); - return mCursor.getInt(INDEX_ALGORITHM); - } - - public int getKeySize(int position) { - mCursor.moveToPosition(position); - return mCursor.getInt(INDEX_KEY_SIZE); - } - - public String getCurveOid(int position) { - mCursor.moveToPosition(position); - return mCursor.getString(INDEX_KEY_CURVE_OID); - } - - public SecretKeyType getSecretKeyType(int position) { - mCursor.moveToPosition(position); - return SecretKeyType.fromNum(mCursor.getInt(INDEX_HAS_SECRET)); - } - - @Override - public Cursor swapCursor(Cursor newCursor) { - mHasAnySecret = false; - if (newCursor != null && newCursor.moveToFirst()) { - do { - SecretKeyType hasSecret = SecretKeyType.fromNum(newCursor.getInt(INDEX_HAS_SECRET)); - if (hasSecret.isUsable()) { - mHasAnySecret = true; - break; - } - } while (newCursor.moveToNext()); + view = layoutInflater.inflate(R.layout.view_key_adv_subkey_item, parent, false); } - return super.swapCursor(newCursor); - } + if (mDefaultTextColor == null) { + TextView keyId = view.findViewById(R.id.subkey_item_key_id); + mDefaultTextColor = keyId.getTextColors(); + } - @Override - public void bindView(View view, Context context, Cursor cursor) { TextView vKeyId = view.findViewById(R.id.subkey_item_key_id); TextView vKeyDetails = view.findViewById(R.id.subkey_item_details); TextView vKeyExpiry = view.findViewById(R.id.subkey_item_expiry); @@ -166,19 +108,20 @@ public class SubkeysAdapter extends CursorAdapter { ImageView deleteImage = view.findViewById(R.id.subkey_item_delete_button); deleteImage.setVisibility(View.GONE); - long keyId = cursor.getLong(INDEX_KEY_ID); - vKeyId.setText(KeyFormattingUtils.beautifyKeyId(keyId)); + SubKey subKey = getItem(position); + + vKeyId.setText(KeyFormattingUtils.beautifyKeyId(subKey.key_id())); // may be set with additional "stripped" later on SpannableStringBuilder algorithmStr = new SpannableStringBuilder(); algorithmStr.append(KeyFormattingUtils.getAlgorithmInfo( context, - cursor.getInt(INDEX_ALGORITHM), - cursor.getInt(INDEX_KEY_SIZE), - cursor.getString(INDEX_KEY_CURVE_OID) + subKey.algorithm(), + subKey.key_size(), + subKey.key_curve_oid() )); - SubkeyChange change = mSkpBuilder != null ? mSkpBuilder.getSubkeyChange(keyId) : null; + SubkeyChange change = mSkpBuilder != null ? mSkpBuilder.getSubkeyChange(subKey.key_id()) : null; if (change != null && (change.getDummyStrip() || change.getMoveKeyToSecurityToken())) { if (change.getDummyStrip()) { algorithmStr.append(", "); @@ -197,7 +140,7 @@ public class SubkeysAdapter extends CursorAdapter { algorithmStr.append(boldDivert); } } else { - switch (SecretKeyType.fromNum(cursor.getInt(INDEX_HAS_SECRET))) { + switch (subKey.has_secret()) { case GNU_DUMMY: algorithmStr.append(", "); algorithmStr.append(context.getString(R.string.key_stripped)); @@ -218,7 +161,7 @@ public class SubkeysAdapter extends CursorAdapter { } vKeyDetails.setText(algorithmStr, TextView.BufferType.SPANNABLE); - boolean isMasterKey = cursor.getInt(INDEX_RANK) == 0; + boolean isMasterKey = subKey.rank() == 0; if (isMasterKey) { vKeyId.setTypeface(null, Typeface.BOLD); } else { @@ -226,22 +169,21 @@ public class SubkeysAdapter extends CursorAdapter { } // Set icons according to properties - vCertifyIcon.setVisibility(cursor.getInt(INDEX_CAN_CERTIFY) != 0 ? View.VISIBLE : View.GONE); - vEncryptIcon.setVisibility(cursor.getInt(INDEX_CAN_ENCRYPT) != 0 ? View.VISIBLE : View.GONE); - vSignIcon.setVisibility(cursor.getInt(INDEX_CAN_SIGN) != 0 ? View.VISIBLE : View.GONE); - vAuthenticateIcon.setVisibility(cursor.getInt(INDEX_CAN_AUTHENTICATE) != 0 ? View.VISIBLE : View.GONE); + vCertifyIcon.setVisibility(subKey.can_certify() ? View.VISIBLE : View.GONE); + vEncryptIcon.setVisibility(subKey.can_encrypt() ? View.VISIBLE : View.GONE); + vSignIcon.setVisibility(subKey.can_sign() ? View.VISIBLE : View.GONE); + vAuthenticateIcon.setVisibility(subKey.can_authenticate() ? View.VISIBLE : View.GONE); - boolean isRevoked = cursor.getInt(INDEX_IS_REVOKED) > 0; - boolean isSecure = cursor.getInt(INDEX_IS_SECURE) > 0; + boolean isRevoked = subKey.is_revoked(); Date expiryDate = null; - if (!cursor.isNull(INDEX_EXPIRY)) { - expiryDate = new Date(cursor.getLong(INDEX_EXPIRY) * 1000); + if (subKey.expires()) { + expiryDate = new Date(subKey.expiry() * 1000); } // for edit key if (mSkpBuilder != null) { - boolean revokeThisSubkey = (mSkpBuilder.getMutableRevokeSubKeys().contains(keyId)); + boolean revokeThisSubkey = (mSkpBuilder.getMutableRevokeSubKeys().contains(subKey.key_id())); if (revokeThisSubkey) { if (!isRevoked) { @@ -249,7 +191,7 @@ public class SubkeysAdapter extends CursorAdapter { } } - SaveKeyringParcel.SubkeyChange subkeyChange = mSkpBuilder.getSubkeyChange(keyId); + SaveKeyringParcel.SubkeyChange subkeyChange = mSkpBuilder.getSubkeyChange(subKey.key_id()); if (subkeyChange != null) { if (subkeyChange.getExpiry() == null || subkeyChange.getExpiry() == 0L) { expiryDate = null; @@ -280,37 +222,37 @@ public class SubkeysAdapter extends CursorAdapter { } // if key is expired or revoked... - boolean isInvalid = isRevoked || isExpired || !isSecure; + boolean isInvalid = isRevoked || isExpired || !subKey.is_secure(); if (isInvalid) { vStatus.setVisibility(View.VISIBLE); vCertifyIcon.setColorFilter( - mContext.getResources().getColor(R.color.key_flag_gray), + context.getResources().getColor(R.color.key_flag_gray), PorterDuff.Mode.SRC_IN); vSignIcon.setColorFilter( - mContext.getResources().getColor(R.color.key_flag_gray), + context.getResources().getColor(R.color.key_flag_gray), PorterDuff.Mode.SRC_IN); vEncryptIcon.setColorFilter( - mContext.getResources().getColor(R.color.key_flag_gray), + context.getResources().getColor(R.color.key_flag_gray), PorterDuff.Mode.SRC_IN); vAuthenticateIcon.setColorFilter( - mContext.getResources().getColor(R.color.key_flag_gray), + context.getResources().getColor(R.color.key_flag_gray), PorterDuff.Mode.SRC_IN); if (isRevoked) { vStatus.setImageResource(R.drawable.status_signature_revoked_cutout_24dp); vStatus.setColorFilter( - mContext.getResources().getColor(R.color.key_flag_gray), + context.getResources().getColor(R.color.key_flag_gray), PorterDuff.Mode.SRC_IN); } else if (isExpired) { vStatus.setImageResource(R.drawable.status_signature_expired_cutout_24dp); vStatus.setColorFilter( - mContext.getResources().getColor(R.color.key_flag_gray), + context.getResources().getColor(R.color.key_flag_gray), PorterDuff.Mode.SRC_IN); - } else if (!isSecure) { + } else if (!subKey.is_secure()) { vStatus.setImageResource(R.drawable.status_signature_invalid_cutout_24dp); vStatus.setColorFilter( - mContext.getResources().getColor(R.color.key_flag_gray), + context.getResources().getColor(R.color.key_flag_gray), PorterDuff.Mode.SRC_IN); } } else { @@ -328,36 +270,20 @@ public class SubkeysAdapter extends CursorAdapter { vKeyId.setEnabled(!isInvalid); vKeyDetails.setEnabled(!isInvalid); vKeyExpiry.setEnabled(!isInvalid); - } - @Override - public View newView(Context context, Cursor cursor, ViewGroup parent) { - View view = mInflater.inflate(R.layout.view_key_adv_subkey_item, null); - if (mDefaultTextColor == null) { - TextView keyId = view.findViewById(R.id.subkey_item_key_id); - mDefaultTextColor = keyId.getTextColors(); - } return view; } // Disable selection of items, http://stackoverflow.com/a/4075045 @Override public boolean areAllItemsEnabled() { - if (mSkpBuilder == null) { - return false; - } else { - return super.areAllItemsEnabled(); - } + return mSkpBuilder != null && super.areAllItemsEnabled(); } // Disable selection of items, http://stackoverflow.com/a/4075045 @Override public boolean isEnabled(int position) { - if (mSkpBuilder == null) { - return false; - } else { - return super.isEnabled(position); - } + return mSkpBuilder != null && super.isEnabled(position); } /** Set this adapter into edit mode. This mode displays additional info for @@ -372,6 +298,7 @@ public class SubkeysAdapter extends CursorAdapter { */ public void setEditMode(@Nullable SaveKeyringParcel.Builder builder) { mSkpBuilder = builder; + notifyDataSetChanged(); } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/UserAttributesAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/UserAttributesAdapter.java deleted file mode 100644 index ac9fbd636..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/UserAttributesAdapter.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright (C) 2017 Schürmann & Breitmoser GbR - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sufficientlysecure.keychain.ui.adapter; - -import android.content.Context; -import android.database.Cursor; -import android.support.v4.widget.CursorAdapter; -import android.view.View; - -import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets; - -public abstract class UserAttributesAdapter extends CursorAdapter { - public static final String[] USER_PACKETS_PROJECTION = new String[]{ - UserPackets._ID, - UserPackets.TYPE, - UserPackets.USER_ID, - UserPackets.ATTRIBUTE_DATA, - UserPackets.RANK, - UserPackets.VERIFIED, - UserPackets.IS_PRIMARY, - UserPackets.IS_REVOKED, - UserPackets.NAME, - UserPackets.EMAIL, - UserPackets.COMMENT, - }; - public static final int INDEX_ID = 0; - public static final int INDEX_TYPE = 1; - public static final int INDEX_USER_ID = 2; - public static final int INDEX_ATTRIBUTE_DATA = 3; - public static final int INDEX_RANK = 4; - public static final int INDEX_VERIFIED = 5; - public static final int INDEX_IS_PRIMARY = 6; - public static final int INDEX_IS_REVOKED = 7; - public static final int INDEX_NAME = 8; - public static final int INDEX_EMAIL = 9; - public static final int INDEX_COMMENT = 10; - - public UserAttributesAdapter(Context context, Cursor c, int flags) { - super(context, c, flags); - } - - @Override - public abstract void bindView(View view, Context context, Cursor cursor); - - public String getUserId(int position) { - mCursor.moveToPosition(position); - return mCursor.getString(INDEX_USER_ID); - } - - public boolean getIsRevoked(int position) { - mCursor.moveToPosition(position); - return mCursor.getInt(INDEX_IS_REVOKED) > 0; - } - - public int getIsVerified(int position) { - mCursor.moveToPosition(position); - return mCursor.getInt(INDEX_VERIFIED); - } - -} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/UserIdsAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/UserIdsAdapter.java index e48b79f1e..518a5ac8c 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/UserIdsAdapter.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/UserIdsAdapter.java @@ -17,45 +17,74 @@ package org.sufficientlysecure.keychain.ui.adapter; -import android.app.Activity; + +import java.util.List; + import android.content.Context; -import android.database.Cursor; import android.graphics.Typeface; -import android.net.Uri; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import android.support.v4.content.CursorLoader; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.BaseAdapter; import android.widget.ImageView; import android.widget.TextView; import android.widget.ViewAnimator; import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.provider.KeychainContract.Certs; -import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets; +import org.sufficientlysecure.keychain.model.UserPacket.UserId; +import org.sufficientlysecure.keychain.pgp.CanonicalizedKeyRing.VerificationStatus; import org.sufficientlysecure.keychain.service.SaveKeyringParcel; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils.State; -public class UserIdsAdapter extends UserAttributesAdapter { - protected LayoutInflater mInflater; +// TODO move to RecyclerView +public class UserIdsAdapter extends BaseAdapter { + private Context context; + private List data; private SaveKeyringParcel.Builder mSkpBuilder; private boolean mShowStatusImages; + private LayoutInflater layoutInflater; - public UserIdsAdapter(Context context, Cursor c, int flags, boolean showStatusImages) { - super(context, c, flags); - mInflater = LayoutInflater.from(context); + public UserIdsAdapter(Context context, boolean showStatusImages) { + super(); + this.context = context; + this.layoutInflater = LayoutInflater.from(context); mShowStatusImages = showStatusImages; } - public UserIdsAdapter(Context context, Cursor c, int flags) { - this(context, c, flags, true); + @Override + public int getCount() { + return data != null ? data.size() : 0; } @Override - public void bindView(View view, Context context, Cursor cursor) { + public UserId getItem(int position) { + return data.get(position); + } + + @Override + public long getItemId(int position) { + return data.get(position).master_key_id(); + } + + public void setData(List data) { + this.data = data; + notifyDataSetChanged(); + } + + @NonNull + @Override + public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { + View view; + if (convertView != null) { + view = convertView; + } else { + view = layoutInflater.inflate(R.layout.view_key_adv_user_id_item, parent, false); + } + TextView vName = view.findViewById(R.id.user_id_item_name); TextView vAddress = view.findViewById(R.id.user_id_item_address); TextView vComment = view.findViewById(R.id.user_id_item_comment); @@ -65,37 +94,35 @@ public class UserIdsAdapter extends UserAttributesAdapter { ImageView vDeleteButton = view.findViewById(R.id.user_id_item_delete_button); vDeleteButton.setVisibility(View.GONE); // not used - String userId = cursor.getString(INDEX_USER_ID); - String name = cursor.getString(INDEX_NAME); - String email = cursor.getString(INDEX_EMAIL); - String comment = cursor.getString(INDEX_COMMENT); - if (name != null) { - vName.setText(name); + UserId userId = getItem(position); + + if (userId.name() != null) { + vName.setText(userId.name()); } else { vName.setText(R.string.user_id_no_name); } - if (email != null) { - vAddress.setText(email); + if (userId.email() != null) { + vAddress.setText(userId.email()); vAddress.setVisibility(View.VISIBLE); } else { vAddress.setVisibility(View.GONE); } - if (comment != null) { - vComment.setText(comment); + if (userId.comment() != null) { + vComment.setText(userId.comment()); vComment.setVisibility(View.VISIBLE); } else { vComment.setVisibility(View.GONE); } - boolean isPrimary = cursor.getInt(INDEX_IS_PRIMARY) != 0; - boolean isRevoked = cursor.getInt(INDEX_IS_REVOKED) > 0; + boolean isPrimary = userId.is_primary(); + boolean isRevoked = userId.is_revoked(); // for edit key if (mSkpBuilder != null) { String changePrimaryUserId = mSkpBuilder.getChangePrimaryUserId(); boolean changeAnyPrimaryUserId = (changePrimaryUserId != null); - boolean changeThisPrimaryUserId = (changeAnyPrimaryUserId && changePrimaryUserId.equals(userId)); - boolean revokeThisUserId = (mSkpBuilder.getMutableRevokeUserIds().contains(userId)); + boolean changeThisPrimaryUserId = (changeAnyPrimaryUserId && changePrimaryUserId.equals(userId.user_id())); + boolean revokeThisUserId = (mSkpBuilder.getMutableRevokeUserIds().contains(userId.user_id())); // only if primary user id will be changed // (this is not triggered if the user id is currently the primary one) @@ -117,7 +144,7 @@ public class UserIdsAdapter extends UserAttributesAdapter { if (isRevoked) { // set revocation icon (can this even be primary?) - KeyFormattingUtils.setStatusImage(mContext, vVerified, null, State.REVOKED, R.color.key_flag_gray); + KeyFormattingUtils.setStatusImage(context, vVerified, null, State.REVOKED, R.color.key_flag_gray); // disable revoked user ids vName.setEnabled(false); @@ -136,24 +163,25 @@ public class UserIdsAdapter extends UserAttributesAdapter { vAddress.setTypeface(null, Typeface.NORMAL); } - int isVerified = cursor.getInt(INDEX_VERIFIED); + VerificationStatus isVerified = getVerificationStatus(position); switch (isVerified) { - case Certs.VERIFIED_SECRET: - KeyFormattingUtils.setStatusImage(mContext, vVerified, null, State.VERIFIED, KeyFormattingUtils.DEFAULT_COLOR); + case VERIFIED_SECRET: + KeyFormattingUtils.setStatusImage(context, vVerified, null, State.VERIFIED, KeyFormattingUtils.DEFAULT_COLOR); break; - case Certs.VERIFIED_SELF: - KeyFormattingUtils.setStatusImage(mContext, vVerified, null, State.UNVERIFIED, KeyFormattingUtils.DEFAULT_COLOR); + case VERIFIED_SELF: + KeyFormattingUtils.setStatusImage(context, vVerified, null, State.UNVERIFIED, KeyFormattingUtils.DEFAULT_COLOR); break; default: - KeyFormattingUtils.setStatusImage(mContext, vVerified, null, State.INVALID, KeyFormattingUtils.DEFAULT_COLOR); + KeyFormattingUtils.setStatusImage(context, vVerified, null, State.INVALID, KeyFormattingUtils.DEFAULT_COLOR); break; } } + + return view; } public boolean getIsRevokedPending(int position) { - mCursor.moveToPosition(position); - String userId = mCursor.getString(INDEX_USER_ID); + String userId = getUserId(position); boolean isRevokedPending = false; if (mSkpBuilder != null) { @@ -180,18 +208,15 @@ public class UserIdsAdapter extends UserAttributesAdapter { mSkpBuilder = saveKeyringParcel; } - @Override - public View newView(Context context, Cursor cursor, ViewGroup parent) { - return mInflater.inflate(R.layout.view_key_adv_user_id_item, null); + public String getUserId(int position) { + return data.get(position).user_id(); } - // don't show revoked user ids, irrelevant for average users - public static final String USER_IDS_WHERE = UserPackets.IS_REVOKED + " = 0"; - - public static CursorLoader createLoader(Context context, Uri dataUri) { - Uri baseUri = UserPackets.buildUserIdsUri(dataUri); - return new CursorLoader(context, baseUri, - UserIdsAdapter.USER_PACKETS_PROJECTION, USER_IDS_WHERE, null, null); + public boolean getIsRevoked(int position) { + return data.get(position).is_revoked(); } + public VerificationStatus getVerificationStatus(int position) { + return data.get(position).verified(); + } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseActivity.java index bb1949537..626bd8fb9 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseActivity.java @@ -33,7 +33,6 @@ import android.widget.TextView; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.service.ContactSyncAdapterService; -import org.sufficientlysecure.keychain.service.KeyserverSyncAdapterService; import org.sufficientlysecure.keychain.ui.util.ThemeChanger; /** @@ -77,7 +76,6 @@ public abstract class BaseActivity extends AppCompatActivity { } public static void onResumeChecks(Context context) { - KeyserverSyncAdapterService.cancelUpdates(context); // in case user has disabled sync from Android account settings ContactSyncAdapterService.deleteIfSyncDisabled(context); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/LoaderFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/LoaderFragment.java deleted file mode 100644 index 27bf1ec1c..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/LoaderFragment.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright (C) 2017 Schürmann & Breitmoser GbR - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sufficientlysecure.keychain.ui.base; - -import android.os.Bundle; -import android.support.v4.app.Fragment; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.view.animation.AnimationUtils; - -import org.sufficientlysecure.keychain.R; - -/** - * This is a fragment helper class, which implements a generic - * progressbar/container view. - *

- * To use it in a fragment, simply subclass, use onCreateView to create the - * layout's root view, and ues getContainer() as root view of your subclass. - * The layout shows a progress bar by default, and can be switched to the - * actual contents by calling setContentShown(). - */ -public abstract class LoaderFragment extends Fragment { - private boolean mContentShown; - private View mProgressContainer; - private ViewGroup mContainer; - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View root = inflater.inflate(R.layout.loader_layout, container, false); - - mContentShown = true; - mContainer = root.findViewById(R.id.loader_container); - mProgressContainer = root.findViewById(R.id.loader_progress); - - // content is not shown (by visibility statuses in the layout files) - mContentShown = false; - - return root; - } - - protected ViewGroup getContainer() { - return mContainer; - } - - public void setContentShown(boolean shown, boolean animate) { - if (mContentShown == shown) { - return; - } - mContentShown = shown; - if (shown) { - if (animate) { - mProgressContainer.startAnimation(AnimationUtils.loadAnimation( - getActivity(), android.R.anim.fade_out)); - mContainer.startAnimation(AnimationUtils.loadAnimation( - getActivity(), android.R.anim.fade_in)); - } - mProgressContainer.setVisibility(View.GONE); - mContainer.setVisibility(View.VISIBLE); - } else { - if (animate) { - mProgressContainer.startAnimation(AnimationUtils.loadAnimation( - getActivity(), android.R.anim.fade_in)); - mContainer.startAnimation(AnimationUtils.loadAnimation( - getActivity(), android.R.anim.fade_out)); - } - mProgressContainer.setVisibility(View.VISIBLE); - mContainer.setVisibility(View.INVISIBLE); - } - } - - public void setContentShown(boolean shown) { - setContentShown(shown, true); - } - - public void setContentShownNoAnimation(boolean shown) { - setContentShown(shown, false); - } -} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/RecyclerFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/RecyclerFragment.java index 171db0849..091d073be 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/RecyclerFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/RecyclerFragment.java @@ -16,10 +16,10 @@ package org.sufficientlysecure.keychain.ui.base; + import android.content.Context; import android.os.Bundle; import android.os.Handler; -import android.support.annotation.LayoutRes; import android.support.v4.app.Fragment; import android.support.v7.widget.RecyclerView; import android.view.Gravity; @@ -32,7 +32,7 @@ import android.widget.LinearLayout; import android.widget.ProgressBar; import android.widget.TextView; -import org.sufficientlysecure.keychain.ui.util.FormattingUtils; +import org.sufficientlysecure.keychain.R; import timber.log.Timber; @@ -124,9 +124,16 @@ public class RecyclerFragment extends Fragment { ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); - FrameLayout listContainer = new FrameLayout(context); + LinearLayout listContainer = new LinearLayout(context); + listContainer.setOrientation(LinearLayout.VERTICAL); listContainer.setId(INTERNAL_LIST_CONTAINER_ID); + LinearLayout headerLayout = new LinearLayout(context); + headerLayout.setId(R.id.headerlayout); + + listContainer.addView(headerLayout, new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + TextView textView = new TextView(context); textView.setId(INTERNAL_EMPTY_VIEW_ID); textView.setGravity(Gravity.CENTER); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/bindings/ImportKeysBindingsUtils.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/bindings/ImportKeysBindingsUtils.java index 3f6aea805..c88d31aac 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/bindings/ImportKeysBindingsUtils.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/bindings/ImportKeysBindingsUtils.java @@ -31,7 +31,8 @@ public class ImportKeysBindingsUtils { public static Highlighter getHighlighter(Context context, String query) { Highlighter highlighter = highlighterCache.get(query); if (highlighter == null) { - highlighter = new Highlighter(context, query); + highlighter = new Highlighter(context); + highlighter.setQuery(query); highlighterCache.put(query, highlighter); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/chips/EncryptRecipientChipAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/chips/EncryptRecipientChipAdapter.java new file mode 100644 index 000000000..929200dad --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/chips/EncryptRecipientChipAdapter.java @@ -0,0 +1,88 @@ +package org.sufficientlysecure.keychain.ui.chips; + + +import android.content.Context; +import android.support.v4.content.ContextCompat; +import android.support.v7.widget.RecyclerView; +import android.view.View; +import android.view.ViewGroup; + +import org.sufficientlysecure.materialchips.ChipView; +import org.sufficientlysecure.materialchips.ChipsInput; +import org.sufficientlysecure.materialchips.adapter.ChipsAdapter; +import org.sufficientlysecure.materialchips.simple.SimpleChip; +import org.sufficientlysecure.materialchips.util.ViewUtil; +import org.sufficientlysecure.materialchips.views.DetailedChipView; + +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.model.SubKey; +import org.sufficientlysecure.keychain.ui.chips.EncryptRecipientChipAdapter.ItemViewHolder; +import org.sufficientlysecure.keychain.ui.chips.EncryptRecipientChipsInput.EncryptRecipientChip; + + +public class EncryptRecipientChipAdapter extends ChipsAdapter { + EncryptRecipientChipAdapter(Context context, ChipsInput chipsInput) { + super(context, chipsInput); + } + + @Override + public ItemViewHolder onCreateChipViewHolder(ViewGroup parent, int viewType) { + int padding = ViewUtil.dpToPx(4); + ChipView chipView = new ChipView.Builder(context) + // .labelColor(mChipLabelColor) + // .deletable(mChipDeletable) + // .deleteIcon(mChipDeleteIcon) + // .deleteIconColor(mChipDeleteIconColor) + .build(); + chipView.setPadding(padding, padding, padding, padding); + + return new ItemViewHolder(chipView); + } + + @Override + public void onBindChipViewHolder(ItemViewHolder holder, int position) { + EncryptRecipientChip chip = getItem(position); + holder.chipView.inflate(simpleChipFromKeyInfo(chip.keyInfo)); + handleClickOnEditText(holder.chipView, position); + } + + @Override + public DetailedChipView getDetailedChipView(EncryptRecipientChip chip) { + return new DetailedChipView.Builder(context) + .chip(simpleChipFromKeyInfo(chip.keyInfo)) + .backgroundColor(ContextCompat.getColorStateList(context, R.color.cardview_light_background)) + .build(); + } + + class ItemViewHolder extends RecyclerView.ViewHolder { + private final ChipView chipView; + + ItemViewHolder(View view) { + super(view); + chipView = (ChipView) view; + } + } + + private SimpleChip simpleChipFromKeyInfo(SubKey.UnifiedKeyInfo keyInfo) { + String name; + String email; + if (keyInfo.name() == null) { + if (keyInfo.email() != null) { + name = keyInfo.email(); + email = null; + } else { + name = context.getString(R.string.user_id_no_name); + email = null; + } + } else { + name = keyInfo.name(); + if (keyInfo.email() != null) { + email = keyInfo.email(); + } else { + email = null; + } + } + + return new SimpleChip(name, email); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/chips/EncryptRecipientChipsInput.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/chips/EncryptRecipientChipsInput.java new file mode 100644 index 000000000..475366a33 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/chips/EncryptRecipientChipsInput.java @@ -0,0 +1,77 @@ +package org.sufficientlysecure.keychain.ui.chips; + + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import android.content.Context; +import android.util.AttributeSet; + +import org.sufficientlysecure.materialchips.ChipsInput; +import org.sufficientlysecure.materialchips.adapter.FilterableAdapter.FilterableItem; +import org.sufficientlysecure.keychain.model.SubKey.UnifiedKeyInfo; +import org.sufficientlysecure.keychain.ui.chips.EncryptRecipientChipsInput.EncryptRecipientChip; + + +public class EncryptRecipientChipsInput extends ChipsInput { + private long[] preselectedKeyIds; + + public EncryptRecipientChipsInput(Context context) { + super(context); + init(); + } + + public EncryptRecipientChipsInput(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + private void init() { + EncryptRecipientChipAdapter chipsAdapter = new EncryptRecipientChipAdapter(getContext(), this); + setChipsAdapter(chipsAdapter); + } + + public void setData(List keyInfoChips) { + EncryptRecipientDropdownAdapter chipDropdownAdapter = new EncryptRecipientDropdownAdapter(getContext(), keyInfoChips); + setChipDropdownAdapter(chipDropdownAdapter); + + if (preselectedKeyIds != null) { + Arrays.sort(preselectedKeyIds); + ArrayList preselectedChips = new ArrayList<>(); + for (EncryptRecipientChip keyInfoChip : keyInfoChips) { + if (Arrays.binarySearch(preselectedKeyIds, keyInfoChip.keyInfo.master_key_id()) >= 0) { + preselectedChips.add(keyInfoChip); + } + } + addChips(preselectedChips); + preselectedKeyIds = null; + } + } + + public void setPreSelectedKeyIds(long[] preselectedEncryptionKeyIds) { + this.preselectedKeyIds = preselectedEncryptionKeyIds; + } + + public static class EncryptRecipientChip implements FilterableItem { + public final UnifiedKeyInfo keyInfo; + + EncryptRecipientChip(UnifiedKeyInfo keyInfo) { + this.keyInfo = keyInfo; + } + + @Override + public long getId() { + return keyInfo.master_key_id(); + } + + @Override + public boolean isKeptForConstraint(CharSequence constraint) { + return keyInfo.uidSearchString().contains(constraint); + } + } + + public static EncryptRecipientChip chipFromUnifiedKeyInfo(UnifiedKeyInfo keyInfo) { + return new EncryptRecipientChip(keyInfo); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/chips/EncryptRecipientDropdownAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/chips/EncryptRecipientDropdownAdapter.java new file mode 100644 index 000000000..3ca48d72b --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/chips/EncryptRecipientDropdownAdapter.java @@ -0,0 +1,69 @@ +package org.sufficientlysecure.keychain.ui.chips; + + +import java.util.List; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import org.sufficientlysecure.materialchips.ChipsInput; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.ui.chips.EncryptRecipientChipsInput.EncryptRecipientChip; +import org.sufficientlysecure.keychain.ui.chips.EncryptRecipientDropdownAdapter.ItemViewHolder; +import org.sufficientlysecure.keychain.ui.util.KeyInfoFormatter; + + +public class EncryptRecipientDropdownAdapter extends ChipsInput.ChipDropdownAdapter { + private final LayoutInflater layoutInflater; + private final KeyInfoFormatter keyInfoFormatter; + + EncryptRecipientDropdownAdapter(Context context, List keyInfoChips) { + super(keyInfoChips); + + layoutInflater = LayoutInflater.from(context); + keyInfoFormatter = new KeyInfoFormatter(context); + } + + class ItemViewHolder extends RecyclerView.ViewHolder { + private final TextView vMainUserId; + private final TextView vMainUserIdRest; + private final TextView vCreationDate; + private final ImageView vStatusIcon; + + ItemViewHolder(View view) { + super(view); + + vMainUserId = itemView.findViewById(R.id.key_list_item_name); + vMainUserIdRest = itemView.findViewById(R.id.key_list_item_email); + vStatusIcon = itemView.findViewById(R.id.key_list_item_status_icon); + vCreationDate = itemView.findViewById(R.id.key_list_item_creation); + } + + public void bind(EncryptRecipientChip chip) { + keyInfoFormatter.setKeyInfo(chip.keyInfo); + + keyInfoFormatter.formatUserId(vMainUserId, vMainUserIdRest); + keyInfoFormatter.formatCreationDate(vCreationDate); + keyInfoFormatter.formatStatusIcon(vStatusIcon); + } + } + + @NonNull + @Override + public ItemViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = layoutInflater.inflate(R.layout.key_list_item, parent, false); + return new ItemViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull ItemViewHolder holder, int position) { + EncryptRecipientChip chip = getItem(position); + holder.bind(chip); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/UserIdInfoDialogFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/UserIdInfoDialogFragment.java index db20f7d1f..3d6122a68 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/UserIdInfoDialogFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/UserIdInfoDialogFragment.java @@ -17,6 +17,7 @@ package org.sufficientlysecure.keychain.ui.dialog; + import android.app.Activity; import android.app.Dialog; import android.content.DialogInterface; @@ -24,7 +25,6 @@ import android.os.Bundle; import android.support.v4.app.DialogFragment; import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.provider.KeychainContract; public class UserIdInfoDialogFragment extends DialogFragment { private static final String ARG_IS_REVOKED = "is_revoked"; @@ -33,11 +33,11 @@ public class UserIdInfoDialogFragment extends DialogFragment { /** * Creates new instance of this dialog fragment */ - public static UserIdInfoDialogFragment newInstance(boolean isRevoked, int isVerified) { + public static UserIdInfoDialogFragment newInstance(boolean isRevoked, boolean isVerified) { UserIdInfoDialogFragment frag = new UserIdInfoDialogFragment(); Bundle args = new Bundle(); args.putBoolean(ARG_IS_REVOKED, isRevoked); - args.putInt(ARG_IS_VERIFIED, isVerified); + args.putBoolean(ARG_IS_VERIFIED, isVerified); frag.setArguments(args); @@ -51,7 +51,7 @@ public class UserIdInfoDialogFragment extends DialogFragment { public Dialog onCreateDialog(Bundle savedInstanceState) { final Activity activity = getActivity(); - int isVerified = getArguments().getInt(ARG_IS_VERIFIED); + boolean isVerified = getArguments().getBoolean(ARG_IS_VERIFIED); boolean isRevoked = getArguments().getBoolean(ARG_IS_REVOKED); CustomAlertDialogBuilder alert = new CustomAlertDialogBuilder(activity); @@ -62,19 +62,12 @@ public class UserIdInfoDialogFragment extends DialogFragment { title = getString(R.string.user_id_info_revoked_title); message = getString(R.string.user_id_info_revoked_text); } else { - switch (isVerified) { - case KeychainContract.Certs.VERIFIED_SECRET: - title = getString(R.string.user_id_info_certified_title); - message = getString(R.string.user_id_info_certified_text); - break; - case KeychainContract.Certs.VERIFIED_SELF: - title = getString(R.string.user_id_info_uncertified_title); - message = getString(R.string.user_id_info_uncertified_text); - break; - default: - title = getString(R.string.user_id_info_invalid_title); - message = getString(R.string.user_id_info_invalid_text); - break; + if (isVerified) { + title = getString(R.string.user_id_info_certified_title); + message = getString(R.string.user_id_info_certified_text); + } else { + title = getString(R.string.user_id_info_uncertified_title); + message = getString(R.string.user_id_info_uncertified_text); } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/GenericViewModel.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/GenericViewModel.java new file mode 100644 index 000000000..12a46720f --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/GenericViewModel.java @@ -0,0 +1,24 @@ +package org.sufficientlysecure.keychain.ui.keyview; + + +import android.arch.lifecycle.LiveData; +import android.arch.lifecycle.ViewModel; +import android.content.Context; + +import org.sufficientlysecure.keychain.livedata.GenericLiveData; +import org.sufficientlysecure.keychain.livedata.GenericLiveData.GenericDataLoader; +import org.sufficientlysecure.keychain.daos.DatabaseNotifyManager; + + +/** A simple generic ViewModel that can be used if exactly one field of data needs to be stored. */ +public class GenericViewModel extends ViewModel { + private LiveData genericLiveData; + + public LiveData getGenericLiveData(Context context, GenericDataLoader func) { + if (genericLiveData == null) { + genericLiveData = new GenericLiveData<>(context, DatabaseNotifyManager.getNotifyUriAllKeys(), func); + } + // noinspection unchecked + return genericLiveData; + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/KeyFragmentViewModel.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/KeyFragmentViewModel.java new file mode 100644 index 000000000..3e3c58466 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/KeyFragmentViewModel.java @@ -0,0 +1,70 @@ +package org.sufficientlysecure.keychain.ui.keyview; + + +import java.util.List; + +import android.arch.lifecycle.LiveData; +import android.arch.lifecycle.Transformations; +import android.arch.lifecycle.ViewModel; +import android.content.Context; + +import org.sufficientlysecure.keychain.livedata.GenericLiveData; +import org.sufficientlysecure.keychain.model.KeyMetadata; +import org.sufficientlysecure.keychain.model.SubKey.UnifiedKeyInfo; +import org.sufficientlysecure.keychain.daos.KeyMetadataDao; +import org.sufficientlysecure.keychain.ui.keyview.loader.IdentityDao; +import org.sufficientlysecure.keychain.ui.keyview.loader.IdentityDao.IdentityInfo; +import org.sufficientlysecure.keychain.ui.keyview.loader.SubkeyStatusDao; +import org.sufficientlysecure.keychain.ui.keyview.loader.SubkeyStatusDao.KeySubkeyStatus; +import org.sufficientlysecure.keychain.ui.keyview.loader.SystemContactDao; +import org.sufficientlysecure.keychain.ui.keyview.loader.SystemContactDao.SystemContactInfo; + + +public class KeyFragmentViewModel extends ViewModel { + private LiveData> identityInfo; + private LiveData subkeyStatus; + private LiveData systemContactInfo; + private LiveData keyserverStatus; + + LiveData> getIdentityInfo(Context context, LiveData unifiedKeyInfoLiveData, + boolean showLinkedIds) { + if (identityInfo == null) { + IdentityDao identityDao = IdentityDao.getInstance(context); + identityInfo = Transformations.switchMap(unifiedKeyInfoLiveData, + (unifiedKeyInfo) -> unifiedKeyInfo == null ? null : new GenericLiveData<>(context, + () -> identityDao.getIdentityInfos(unifiedKeyInfo.master_key_id(), showLinkedIds))); + } + return identityInfo; + } + + LiveData getSubkeyStatus(Context context, LiveData unifiedKeyInfoLiveData) { + if (subkeyStatus == null) { + SubkeyStatusDao subkeyStatusDao = SubkeyStatusDao.getInstance(context); + subkeyStatus = Transformations.switchMap(unifiedKeyInfoLiveData, + (unifiedKeyInfo) -> unifiedKeyInfo == null ? null : new GenericLiveData<>(context, + () -> subkeyStatusDao.getSubkeyStatus(unifiedKeyInfo.master_key_id()))); + } + return subkeyStatus; + } + + LiveData getSystemContactInfo(Context context, LiveData unifiedKeyInfoLiveData) { + if (systemContactInfo == null) { + SystemContactDao systemContactDao = SystemContactDao.getInstance(context); + systemContactInfo = Transformations.switchMap(unifiedKeyInfoLiveData, + (unifiedKeyInfo) -> unifiedKeyInfo == null ? null : new GenericLiveData<>(context, + () -> systemContactDao.getSystemContactInfo(unifiedKeyInfo.master_key_id(), + unifiedKeyInfo.has_any_secret()))); + } + return systemContactInfo; + } + + LiveData getKeyserverStatus(Context context, LiveData unifiedKeyInfoLiveData) { + if (keyserverStatus == null) { + KeyMetadataDao keyMetadataDao = KeyMetadataDao.create(context); + keyserverStatus = Transformations.switchMap(unifiedKeyInfoLiveData, + (unifiedKeyInfo) -> unifiedKeyInfo == null ? null : new GenericLiveData<>(context, + () -> keyMetadataDao.getKeyMetadata(unifiedKeyInfo.master_key_id()))); + } + return keyserverStatus; + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/LinkedIdViewFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/LinkedIdViewFragment.java index cfa9b283f..493403ca5 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/LinkedIdViewFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/LinkedIdViewFragment.java @@ -18,14 +18,15 @@ package org.sufficientlysecure.keychain.ui.keyview; -import java.io.IOException; import java.util.Collections; +import java.util.List; +import android.arch.lifecycle.LiveData; +import android.arch.lifecycle.ViewModel; +import android.arch.lifecycle.ViewModelProviders; import android.content.Context; import android.content.Intent; -import android.database.Cursor; import android.graphics.PorterDuff; -import android.net.Uri; import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; @@ -36,10 +37,7 @@ import android.support.annotation.Nullable; import android.support.v4.app.FragmentActivity; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentManager.OnBackStackChangedListener; -import android.support.v4.app.LoaderManager; import android.support.v4.content.ContextCompat; -import android.support.v4.content.CursorLoader; -import android.support.v4.content.Loader; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -54,37 +52,36 @@ import org.sufficientlysecure.keychain.linked.LinkedAttribute; import org.sufficientlysecure.keychain.linked.LinkedResource; import org.sufficientlysecure.keychain.linked.LinkedTokenResource; import org.sufficientlysecure.keychain.linked.UriAttribute; +import org.sufficientlysecure.keychain.daos.CertificationDao; +import org.sufficientlysecure.keychain.livedata.GenericLiveData; +import org.sufficientlysecure.keychain.model.Certification.CertDetails; +import org.sufficientlysecure.keychain.model.SubKey.UnifiedKeyInfo; import org.sufficientlysecure.keychain.operations.results.LinkedVerifyResult; import org.sufficientlysecure.keychain.operations.results.OperationResult; -import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; -import org.sufficientlysecure.keychain.provider.KeyRepository; -import org.sufficientlysecure.keychain.provider.KeychainContract.Certs; -import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets; -import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables; +import org.sufficientlysecure.keychain.daos.KeyRepository; +import org.sufficientlysecure.keychain.daos.KeyRepository.NotFoundException; import org.sufficientlysecure.keychain.service.CertifyActionsParcel; import org.sufficientlysecure.keychain.service.CertifyActionsParcel.CertifyAction; import org.sufficientlysecure.keychain.ui.adapter.IdentityAdapter; -import org.sufficientlysecure.keychain.ui.adapter.UserIdsAdapter; import org.sufficientlysecure.keychain.ui.base.CryptoOperationFragment; import org.sufficientlysecure.keychain.ui.keyview.LinkedIdViewFragment.ViewHolder.VerifyState; +import org.sufficientlysecure.keychain.ui.keyview.loader.IdentityDao; +import org.sufficientlysecure.keychain.ui.keyview.loader.IdentityDao.LinkedIdInfo; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils.State; import org.sufficientlysecure.keychain.ui.util.Notify; import org.sufficientlysecure.keychain.ui.util.Notify.Style; import org.sufficientlysecure.keychain.ui.util.SubtleAttentionSeeker; import org.sufficientlysecure.keychain.ui.widget.CertListWidget; -import org.sufficientlysecure.keychain.ui.widget.CertifyKeySpinner; +import org.sufficientlysecure.keychain.ui.widget.KeySpinner; import timber.log.Timber; -public class LinkedIdViewFragment extends CryptoOperationFragment implements - LoaderManager.LoaderCallbacks, OnBackStackChangedListener { +public class LinkedIdViewFragment extends CryptoOperationFragment implements OnBackStackChangedListener { - private static final String ARG_DATA_URI = "data_uri"; private static final String ARG_LID_RANK = "rank"; private static final String ARG_IS_SECRET = "verified"; private static final String ARG_MASTER_KEY_ID = "master_key_id"; - private static final int LOADER_ID_LINKED_ID = 1; private long masterKeyId; private boolean isSecret; @@ -94,17 +91,14 @@ public class LinkedIdViewFragment extends CryptoOperationFragment implements private AsyncTask taskInProgress; - private Uri dataUri; private ViewHolder viewHolder; private int lidRank; private long certifyKeyId; - public static LinkedIdViewFragment newInstance(Uri dataUri, int rank, - boolean isSecret, long masterKeyId) throws IOException { + public static LinkedIdViewFragment newInstance(long masterKeyId, int rank, boolean isSecret) { LinkedIdViewFragment frag = new LinkedIdViewFragment(); Bundle args = new Bundle(); - args.putParcelable(ARG_DATA_URI, dataUri); args.putInt(ARG_LID_RANK, rank); args.putBoolean(ARG_IS_SECRET, isSecret); args.putLong(ARG_MASTER_KEY_ID, masterKeyId); @@ -124,57 +118,31 @@ public class LinkedIdViewFragment extends CryptoOperationFragment implements super.onCreate(savedInstanceState); Bundle args = getArguments(); - dataUri = args.getParcelable(ARG_DATA_URI); lidRank = args.getInt(ARG_LID_RANK); isSecret = args.getBoolean(ARG_IS_SECRET); masterKeyId = args.getLong(ARG_MASTER_KEY_ID); - - getLoaderManager().initLoader(LOADER_ID_LINKED_ID, null, this); - } @Override - public Loader onCreateLoader(int id, Bundle args) { - switch (id) { - case LOADER_ID_LINKED_ID: - return new CursorLoader(getContext(), dataUri, - UserIdsAdapter.USER_PACKETS_PROJECTION, - Tables.USER_PACKETS + "." + UserPackets.RANK - + " = " + Integer.toString(lidRank), null, null); - default: - return null; - } + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + LinkedIdViewModel viewModel = ViewModelProviders.of(this).get(LinkedIdViewModel.class); + viewModel.getLinkedIdInfo(requireContext(), masterKeyId, lidRank).observe(this, this::onLinkedIdInfoLoaded); + viewModel.getCertifyingKeys(requireContext()).observe(this, viewHolder.vKeySpinner::setData); } - @Override - public void onLoadFinished(Loader loader, Cursor cursor) { - switch (loader.getId()) { - case LOADER_ID_LINKED_ID: - - // Nothing to load means break if we are *expected* to load - if (!cursor.moveToFirst()) { - // Or just ignore, this is probably some intermediate state during certify - break; - } - - try { - int certStatus = cursor.getInt(UserIdsAdapter.INDEX_VERIFIED); - - byte[] data = cursor.getBlob(UserIdsAdapter.INDEX_ATTRIBUTE_DATA); - UriAttribute linkedId = LinkedAttribute.fromAttributeData(data); - - loadIdentity(linkedId, certStatus); - - } catch (IOException e) { - Timber.e(e, "error parsing identity"); - Notify.create(getActivity(), "Error parsing identity!", - Notify.LENGTH_LONG, Style.ERROR).show(); - finishFragment(); - } - - break; + private void onLinkedIdInfoLoaded(LinkedIdInfo linkedIdInfo) { + if (linkedIdInfo == null) { + Timber.e("error loading identity"); + Notify.create(getActivity(), "Error loading linked identity!", + Notify.LENGTH_LONG, Style.ERROR).show(); + finishFragment(); + return; } + + loadIdentity(linkedIdInfo.getLinkedAttribute(), linkedIdInfo.isVerified()); } public void finishFragment() { @@ -185,28 +153,19 @@ public class LinkedIdViewFragment extends CryptoOperationFragment implements }); } - private void loadIdentity(UriAttribute linkedId, int certStatus) { + private void loadIdentity(LinkedAttribute linkedId, boolean isVerified) { this.linkedId = linkedId; - if (this.linkedId instanceof LinkedAttribute) { - LinkedResource res = ((LinkedAttribute) this.linkedId).mResource; - linkedResource = (LinkedTokenResource) res; - } + LinkedResource res = ((LinkedAttribute) this.linkedId).mResource; + linkedResource = (LinkedTokenResource) res; if (!isSecret) { - switch (certStatus) { - case Certs.VERIFIED_SECRET: - KeyFormattingUtils.setStatusImage(getContext(), viewHolder.mLinkedIdHolder.vVerified, - null, State.VERIFIED, KeyFormattingUtils.DEFAULT_COLOR); - break; - case Certs.VERIFIED_SELF: - KeyFormattingUtils.setStatusImage(getContext(), viewHolder.mLinkedIdHolder.vVerified, - null, State.UNVERIFIED, KeyFormattingUtils.DEFAULT_COLOR); - break; - default: - KeyFormattingUtils.setStatusImage(getContext(), viewHolder.mLinkedIdHolder.vVerified, - null, State.INVALID, KeyFormattingUtils.DEFAULT_COLOR); - break; + if (isVerified) { + KeyFormattingUtils.setStatusImage(getContext(), viewHolder.mLinkedIdHolder.vVerified, + null, State.VERIFIED, KeyFormattingUtils.DEFAULT_COLOR); + } else { + KeyFormattingUtils.setStatusImage(getContext(), viewHolder.mLinkedIdHolder.vVerified, + null, State.UNVERIFIED, KeyFormattingUtils.DEFAULT_COLOR); } } else { viewHolder.mLinkedIdHolder.vVerified.setImageResource(R.drawable.octo_link_24dp); @@ -216,13 +175,6 @@ public class LinkedIdViewFragment extends CryptoOperationFragment implements setShowVerifying(false); - // no resource, nothing further we can do… - if (linkedResource == null) { - viewHolder.vButtonView.setVisibility(View.GONE); - viewHolder.vButtonVerify.setVisibility(View.GONE); - return; - } - if (linkedResource.isViewable()) { viewHolder.vButtonView.setVisibility(View.VISIBLE); viewHolder.vButtonView.setOnClickListener(v -> { @@ -238,11 +190,6 @@ public class LinkedIdViewFragment extends CryptoOperationFragment implements } - @Override - public void onLoaderReset(Loader loader) { - - } - static class ViewHolder { private final View vButtonView; private final ViewAnimator vVerifyingContainer; @@ -252,7 +199,7 @@ public class LinkedIdViewFragment extends CryptoOperationFragment implements private ViewAnimator vButtonSwitcher; private CertListWidget vLinkedCerts; - private CertifyKeySpinner vKeySpinner; + private KeySpinner vKeySpinner; private final View vButtonVerify; private final View vButtonRetry; private final View vButtonConfirm; @@ -297,6 +244,7 @@ public class LinkedIdViewFragment extends CryptoOperationFragment implements if (!isSecret) { showButton(2); if (!vKeySpinner.isSingleEntry()) { + vKeySpinner.setShowNone(R.string.choice_select_cert); vKeySpinnerContainer.setVisibility(View.VISIBLE); } } else { @@ -392,33 +340,36 @@ public class LinkedIdViewFragment extends CryptoOperationFragment implements @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup superContainer, Bundle savedInstanceState) { - View root = inflater.inflate(R.layout.linked_id_view_fragment, null); + View root = inflater.inflate(R.layout.linked_id_view_fragment, superContainer, false); + Context context = getContext(); + if (context == null) { + throw new NullPointerException(); + } viewHolder = new ViewHolder(root); root.setTag(viewHolder); ((ImageView) root.findViewById(R.id.status_icon_verified)) - .setColorFilter(ContextCompat.getColor(getContext(), R.color.android_green_light), + .setColorFilter(ContextCompat.getColor(context, R.color.android_green_light), PorterDuff.Mode.SRC_IN); ((ImageView) root.findViewById(R.id.status_icon_invalid)) - .setColorFilter(ContextCompat.getColor(getContext(), R.color.android_red_light), + .setColorFilter(ContextCompat.getColor(context, R.color.android_red_light), PorterDuff.Mode.SRC_IN); viewHolder.vButtonVerify.setOnClickListener(v -> verifyResource()); viewHolder.vButtonRetry.setOnClickListener(v -> verifyResource()); viewHolder.vButtonConfirm.setOnClickListener(v -> initiateCertifying()); - { - Bundle args = new Bundle(); - args.putParcelable(CertListWidget.ARG_URI, Certs.buildLinkedIdCertsUri(dataUri, lidRank)); - args.putBoolean(CertListWidget.ARG_IS_SECRET, isSecret); - getLoaderManager().initLoader(CertListWidget.LOADER_ID_LINKED_CERTS, - args, viewHolder.vLinkedCerts); - } + LinkedIdViewModel viewModel = ViewModelProviders.of(this).get(LinkedIdViewModel.class); + viewModel.getCertDetails(context, masterKeyId, lidRank).observe(this, this::onLoadCertDetails); return root; } + private void onLoadCertDetails(CertDetails certDetails) { + viewHolder.vLinkedCerts.setData(certDetails, isSecret); + } + void verifyResource() { // only one at a time (no sync needed, taskInProgress is only touched in ui thread) @@ -438,9 +389,8 @@ public class LinkedIdViewFragment extends CryptoOperationFragment implements byte[] fingerprint; try { - fingerprint = KeyRepository.create(activity).getCachedPublicKeyRing( - masterKeyId).getFingerprint(); - } catch (PgpKeyNotFoundException e) { + fingerprint = KeyRepository.create(activity).getFingerprintByKeyId(masterKeyId); + } catch (NotFoundException e) { throw new IllegalStateException("Key to verify linked id for must exist in db!"); } @@ -540,4 +490,38 @@ public class LinkedIdViewFragment extends CryptoOperationFragment implements return true; } + public static class LinkedIdViewModel extends ViewModel { + LiveData> certifyingKeysLiveData; + LiveData certDetailsLiveData; + LiveData linkedIfInfoLiveData; + + LiveData> getCertifyingKeys(Context context) { + if (certifyingKeysLiveData == null) { + certifyingKeysLiveData = new GenericLiveData<>(context, () -> { + KeyRepository keyRepository = KeyRepository.create(context); + return keyRepository.getAllUnifiedKeyInfoWithSecret(); + }); + } + return certifyingKeysLiveData; + } + + LiveData getCertDetails(Context context, long masterKeyId, int lidRank) { + if (certDetailsLiveData == null) { + CertificationDao certificationDao = CertificationDao.getInstance(context); + certDetailsLiveData = new GenericLiveData<>(context, masterKeyId, + () -> certificationDao.getVerifyingCertDetails(masterKeyId, lidRank)); + } + return certDetailsLiveData; + } + + public LiveData getLinkedIdInfo(Context context, long masterKeyId, int lidRank) { + if (linkedIfInfoLiveData == null) { + IdentityDao identityDao = IdentityDao.getInstance(context); + linkedIfInfoLiveData = new GenericLiveData<>(context, masterKeyId, + () -> identityDao.getLinkedIdInfo(masterKeyId, lidRank)); + } + return linkedIfInfoLiveData; + } + } + } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/UnifiedKeyInfoViewModel.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/UnifiedKeyInfoViewModel.java new file mode 100644 index 000000000..69472c8ab --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/UnifiedKeyInfoViewModel.java @@ -0,0 +1,42 @@ +package org.sufficientlysecure.keychain.ui.keyview; + + +import android.arch.lifecycle.LiveData; +import android.arch.lifecycle.ViewModel; +import android.content.Context; +import android.net.Uri; + +import org.sufficientlysecure.keychain.livedata.GenericLiveData; +import org.sufficientlysecure.keychain.model.SubKey.UnifiedKeyInfo; +import org.sufficientlysecure.keychain.daos.DatabaseNotifyManager; +import org.sufficientlysecure.keychain.daos.KeyRepository; + + +public class UnifiedKeyInfoViewModel extends ViewModel { + private Long masterKeyId; + private LiveData unifiedKeyInfoLiveData; + + public void setMasterKeyId(long masterKeyId) { + if (this.masterKeyId != null) { + throw new IllegalStateException("cannot change masterKeyId once set!"); + } + this.masterKeyId = masterKeyId; + } + + public long getMasterKeyId() { + return masterKeyId; + } + + public LiveData getUnifiedKeyInfoLiveData(Context context) { + if (masterKeyId == null) { + throw new IllegalStateException("masterKeyId must be set to retrieve this!"); + } + if (unifiedKeyInfoLiveData == null) { + KeyRepository keyRepository = KeyRepository.create(context); + Uri notifyUri = DatabaseNotifyManager.getNotifyUriMasterKeyId(masterKeyId); + unifiedKeyInfoLiveData = new GenericLiveData<>(context, notifyUri, + () -> keyRepository.getUnifiedKeyInfo(masterKeyId)); + } + return unifiedKeyInfoLiveData; + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/ViewKeyActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/ViewKeyActivity.java index bda2c9a7a..156b5c3a4 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/ViewKeyActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/ViewKeyActivity.java @@ -28,8 +28,9 @@ import android.animation.ObjectAnimator; import android.annotation.SuppressLint; import android.app.Activity; import android.app.ActivityOptions; +import android.arch.lifecycle.ViewModelProviders; +import android.content.Context; import android.content.Intent; -import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.PorterDuff; import android.net.Uri; @@ -41,15 +42,13 @@ import android.os.Message; import android.os.Messenger; import android.provider.ContactsContract; import android.support.annotation.IntDef; +import android.support.annotation.NonNull; import android.support.design.widget.AppBarLayout; import android.support.design.widget.CollapsingToolbarLayout; import android.support.design.widget.CoordinatorLayout; import android.support.design.widget.FloatingActionButton; import android.support.v4.app.ActivityCompat; import android.support.v4.app.FragmentManager; -import android.support.v4.app.LoaderManager; -import android.support.v4.content.CursorLoader; -import android.support.v4.content.Loader; import android.support.v7.widget.CardView; import android.view.Menu; import android.view.MenuItem; @@ -69,15 +68,13 @@ import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.keyimport.HkpKeyserverAddress; import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing; +import org.sufficientlysecure.keychain.model.SubKey.UnifiedKeyInfo; import org.sufficientlysecure.keychain.operations.results.EditKeyResult; import org.sufficientlysecure.keychain.operations.results.ImportKeyResult; import org.sufficientlysecure.keychain.operations.results.OperationResult; import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey.SecretKeyType; -import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; -import org.sufficientlysecure.keychain.provider.KeyRepository; -import org.sufficientlysecure.keychain.provider.KeyRepository.NotFoundException; -import org.sufficientlysecure.keychain.provider.KeychainContract; -import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; +import org.sufficientlysecure.keychain.daos.KeyRepository; +import org.sufficientlysecure.keychain.daos.KeyRepository.NotFoundException; import org.sufficientlysecure.keychain.securitytoken.SecurityTokenConnection; import org.sufficientlysecure.keychain.service.ChangeUnlockParcel; import org.sufficientlysecure.keychain.service.ImportKeyringParcel; @@ -112,7 +109,6 @@ import timber.log.Timber; public class ViewKeyActivity extends BaseSecurityTokenActivity implements - LoaderManager.LoaderCallbacks, CryptoOperationHelper.Callback { @Retention(RetentionPolicy.SOURCE) @IntDef({REQUEST_QR_FINGERPRINT, REQUEST_BACKUP, REQUEST_CERTIFY, REQUEST_DELETE}) @@ -124,13 +120,12 @@ public class ViewKeyActivity extends BaseSecurityTokenActivity implements static final int REQUEST_CERTIFY = 3; static final int REQUEST_DELETE = 4; + public static final String EXTRA_MASTER_KEY_ID = "master_key_id"; public static final String EXTRA_DISPLAY_RESULT = "display_result"; public static final String EXTRA_LINKED_TRANSITION = "linked_transition"; KeyRepository keyRepository; - protected Uri dataUri; - // For CryptoOperationHelper.Callback private CryptoOperationHelper importOpHelper; private CryptoOperationHelper editOpHelper; @@ -151,22 +146,18 @@ public class ViewKeyActivity extends BaseSecurityTokenActivity implements private byte[] qrCodeLoaded; - private static final int LOADER_ID_UNIFIED = 0; - - private boolean isSecret = false; - private boolean hasEncrypt = false; - private boolean isVerified = false; - private boolean isRevoked = false; - private boolean isSecure = true; - private boolean isExpired = false; + private UnifiedKeyInfo unifiedKeyInfo; private MenuItem refreshItem; private boolean isRefreshing; private Animation rotate, rotateSpin; private View refreshView; - private long masterKeyId; - private byte[] fingerprint; + public static Intent getViewKeyActivityIntent(@NonNull Context context, long masterKeyId) { + Intent viewIntent = new Intent(context, ViewKeyActivity.class); + viewIntent.putExtra(ViewKeyActivity.EXTRA_MASTER_KEY_ID, masterKeyId); + return viewIntent; + } @SuppressLint("InflateParams") @Override @@ -244,30 +235,30 @@ public class ViewKeyActivity extends BaseSecurityTokenActivity implements }); refreshView = getLayoutInflater().inflate(R.layout.indeterminate_progress, null); - dataUri = getIntent().getData(); - if (dataUri == null) { - Timber.e("Data missing. Should be uri of key!"); - finish(); - return; - } - if (dataUri.getHost().equals(ContactsContract.AUTHORITY)) { - dataUri = new ContactHelper(this).dataUriFromContactUri(dataUri); - if (dataUri == null) { + long masterKeyId; + Intent intent = getIntent(); + Uri dataUri = intent.getData(); + if (intent.hasExtra(EXTRA_MASTER_KEY_ID)) { + masterKeyId = intent.getLongExtra(EXTRA_MASTER_KEY_ID, 0L); + } else if (dataUri != null && dataUri.getHost().equals(ContactsContract.AUTHORITY)) { + Long contactMasterKeyId = new ContactHelper(this).masterKeyIdFromContactsDataUri(dataUri); + if (contactMasterKeyId == null) { Timber.e("Contact Data missing. Should be uri of key!"); Toast.makeText(this, R.string.error_contacts_key_id_missing, Toast.LENGTH_LONG).show(); finish(); return; } + masterKeyId = contactMasterKeyId; + } else { + throw new IllegalArgumentException("Missing required extra master_key_id or contact uri"); } - Timber.i("dataUri: " + dataUri); - - actionEncryptFile.setOnClickListener(v -> encrypt(dataUri, false)); - actionEncryptText.setOnClickListener(v -> encrypt(dataUri, true)); + actionEncryptFile.setOnClickListener(v -> encrypt(false)); + actionEncryptText.setOnClickListener(v -> encrypt(true)); floatingActionButton.setOnClickListener(v -> { - if (isSecret) { - startSafeSlinger(dataUri); + if (unifiedKeyInfo.has_any_secret()) { + startSafeSlinger(); } else { scanQrCode(); } @@ -275,12 +266,12 @@ public class ViewKeyActivity extends BaseSecurityTokenActivity implements qrCodeLayout.setOnClickListener(v -> showQrCodeDialog()); - // Prepare the loaders. Either re-connect with an existing ones, - // or start new ones. - getSupportLoaderManager().initLoader(LOADER_ID_UNIFIED, null, this); + UnifiedKeyInfoViewModel viewModel = ViewModelProviders.of(this).get(UnifiedKeyInfoViewModel.class); + viewModel.setMasterKeyId(getIntent().getLongExtra(EXTRA_MASTER_KEY_ID, 0L)); + viewModel.getUnifiedKeyInfoLiveData(getApplicationContext()).observe(this, this::onLoadUnifiedKeyInfo); - if (savedInstanceState == null && getIntent().hasExtra(EXTRA_DISPLAY_RESULT)) { - OperationResult result = getIntent().getParcelableExtra(EXTRA_DISPLAY_RESULT); + if (savedInstanceState == null && intent.hasExtra(EXTRA_DISPLAY_RESULT)) { + OperationResult result = intent.getParcelableExtra(EXTRA_DISPLAY_RESULT); result.createNotify(this).show(); } @@ -289,12 +280,14 @@ public class ViewKeyActivity extends BaseSecurityTokenActivity implements return; } + FragmentManager manager = getSupportFragmentManager(); + + ViewKeyFragment frag = ViewKeyFragment.newInstance(); + manager.beginTransaction().replace(R.id.view_key_fragment, frag, "view_key_fragment").commit(); + if (Preferences.getPreferences(this).getExperimentalEnableKeybase()) { - FragmentManager manager = getSupportFragmentManager(); - final ViewKeyKeybaseFragment keybaseFrag = ViewKeyKeybaseFragment.newInstance(dataUri); - manager.beginTransaction() - .replace(R.id.view_key_keybase_fragment, keybaseFrag) - .commit(); + final ViewKeyKeybaseFragment keybaseFrag = ViewKeyKeybaseFragment.newInstance(); + manager.beginTransaction().replace(R.id.view_key_keybase_fragment, keybaseFrag).commit(); } } @@ -341,7 +334,7 @@ public class ViewKeyActivity extends BaseSecurityTokenActivity implements } case R.id.menu_key_view_advanced: { Intent advancedIntent = new Intent(this, ViewKeyAdvActivity.class); - advancedIntent.setData(dataUri); + advancedIntent.putExtra(ViewKeyAdvActivity.EXTRA_MASTER_KEY_ID, unifiedKeyInfo.master_key_id()); startActivity(advancedIntent); return true; } @@ -350,7 +343,7 @@ public class ViewKeyActivity extends BaseSecurityTokenActivity implements return true; } case R.id.menu_key_view_certify_fingerprint: { - certifyFingerprint(dataUri); + certifyFingerprint(); return true; } } @@ -359,14 +352,19 @@ public class ViewKeyActivity extends BaseSecurityTokenActivity implements @Override public boolean onPrepareOptionsMenu(Menu menu) { + if (unifiedKeyInfo == null) { + return false; + } MenuItem backupKey = menu.findItem(R.id.menu_key_view_backup); - backupKey.setVisible(isSecret); - menu.findItem(R.id.menu_key_view_skt).setVisible(isSecret); + backupKey.setVisible(unifiedKeyInfo.has_any_secret()); + menu.findItem(R.id.menu_key_view_skt).setVisible(unifiedKeyInfo.has_any_secret()); MenuItem changePassword = menu.findItem(R.id.menu_key_change_password); - changePassword.setVisible(isSecret); + changePassword.setVisible(unifiedKeyInfo.has_any_secret()); MenuItem certifyFingerprint = menu.findItem(R.id.menu_key_view_certify_fingerprint); - certifyFingerprint.setVisible(!isSecret && !isVerified && !isExpired && !isRevoked); + certifyFingerprint.setVisible( + !unifiedKeyInfo.has_any_secret() && !unifiedKeyInfo.is_verified() && !unifiedKeyInfo.is_expired() && + !unifiedKeyInfo.is_revoked()); return true; } @@ -382,6 +380,7 @@ public class ViewKeyActivity extends BaseSecurityTokenActivity implements @Override public void onCryptoOperationSuccess(EditKeyResult result) { displayResult(result); + long masterKeyId = unifiedKeyInfo.master_key_id(); PassphraseCacheService.clearCachedPassphrase(getApplicationContext(), masterKeyId, masterKeyId); } @@ -412,7 +411,7 @@ public class ViewKeyActivity extends BaseSecurityTokenActivity implements // use new passphrase! changeUnlockParcel = ChangeUnlockParcel.createChangeUnlockParcel( - masterKeyId, fingerprint, + unifiedKeyInfo.master_key_id(), unifiedKeyInfo.fingerprint(), data.getParcelable(SetPassphraseDialogFragment.MESSAGE_NEW_PASSPHRASE) ); @@ -440,16 +439,16 @@ public class ViewKeyActivity extends BaseSecurityTokenActivity implements startActivityForResult(scanQrCode, REQUEST_QR_FINGERPRINT); } - private void certifyFingerprint(Uri dataUri) { + private void certifyFingerprint() { Intent intent = new Intent(this, CertifyFingerprintActivity.class); - intent.setData(dataUri); + intent.putExtra(CertifyFingerprintActivity.EXTRA_MASTER_KEY_ID, unifiedKeyInfo.master_key_id()); startActivityForResult(intent, REQUEST_CERTIFY); } private void certifyImmediate() { Intent intent = new Intent(this, CertifyKeyActivity.class); - intent.putExtra(CertifyKeyActivity.EXTRA_KEY_IDS, new long[] { masterKeyId }); + intent.putExtra(CertifyKeyActivity.EXTRA_KEY_IDS, new long[] { unifiedKeyInfo.master_key_id() }); startActivityForResult(intent, REQUEST_CERTIFY); } @@ -466,7 +465,7 @@ public class ViewKeyActivity extends BaseSecurityTokenActivity implements opts = options.toBundle(); } - qrCodeIntent.setData(dataUri); + qrCodeIntent.putExtra(QrCodeViewActivity.EXTRA_MASTER_KEY_ID, unifiedKeyInfo.master_key_id()); ActivityCompat.startActivity(this, qrCodeIntent, opts); } @@ -474,6 +473,7 @@ public class ViewKeyActivity extends BaseSecurityTokenActivity implements if (keyHasPassphrase()) { Intent intent = new Intent(this, PassphraseDialogActivity.class); + long masterKeyId = unifiedKeyInfo.master_key_id(); RequiredInputParcel requiredInput = RequiredInputParcel.createRequiredDecryptPassphrase(masterKeyId, masterKeyId); requiredInput.mSkipCaching = true; @@ -486,8 +486,8 @@ public class ViewKeyActivity extends BaseSecurityTokenActivity implements private boolean keyHasPassphrase() { try { - SecretKeyType secretKeyType = - keyRepository.getCachedPublicKeyRing(masterKeyId).getSecretKeyType(masterKeyId); + long masterKeyId = unifiedKeyInfo.master_key_id(); + SecretKeyType secretKeyType = keyRepository.getSecretKeyType(masterKeyId); switch (secretKeyType) { // all of these make no sense to ask case PASSPHRASE_EMPTY: @@ -505,7 +505,7 @@ public class ViewKeyActivity extends BaseSecurityTokenActivity implements private void startBackupActivity() { Intent intent = new Intent(this, BackupActivity.class); - intent.putExtra(BackupActivity.EXTRA_MASTER_KEY_IDS, new long[]{ masterKeyId }); + intent.putExtra(BackupActivity.EXTRA_MASTER_KEY_IDS, new long[] { unifiedKeyInfo.master_key_id() }); intent.putExtra(BackupActivity.EXTRA_SECRET, true); startActivity(intent); } @@ -514,9 +514,9 @@ public class ViewKeyActivity extends BaseSecurityTokenActivity implements Intent deleteIntent = new Intent(this, DeleteKeyDialogActivity.class); deleteIntent.putExtra(DeleteKeyDialogActivity.EXTRA_DELETE_MASTER_KEY_IDS, - new long[]{ masterKeyId }); - deleteIntent.putExtra(DeleteKeyDialogActivity.EXTRA_HAS_SECRET, isSecret); - if (isSecret) { + new long[]{ unifiedKeyInfo.master_key_id() }); + deleteIntent.putExtra(DeleteKeyDialogActivity.EXTRA_HAS_SECRET, unifiedKeyInfo.has_any_secret()); + if (unifiedKeyInfo.has_any_secret()) { // for upload in case key is secret deleteIntent.putExtra(DeleteKeyDialogActivity.EXTRA_KEYSERVER, Preferences.getPreferences(this).getPreferredKeyserver()); @@ -554,7 +554,7 @@ public class ViewKeyActivity extends BaseSecurityTokenActivity implements Notify.create(this, R.string.error_scan_fp, Notify.LENGTH_LONG, Style.ERROR).show(); return; } - if (Arrays.equals(this.fingerprint, fingerprint)) { + if (Arrays.equals(unifiedKeyInfo.fingerprint(), fingerprint)) { certifyImmediate(); } else { Notify.create(this, R.string.error_scan_match, Notify.LENGTH_LONG, Style.ERROR).show(); @@ -593,67 +593,30 @@ public class ViewKeyActivity extends BaseSecurityTokenActivity implements finish(); } - public void showMainFragment() { - new Handler().post(() -> { - FragmentManager manager = getSupportFragmentManager(); - - // unless we must refresh - ViewKeyFragment frag = (ViewKeyFragment) manager.findFragmentByTag("view_key_fragment"); - // if everything is valid, just drop it - if (frag != null && frag.isValidForData(isSecret)) { - return; - } - - // if the main fragment doesn't exist, or is not of the correct type, (re)create it - frag = ViewKeyFragment.newInstance(masterKeyId, isSecret); - // get rid of possible backstack, this fragment is always at the bottom - manager.popBackStack("security_token", FragmentManager.POP_BACK_STACK_INCLUSIVE); - manager.beginTransaction() - .replace(R.id.view_key_fragment, frag, "view_key_fragment") - // if this gets lost, it doesn't really matter since the loader will reinstate it onResume - .commitAllowingStateLoss(); - }); - } - - private void encrypt(Uri dataUri, boolean text) { + private void encrypt(boolean text) { // If there is no encryption key, don't bother. - if (!hasEncrypt) { + if (!unifiedKeyInfo.has_encrypt_key()) { Notify.create(this, R.string.error_no_encrypt_subkey, Notify.Style.ERROR).show(); return; } - try { - long keyId = KeyRepository.create(this) - .getCachedPublicKeyRing(dataUri) - .extractOrGetMasterKeyId(); - long[] encryptionKeyIds = new long[]{keyId}; - Intent intent; - if (text) { - intent = new Intent(this, EncryptTextActivity.class); - intent.setAction(EncryptTextActivity.ACTION_ENCRYPT_TEXT); - intent.putExtra(EncryptTextActivity.EXTRA_ENCRYPTION_KEY_IDS, encryptionKeyIds); - } else { - intent = new Intent(this, EncryptFilesActivity.class); - intent.setAction(EncryptFilesActivity.ACTION_ENCRYPT_DATA); - intent.putExtra(EncryptFilesActivity.EXTRA_ENCRYPTION_KEY_IDS, encryptionKeyIds); - } - // used instead of startActivity set actionbar based on callingPackage - startActivityForResult(intent, 0); - } catch (PgpKeyNotFoundException e) { - Timber.e(e, "key not found!"); + long[] encryptionKeyIds = new long[] { unifiedKeyInfo.master_key_id() }; + Intent intent; + if (text) { + intent = new Intent(this, EncryptTextActivity.class); + intent.setAction(EncryptTextActivity.ACTION_ENCRYPT_TEXT); + intent.putExtra(EncryptTextActivity.EXTRA_ENCRYPTION_KEY_IDS, encryptionKeyIds); + } else { + intent = new Intent(this, EncryptFilesActivity.class); + intent.setAction(EncryptFilesActivity.ACTION_ENCRYPT_DATA); + intent.putExtra(EncryptFilesActivity.EXTRA_ENCRYPTION_KEY_IDS, encryptionKeyIds); } + // used instead of startActivity set actionbar based on callingPackage + startActivityForResult(intent, 0); } - private void startSafeSlinger(Uri dataUri) { - long keyId = 0; - try { - keyId = KeyRepository.create(this) - .getCachedPublicKeyRing(dataUri) - .extractOrGetMasterKeyId(); - } catch (PgpKeyNotFoundException e) { - Timber.e(e, "key not found!"); - } + private void startSafeSlinger() { Intent safeSlingerIntent = new Intent(this, SafeSlingerActivity.class); - safeSlingerIntent.putExtra(SafeSlingerActivity.EXTRA_MASTER_KEY_ID, keyId); + safeSlingerIntent.putExtra(SafeSlingerActivity.EXTRA_MASTER_KEY_ID, unifiedKeyInfo.master_key_id()); startActivityForResult(safeSlingerIntent, 0); } @@ -692,50 +655,6 @@ public class ViewKeyActivity extends BaseSecurityTokenActivity implements loadTask.execute(); } - - // These are the rows that we will retrieve. - static final String[] PROJECTION = new String[]{ - KeychainContract.KeyRings._ID, - KeychainContract.KeyRings.MASTER_KEY_ID, - KeychainContract.KeyRings.USER_ID, - KeychainContract.KeyRings.IS_REVOKED, - KeychainContract.KeyRings.IS_EXPIRED, - KeychainContract.KeyRings.IS_SECURE, - KeychainContract.KeyRings.VERIFIED, - KeychainContract.KeyRings.HAS_ANY_SECRET, - KeychainContract.KeyRings.FINGERPRINT, - KeychainContract.KeyRings.HAS_ENCRYPT, - KeyRings.NAME, - KeyRings.EMAIL, - KeyRings.COMMENT - }; - - static final int INDEX_MASTER_KEY_ID = 1; - static final int INDEX_USER_ID = 2; - static final int INDEX_IS_REVOKED = 3; - static final int INDEX_IS_EXPIRED = 4; - static final int INDEX_IS_SECURE = 5; - static final int INDEX_VERIFIED = 6; - static final int INDEX_HAS_ANY_SECRET = 7; - static final int INDEX_FINGERPRINT = 8; - static final int INDEX_HAS_ENCRYPT = 9; - static final int INDEX_NAME = 10; - static final int INDEX_EMAIL = 11; - static final int INDEX_COMMENT = 12; - - @Override - public Loader onCreateLoader(int id, Bundle args) { - switch (id) { - case LOADER_ID_UNIFIED: { - Uri baseUri = KeychainContract.KeyRings.buildUnifiedKeyRingUri(dataUri); - return new CursorLoader(this, baseUri, PROJECTION, null, null, null); - } - - default: - return null; - } - } - int mPreviousColor = 0; /** @@ -757,127 +676,104 @@ public class ViewKeyActivity extends BaseSecurityTokenActivity implements return (0xff << 24) | (r << 16) | (g << 8) | b; } - @Override - public void onLoadFinished(Loader loader, Cursor data) { - /* TODO better error handling? May cause problems when a key is deleted, - * because the notification triggers faster than the activity closes. - */ + private void onLoadUnifiedKeyInfo(UnifiedKeyInfo unifiedKeyInfo) { + if (unifiedKeyInfo == null) { + return; + } - // Swap the new cursor in. (The framework will take care of closing the - // old cursor once we return.) - switch (loader.getId()) { - case LOADER_ID_UNIFIED: { - // Avoid NullPointerExceptions... - if (data.getCount() == 0) { - return; - } + this.unifiedKeyInfo = unifiedKeyInfo; - if (data.moveToFirst()) { - // get name, email, and comment from USER_ID + String name = unifiedKeyInfo.name(); + collapsingToolbarLayout.setTitle(name != null ? name : getString(R.string.user_id_no_name)); - String name = data.getString(INDEX_NAME); + // if the refresh animation isn't playing + if (!rotate.hasStarted() && !rotateSpin.hasStarted()) { + // re-create options menu based on isSecret, isVerified + supportInvalidateOptionsMenu(); + // this is done at the end of the animation otherwise + } - collapsingToolbarLayout.setTitle(name != null ? name : getString(R.string.user_id_no_name)); - - masterKeyId = data.getLong(INDEX_MASTER_KEY_ID); - fingerprint = data.getBlob(INDEX_FINGERPRINT); - isSecret = data.getInt(INDEX_HAS_ANY_SECRET) != 0; - hasEncrypt = data.getInt(INDEX_HAS_ENCRYPT) != 0; - isRevoked = data.getInt(INDEX_IS_REVOKED) > 0; - isExpired = data.getInt(INDEX_IS_EXPIRED) != 0; - isSecure = data.getInt(INDEX_IS_SECURE) == 1; - isVerified = data.getInt(INDEX_VERIFIED) > 0; - - // queue showing of the main fragment - showMainFragment(); - - // if the refresh animation isn't playing - if (!rotate.hasStarted() && !rotateSpin.hasStarted()) { - // re-create options menu based on isSecret, isVerified - supportInvalidateOptionsMenu(); - // this is done at the end of the animation otherwise + AsyncTask photoTask = + new AsyncTask() { + protected Bitmap doInBackground(Long... mMasterKeyId) { + return new ContactHelper(ViewKeyActivity.this) + .loadPhotoByMasterKeyId(mMasterKeyId[0], true); } - AsyncTask photoTask = - new AsyncTask() { - protected Bitmap doInBackground(Long... mMasterKeyId) { - return new ContactHelper(ViewKeyActivity.this) - .loadPhotoByMasterKeyId(mMasterKeyId[0], true); - } - - protected void onPostExecute(Bitmap photo) { - if (photo == null) { - return; - } - - photoView.setImageBitmap(photo); - photoView.setColorFilter(getResources().getColor(R.color.toolbar_photo_tint), PorterDuff.Mode.SRC_ATOP); - photoLayout.setVisibility(View.VISIBLE); - } - }; - - boolean showStatusText = isSecure && !isExpired && !isRevoked; - if (showStatusText) { - statusText.setVisibility(View.VISIBLE); - - if (isSecret) { - statusText.setText(R.string.view_key_my_key); - } else if (isVerified) { - statusText.setText(R.string.view_key_verified); - } else { - statusText.setText(R.string.view_key_unverified); + protected void onPostExecute(Bitmap photo) { + if (photo == null) { + return; } - } else { - statusText.setVisibility(View.GONE); + + photoView.setImageBitmap(photo); + photoView.setColorFilter(getResources().getColor(R.color.toolbar_photo_tint), + PorterDuff.Mode.SRC_ATOP); + photoLayout.setVisibility(View.VISIBLE); } + }; - // Note: order is important - int color; - if (isRevoked) { - statusImage.setVisibility(View.VISIBLE); - KeyFormattingUtils.setStatusImage(this, statusImage, statusText, - State.REVOKED, R.color.icons, true); - // noinspection deprecation, fix requires api level 23 - color = getResources().getColor(R.color.key_flag_red); + boolean showStatusText = unifiedKeyInfo.is_secure() && !unifiedKeyInfo.is_expired() && !unifiedKeyInfo.is_revoked(); + if (showStatusText) { + statusText.setVisibility(View.VISIBLE); - actionEncryptFile.setVisibility(View.INVISIBLE); - actionEncryptText.setVisibility(View.INVISIBLE); - hideFab(); - qrCodeLayout.setVisibility(View.GONE); - } else if (!isSecure) { - statusImage.setVisibility(View.VISIBLE); - KeyFormattingUtils.setStatusImage(this, statusImage, statusText, - State.INSECURE, R.color.icons, true); - // noinspection deprecation, fix requires api level 23 - color = getResources().getColor(R.color.key_flag_red); + if (unifiedKeyInfo.has_any_secret()) { + statusText.setText(R.string.view_key_my_key); + } else if (unifiedKeyInfo.is_verified()) { + statusText.setText(R.string.view_key_verified); + } else { + statusText.setText(R.string.view_key_unverified); + } + } else { + statusText.setVisibility(View.GONE); + } - actionEncryptFile.setVisibility(View.INVISIBLE); - actionEncryptText.setVisibility(View.INVISIBLE); - hideFab(); - qrCodeLayout.setVisibility(View.GONE); - } else if (isExpired) { - statusImage.setVisibility(View.VISIBLE); - KeyFormattingUtils.setStatusImage(this, statusImage, statusText, - State.EXPIRED, R.color.icons, true); - // noinspection deprecation, fix requires api level 23 - color = getResources().getColor(R.color.key_flag_red); + // Note: order is important + int color; + if (unifiedKeyInfo.is_revoked()) { + statusImage.setVisibility(View.VISIBLE); + KeyFormattingUtils.setStatusImage(this, statusImage, statusText, + State.REVOKED, R.color.icons, true); + // noinspection deprecation, fix requires api level 23 + color = getResources().getColor(R.color.key_flag_red); - actionEncryptFile.setVisibility(View.INVISIBLE); - actionEncryptText.setVisibility(View.INVISIBLE); - hideFab(); - qrCodeLayout.setVisibility(View.GONE); - } else if (isSecret) { - statusImage.setVisibility(View.GONE); - // noinspection deprecation, fix requires api level 23 - color = getResources().getColor(R.color.key_flag_green); - // reload qr code only if the fingerprint changed - if (!Arrays.equals(fingerprint, qrCodeLoaded)) { - loadQrCode(fingerprint); - } - photoTask.execute(masterKeyId); - qrCodeLayout.setVisibility(View.VISIBLE); + actionEncryptFile.setVisibility(View.INVISIBLE); + actionEncryptText.setVisibility(View.INVISIBLE); + hideFab(); + qrCodeLayout.setVisibility(View.GONE); + } else if (unifiedKeyInfo.is_expired()) { + statusImage.setVisibility(View.VISIBLE); + KeyFormattingUtils.setStatusImage(this, statusImage, statusText, + State.EXPIRED, R.color.icons, true); + // noinspection deprecation, fix requires api level 23 + color = getResources().getColor(R.color.key_flag_red); - // and place leftOf qr code + actionEncryptFile.setVisibility(View.INVISIBLE); + actionEncryptText.setVisibility(View.INVISIBLE); + hideFab(); + qrCodeLayout.setVisibility(View.GONE); + } else if (!unifiedKeyInfo.is_secure()) { + statusImage.setVisibility(View.VISIBLE); + KeyFormattingUtils.setStatusImage(this, statusImage, statusText, + State.INSECURE, R.color.icons, true); + // noinspection deprecation, fix requires api level 23 + color = getResources().getColor(R.color.key_flag_red); + + actionEncryptFile.setVisibility(View.INVISIBLE); + actionEncryptText.setVisibility(View.INVISIBLE); + hideFab(); + qrCodeLayout.setVisibility(View.GONE); + } else if (unifiedKeyInfo.has_any_secret()) { + statusImage.setVisibility(View.GONE); + // noinspection deprecation, fix requires api level 23 + color = getResources().getColor(R.color.key_flag_green); + // reload qr code only if the fingerprint changed + if (!Arrays.equals(unifiedKeyInfo.fingerprint(), qrCodeLoaded)) { + loadQrCode(unifiedKeyInfo.fingerprint()); + } + photoTask.execute(unifiedKeyInfo.master_key_id()); + qrCodeLayout.setVisibility(View.VISIBLE); + + // and place leftOf qr code // RelativeLayout.LayoutParams nameParams = (RelativeLayout.LayoutParams) // mName.getLayoutParams(); // // remove right margin @@ -888,72 +784,67 @@ public class ViewKeyActivity extends BaseSecurityTokenActivity implements // nameParams.addRule(RelativeLayout.LEFT_OF, R.id.view_key_qr_code_layout); // mName.setLayoutParams(nameParams); - RelativeLayout.LayoutParams statusParams = (RelativeLayout.LayoutParams) - statusText.getLayoutParams(); - statusParams.setMargins(FormattingUtils.dpToPx(this, 48), 0, 0, 0); - if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { - statusParams.setMarginEnd(0); - } - statusParams.addRule(RelativeLayout.LEFT_OF, R.id.view_key_qr_code_layout); - statusText.setLayoutParams(statusParams); + RelativeLayout.LayoutParams statusParams = (RelativeLayout.LayoutParams) + statusText.getLayoutParams(); + statusParams.setMargins(FormattingUtils.dpToPx(this, 48), 0, 0, 0); + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + statusParams.setMarginEnd(0); + } + statusParams.addRule(RelativeLayout.LEFT_OF, R.id.view_key_qr_code_layout); + statusText.setLayoutParams(statusParams); - actionEncryptFile.setVisibility(View.VISIBLE); - actionEncryptText.setVisibility(View.VISIBLE); + actionEncryptFile.setVisibility(View.VISIBLE); + actionEncryptText.setVisibility(View.VISIBLE); - showFab(); - // noinspection deprecation (no getDrawable with theme at current minApi level 15!) - floatingActionButton.setImageDrawable(getResources().getDrawable(R.drawable.ic_repeat_white_24dp)); - } else { - actionEncryptFile.setVisibility(View.VISIBLE); - actionEncryptText.setVisibility(View.VISIBLE); - qrCodeLayout.setVisibility(View.GONE); + showFab(); + // noinspection deprecation (no getDrawable with theme at current minApi level 15!) + floatingActionButton.setImageDrawable(getResources().getDrawable(R.drawable.ic_repeat_white_24dp)); + } else { + actionEncryptFile.setVisibility(View.VISIBLE); + actionEncryptText.setVisibility(View.VISIBLE); + qrCodeLayout.setVisibility(View.GONE); - if (isVerified) { - statusText.setText(R.string.view_key_verified); - statusImage.setVisibility(View.VISIBLE); - KeyFormattingUtils.setStatusImage(this, statusImage, statusText, - State.VERIFIED, R.color.icons, true); - // noinspection deprecation, fix requires api level 23 - color = getResources().getColor(R.color.key_flag_green); - photoTask.execute(masterKeyId); + if (unifiedKeyInfo.is_verified()) { + statusText.setText(R.string.view_key_verified); + statusImage.setVisibility(View.VISIBLE); + KeyFormattingUtils.setStatusImage(this, statusImage, statusText, + State.VERIFIED, R.color.icons, true); + // noinspection deprecation, fix requires api level 23 + color = getResources().getColor(R.color.key_flag_green); + photoTask.execute(unifiedKeyInfo.master_key_id()); - hideFab(); - } else { - statusText.setText(R.string.view_key_unverified); - statusImage.setVisibility(View.VISIBLE); - KeyFormattingUtils.setStatusImage(this, statusImage, statusText, - State.UNVERIFIED, R.color.icons, true); - // noinspection deprecation, fix requires api level 23 - color = getResources().getColor(R.color.key_flag_orange); + hideFab(); + } else { + statusText.setText(R.string.view_key_unverified); + statusImage.setVisibility(View.VISIBLE); + KeyFormattingUtils.setStatusImage(this, statusImage, statusText, + State.UNVERIFIED, R.color.icons, true); + // noinspection deprecation, fix requires api level 23 + color = getResources().getColor(R.color.key_flag_orange); - showFab(); - } - } - - if (mPreviousColor == 0 || mPreviousColor == color) { - appBarLayout.setBackgroundColor(color); - collapsingToolbarLayout.setContentScrimColor(color); - collapsingToolbarLayout.setStatusBarScrimColor(getStatusBarBackgroundColor(color)); - mPreviousColor = color; - } else { - ObjectAnimator colorFade = - ObjectAnimator.ofObject(appBarLayout, "backgroundColor", - new ArgbEvaluator(), mPreviousColor, color); - collapsingToolbarLayout.setContentScrimColor(color); - collapsingToolbarLayout.setStatusBarScrimColor(getStatusBarBackgroundColor(color)); - - colorFade.setDuration(1200); - colorFade.start(); - mPreviousColor = color; - } - - //noinspection deprecation - statusImage.setAlpha(80); - - break; - } + showFab(); } } + + if (mPreviousColor == 0 || mPreviousColor == color) { + appBarLayout.setBackgroundColor(color); + collapsingToolbarLayout.setContentScrimColor(color); + collapsingToolbarLayout.setStatusBarScrimColor(getStatusBarBackgroundColor(color)); + mPreviousColor = color; + } else { + ObjectAnimator colorFade = + ObjectAnimator.ofObject(appBarLayout, "backgroundColor", + new ArgbEvaluator(), mPreviousColor, color); + collapsingToolbarLayout.setContentScrimColor(color); + collapsingToolbarLayout.setStatusBarScrimColor(getStatusBarBackgroundColor(color)); + + colorFade.setDuration(1200); + colorFade.start(); + mPreviousColor = color; + } + + //noinspection deprecation + statusImage.setAlpha(80); } /** @@ -978,16 +869,11 @@ public class ViewKeyActivity extends BaseSecurityTokenActivity implements floatingActionButton.setVisibility(View.GONE); } - @Override - public void onLoaderReset(Loader loader) { - - } - // CryptoOperationHelper.Callback functions private void updateFromKeyserver() { - if (fingerprint == null) { + if (unifiedKeyInfo == null) { return; } @@ -1003,7 +889,7 @@ public class ViewKeyActivity extends BaseSecurityTokenActivity implements public ImportKeyringParcel createOperationInput() { HkpKeyserverAddress preferredKeyserver = Preferences.getPreferences(this).getPreferredKeyserver(); - ParcelableKeyRing keyEntry = ParcelableKeyRing.createFromReference(fingerprint, null, null, null); + ParcelableKeyRing keyEntry = ParcelableKeyRing.createFromReference(unifiedKeyInfo.fingerprint(), null, null, null); return ImportKeyringParcel.createImportKeyringParcel(Collections.singletonList(keyEntry), preferredKeyserver); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/ViewKeyFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/ViewKeyFragment.java index b1e675583..7aa2303a7 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/ViewKeyFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/ViewKeyFragment.java @@ -21,16 +21,18 @@ package org.sufficientlysecure.keychain.ui.keyview; import java.util.List; import android.arch.lifecycle.LiveData; -import android.arch.lifecycle.ViewModel; import android.arch.lifecycle.ViewModelProviders; +import android.content.Context; import android.content.Intent; +import android.net.Uri; import android.os.Bundle; import android.os.Handler; +import android.provider.ContactsContract; +import android.support.annotation.NonNull; import android.support.v4.app.DialogFragment; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentTransaction; import android.support.v7.widget.PopupMenu; -import android.support.v7.widget.PopupMenu.OnDismissListener; import android.support.v7.widget.PopupMenu.OnMenuItemClickListener; import android.view.LayoutInflater; import android.view.MenuItem; @@ -39,143 +41,297 @@ import android.view.ViewGroup; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.compatibility.DialogFragmentWorkaround; +import org.sufficientlysecure.keychain.model.KeyMetadata; +import org.sufficientlysecure.keychain.model.SubKey.UnifiedKeyInfo; import org.sufficientlysecure.keychain.operations.results.OperationResult; -import org.sufficientlysecure.keychain.ui.base.LoaderFragment; +import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey.SecretKeyType; +import org.sufficientlysecure.keychain.daos.AutocryptPeerDao; +import org.sufficientlysecure.keychain.ui.adapter.IdentityAdapter; +import org.sufficientlysecure.keychain.ui.adapter.IdentityAdapter.IdentityClickListener; +import org.sufficientlysecure.keychain.ui.dialog.UserIdInfoDialogFragment; +import org.sufficientlysecure.keychain.ui.keyview.loader.IdentityDao.AutocryptPeerInfo; import org.sufficientlysecure.keychain.ui.keyview.loader.IdentityDao.IdentityInfo; -import org.sufficientlysecure.keychain.ui.keyview.loader.KeyserverStatusDao.KeyserverStatus; +import org.sufficientlysecure.keychain.ui.keyview.loader.IdentityDao.LinkedIdInfo; +import org.sufficientlysecure.keychain.ui.keyview.loader.IdentityDao.UserIdInfo; +import org.sufficientlysecure.keychain.ui.keyview.loader.SubkeyStatusDao.KeyHealthStatus; import org.sufficientlysecure.keychain.ui.keyview.loader.SubkeyStatusDao.KeySubkeyStatus; +import org.sufficientlysecure.keychain.ui.keyview.loader.SubkeyStatusDao.SubKeyItem; import org.sufficientlysecure.keychain.ui.keyview.loader.SystemContactDao.SystemContactInfo; -import org.sufficientlysecure.keychain.ui.keyview.presenter.IdentitiesPresenter; -import org.sufficientlysecure.keychain.ui.keyview.presenter.KeyHealthPresenter; -import org.sufficientlysecure.keychain.ui.keyview.presenter.KeyserverStatusPresenter; -import org.sufficientlysecure.keychain.ui.keyview.presenter.SystemContactPresenter; -import org.sufficientlysecure.keychain.ui.keyview.presenter.ViewKeyMvpView; import org.sufficientlysecure.keychain.ui.keyview.view.IdentitiesCardView; import org.sufficientlysecure.keychain.ui.keyview.view.KeyHealthView; +import org.sufficientlysecure.keychain.ui.keyview.view.KeyStatusList.KeyDisplayStatus; import org.sufficientlysecure.keychain.ui.keyview.view.KeyserverStatusView; import org.sufficientlysecure.keychain.ui.keyview.view.SystemContactCardView; +import org.sufficientlysecure.keychain.ui.linked.LinkedIdWizard; +import org.sufficientlysecure.keychain.util.Preferences; +import timber.log.Timber; -public class ViewKeyFragment extends LoaderFragment implements ViewKeyMvpView, OnMenuItemClickListener { - public static final String ARG_MASTER_KEY_ID = "master_key_id"; - public static final String ARG_IS_SECRET = "is_secret"; - - boolean mIsSecret = false; - +public class ViewKeyFragment extends Fragment implements OnMenuItemClickListener { private IdentitiesCardView identitiesCardView; - private IdentitiesPresenter identitiesPresenter; + private SystemContactCardView systemContactCard; + private KeyHealthView keyStatusHealth; + private KeyserverStatusView keyserverStatusView; + private View keyStatusCardView; - SystemContactCardView systemContactCard; - SystemContactPresenter systemContactPresenter; - - KeyHealthView keyStatusHealth; - KeyserverStatusView keyStatusKeyserver; - - KeyHealthPresenter keyHealthPresenter; - KeyserverStatusPresenter keyserverStatusPresenter; + IdentityAdapter identitiesAdapter; private Integer displayedContextMenuPosition; + private UnifiedKeyInfo unifiedKeyInfo; + private KeySubkeyStatus subkeyStatus; + private boolean showingExpandedInfo; - /** - * Creates new instance of this fragment - */ - public static ViewKeyFragment newInstance(long masterKeyId, boolean isSecret) { - ViewKeyFragment frag = new ViewKeyFragment(); - Bundle args = new Bundle(); - args.putLong(ARG_MASTER_KEY_ID, masterKeyId); - args.putBoolean(ARG_IS_SECRET, isSecret); - - frag.setArguments(args); - - return frag; + public static ViewKeyFragment newInstance() { + return new ViewKeyFragment(); } @Override - public View onCreateView(LayoutInflater inflater, ViewGroup superContainer, Bundle savedInstanceState) { - View root = super.onCreateView(inflater, superContainer, savedInstanceState); - View view = inflater.inflate(R.layout.view_key_fragment, getContainer()); + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup viewGroup, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.view_key_fragment, viewGroup, false); identitiesCardView = view.findViewById(R.id.card_identities); - systemContactCard = view.findViewById(R.id.linked_system_contact_card); + keyStatusCardView = view.findViewById(R.id.subkey_status_card); keyStatusHealth = view.findViewById(R.id.key_status_health); - keyStatusKeyserver = view.findViewById(R.id.key_status_keyserver); + keyserverStatusView = view.findViewById(R.id.key_status_keyserver); - return root; - } - - public static class KeyFragmentViewModel extends ViewModel { - private LiveData> identityInfo; - private LiveData subkeyStatus; - private LiveData systemContactInfo; - private LiveData keyserverStatus; - - LiveData> getIdentityInfo(IdentitiesPresenter identitiesPresenter) { - if (identityInfo == null) { - identityInfo = identitiesPresenter.getLiveDataInstance(); + identitiesAdapter = new IdentityAdapter(requireContext(), new IdentityClickListener() { + @Override + public void onClickIdentity(int position) { + showIdentityInfo(position); } - return identityInfo; - } - LiveData getSubkeyStatus(KeyHealthPresenter keyHealthPresenter) { - if (subkeyStatus == null) { - subkeyStatus = keyHealthPresenter.getLiveDataInstance(); + @Override + public void onClickIdentityMore(int position, View anchor) { + showIdentityContextMenu(position, anchor); } - return subkeyStatus; - } + }); + identitiesCardView.setIdentitiesAdapter(identitiesAdapter); - LiveData getSystemContactInfo(SystemContactPresenter systemContactPresenter) { - if (systemContactInfo == null) { - systemContactInfo = systemContactPresenter.getLiveDataInstance(); - } - return systemContactInfo; - } + identitiesCardView.setVisibility(View.GONE); + keyStatusCardView.setVisibility(View.GONE); - LiveData getKeyserverStatus(KeyserverStatusPresenter keyserverStatusPresenter) { - if (keyserverStatus == null) { - keyserverStatus = keyserverStatusPresenter.getLiveDataInstance(); - } - return keyserverStatus; - } + keyStatusHealth.setOnHealthClickListener((v) -> onKeyHealthClick()); + + return view; } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - long masterKeyId = getArguments().getLong(ARG_MASTER_KEY_ID); - mIsSecret = getArguments().getBoolean(ARG_IS_SECRET); + Context context = requireContext(); + + UnifiedKeyInfoViewModel viewKeyViewModel = ViewModelProviders.of(requireActivity()).get(UnifiedKeyInfoViewModel.class); + LiveData unifiedKeyInfoLiveData = viewKeyViewModel.getUnifiedKeyInfoLiveData(requireContext()); + + unifiedKeyInfoLiveData.observe(this, this::onLoadUnifiedKeyInfo); KeyFragmentViewModel model = ViewModelProviders.of(this).get(KeyFragmentViewModel.class); - identitiesPresenter = new IdentitiesPresenter( - getContext(), identitiesCardView, this, masterKeyId, mIsSecret); - model.getIdentityInfo(identitiesPresenter).observe(this, identitiesPresenter); - - systemContactPresenter = new SystemContactPresenter( - getContext(), systemContactCard, masterKeyId, mIsSecret); - model.getSystemContactInfo(systemContactPresenter).observe(this, systemContactPresenter); - - keyHealthPresenter = new KeyHealthPresenter(getContext(), keyStatusHealth, masterKeyId); - model.getSubkeyStatus(keyHealthPresenter).observe(this, keyHealthPresenter); - - keyserverStatusPresenter = new KeyserverStatusPresenter( - getContext(), keyStatusKeyserver, masterKeyId, mIsSecret); - model.getKeyserverStatus(keyserverStatusPresenter).observe(this, keyserverStatusPresenter); + boolean showLinkedIds = Preferences.getPreferences(context).getExperimentalEnableLinkedIdentities(); + model.getIdentityInfo(context, unifiedKeyInfoLiveData, showLinkedIds).observe(this, this::onLoadIdentityInfo); + model.getKeyserverStatus(context, unifiedKeyInfoLiveData).observe(this, this::onLoadKeyMetadata); + model.getSystemContactInfo(context, unifiedKeyInfoLiveData).observe(this, this::onLoadSystemContact); + model.getSubkeyStatus(context, unifiedKeyInfoLiveData).observe(this, this::onLoadSubkeyStatus); } - @Override - public void switchToFragment(final Fragment frag, final String backStackName) { - new Handler().post(new Runnable() { - @Override - public void run() { - getFragmentManager().beginTransaction() - .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE) - .replace(R.id.view_key_fragment, frag) - .addToBackStack(backStackName) - .commit(); + private void onLoadSubkeyStatus(KeySubkeyStatus subkeyStatus) { + if (subkeyStatus == null) { + return; + } + + keyStatusCardView.setVisibility(View.VISIBLE); + + this.subkeyStatus = subkeyStatus; + + KeyHealthStatus keyHealthStatus = subkeyStatus.keyHealthStatus; + + boolean isInsecure = keyHealthStatus == KeyHealthStatus.INSECURE; + boolean isExpired = keyHealthStatus == KeyHealthStatus.EXPIRED; + if (isInsecure) { + boolean primaryKeySecurityProblem = subkeyStatus.keyCertify.mSecurityProblem != null; + if (primaryKeySecurityProblem) { + keyStatusHealth.setKeyStatus(keyHealthStatus); + keyStatusHealth.setPrimarySecurityProblem(subkeyStatus.keyCertify.mSecurityProblem); + keyStatusHealth.setShowExpander(false); + } else { + keyStatusHealth.setKeyStatus(keyHealthStatus); + keyStatusHealth.setShowExpander(false); + displayExpandedInfo(false); } - }); + } else if (isExpired) { + keyStatusHealth.setKeyStatus(keyHealthStatus); + keyStatusHealth.setPrimaryExpiryDate(subkeyStatus.keyCertify.mExpiry); + keyStatusHealth.setShowExpander(false); + keyStatusHealth.hideExpandedInfo(); + } else { + keyStatusHealth.setKeyStatus(keyHealthStatus); + keyStatusHealth.setShowExpander(keyHealthStatus != KeyHealthStatus.REVOKED); + keyStatusHealth.hideExpandedInfo(); + } + } + + private void displayExpandedInfo(boolean displayAll) { + SubKeyItem keyCertify = subkeyStatus.keyCertify; + SubKeyItem keySign = subkeyStatus.keysSign.isEmpty() ? null : subkeyStatus.keysSign.get(0); + SubKeyItem keyEncrypt = subkeyStatus.keysEncrypt.isEmpty() ? null : subkeyStatus.keysEncrypt.get(0); + + KeyDisplayStatus certDisplayStatus = getKeyDisplayStatus(keyCertify); + KeyDisplayStatus signDisplayStatus = getKeyDisplayStatus(keySign); + KeyDisplayStatus encryptDisplayStatus = getKeyDisplayStatus(keyEncrypt); + + if (!displayAll) { + if (certDisplayStatus == KeyDisplayStatus.OK) { + certDisplayStatus = null; + } + if (certDisplayStatus == KeyDisplayStatus.INSECURE) { + signDisplayStatus = null; + encryptDisplayStatus = null; + } + if (signDisplayStatus == KeyDisplayStatus.OK) { + signDisplayStatus = null; + } + if (encryptDisplayStatus == KeyDisplayStatus.OK) { + encryptDisplayStatus = null; + } + } + + keyStatusHealth.showExpandedState(certDisplayStatus, signDisplayStatus, encryptDisplayStatus); + } + + private void onKeyHealthClick() { + if (showingExpandedInfo) { + showingExpandedInfo = false; + keyStatusHealth.hideExpandedInfo(); + } else { + showingExpandedInfo = true; + displayExpandedInfo(true); + } + } + + private KeyDisplayStatus getKeyDisplayStatus(SubKeyItem subKeyItem) { + if (subKeyItem == null) { + return KeyDisplayStatus.UNAVAILABLE; + } + + if (subKeyItem.mIsRevoked) { + return KeyDisplayStatus.REVOKED; + } + if (subKeyItem.mIsExpired) { + return KeyDisplayStatus.EXPIRED; + } + if (subKeyItem.mSecurityProblem != null) { + return KeyDisplayStatus.INSECURE; + } + if (subKeyItem.mSecretKeyType == SecretKeyType.GNU_DUMMY) { + return KeyDisplayStatus.STRIPPED; + } + if (subKeyItem.mSecretKeyType == SecretKeyType.DIVERT_TO_CARD) { + return KeyDisplayStatus.DIVERT; + } + + return KeyDisplayStatus.OK; + } + + private void onLoadUnifiedKeyInfo(UnifiedKeyInfo unifiedKeyInfo) { + if (unifiedKeyInfo == null) { + return; + } + + Context context = requireContext(); + + this.unifiedKeyInfo = unifiedKeyInfo; + + boolean showLinkedIds = Preferences.getPreferences(context).getExperimentalEnableLinkedIdentities(); + boolean isSecret = unifiedKeyInfo.has_any_secret(); + identitiesCardView.setAddLinkedIdButtonVisible(showLinkedIds && isSecret); + identitiesCardView.setIdentitiesCardListener((v) -> addLinkedIdentity()); + } + + private void showIdentityInfo(final int position) { + IdentityInfo info = identitiesAdapter.getInfo(position); + if (info instanceof LinkedIdInfo) { + showLinkedId((LinkedIdInfo) info); + } else if (info instanceof UserIdInfo) { + showUserIdInfo((UserIdInfo) info); + } else if (info instanceof AutocryptPeerInfo) { + Intent autocryptPeerIntent = ((AutocryptPeerInfo) info).getAutocryptPeerIntent(); + if (autocryptPeerIntent != null) { + startActivity(autocryptPeerIntent); + } + } + } + + private void showIdentityContextMenu(int position, View anchor) { + showContextMenu(position, anchor); + } + + private void showLinkedId(final LinkedIdInfo info) { + LinkedIdViewFragment frag = LinkedIdViewFragment.newInstance(info.getMasterKeyId(), info.getRank(), unifiedKeyInfo.has_any_secret()); + + switchToFragment(frag, "linked_id"); + } + + private void showUserIdInfo(UserIdInfo info) { + if (!unifiedKeyInfo.has_any_secret()) { + UserIdInfoDialogFragment dialogFragment = UserIdInfoDialogFragment.newInstance(false, info.isVerified()); + showDialogFragment(dialogFragment, "userIdInfoDialog"); + } + } + + private void addLinkedIdentity() { + Intent intent = new Intent(requireContext(), LinkedIdWizard.class); + intent.putExtra(LinkedIdWizard.EXTRA_MASTER_KEY_ID, unifiedKeyInfo.master_key_id()); + startActivity(intent); + } + + public void onClickForgetIdentity(int position) { + AutocryptPeerInfo info = (AutocryptPeerInfo) identitiesAdapter.getInfo(position); + if (info == null) { + Timber.e("got a 'forget' click on a bad trust id"); + return; + } + + AutocryptPeerDao.getInstance(requireContext()).deleteByIdentifier(info.getPackageName(), info.getIdentity()); + } + + private void onLoadIdentityInfo(List identityInfos) { + identitiesAdapter.setData(identityInfos, unifiedKeyInfo.has_any_secret()); + identitiesCardView.setVisibility(View.VISIBLE); + } + + private void onLoadSystemContact(SystemContactInfo systemContactInfo) { + if (systemContactInfo == null) { + systemContactCard.hideLinkedSystemContact(); + return; + } + + systemContactCard.showLinkedSystemContact(systemContactInfo.contactName, systemContactInfo.contactPicture); + systemContactCard.setSystemContactClickListener((v) -> launchAndroidContactActivity(systemContactInfo.contactId)); + } + + private void onLoadKeyMetadata(KeyMetadata keyMetadata) { + if (keyMetadata == null) { + keyserverStatusView.setDisplayStatusUnknown(); + } else if (keyMetadata.hasBeenUpdated()) { + if (keyMetadata.isPublished()) { + keyserverStatusView.setDisplayStatusPublished(); + } else { + keyserverStatusView.setDisplayStatusNotPublished(); + } + keyserverStatusView.setLastUpdated(keyMetadata.last_updated()); + } else { + keyserverStatusView.setDisplayStatusUnknown(); + } + } + + public void switchToFragment(final Fragment frag, final String backStackName) { + new Handler().post(() -> requireFragmentManager().beginTransaction() + .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE) + .replace(R.id.view_key_fragment, frag) + .addToBackStack(backStackName) + .commit()); } @Override @@ -189,37 +345,18 @@ public class ViewKeyFragment extends LoaderFragment implements ViewKeyMvpView, O } } - public boolean isValidForData(boolean isSecret) { - return isSecret == mIsSecret; - } - - @Override - public void startActivityAndShowResultSnackbar(Intent intent) { - startActivityForResult(intent, 0); - } - - @Override public void showDialogFragment(final DialogFragment dialogFragment, final String tag) { - DialogFragmentWorkaround.INTERFACE.runnableRunDelayed(new Runnable() { - public void run() { - dialogFragment.show(getActivity().getSupportFragmentManager(), tag); - } - }); + DialogFragmentWorkaround.INTERFACE.runnableRunDelayed( + () -> dialogFragment.show(requireFragmentManager(), tag)); } - @Override public void showContextMenu(int position, View anchor) { displayedContextMenuPosition = position; - PopupMenu menu = new PopupMenu(getContext(), anchor); + PopupMenu menu = new PopupMenu(requireContext(), anchor); menu.inflate(R.menu.identity_context_menu); menu.setOnMenuItemClickListener(this); - menu.setOnDismissListener(new OnDismissListener() { - @Override - public void onDismiss(PopupMenu popupMenu) { - displayedContextMenuPosition = null; - } - }); + menu.setOnDismissListener(popupMenu -> displayedContextMenuPosition = null); menu.show(); } @@ -233,10 +370,17 @@ public class ViewKeyFragment extends LoaderFragment implements ViewKeyMvpView, O case R.id.autocrypt_forget: int position = displayedContextMenuPosition; displayedContextMenuPosition = null; - identitiesPresenter.onClickForgetIdentity(position); + onClickForgetIdentity(position); return true; } return false; } + + private void launchAndroidContactActivity(long contactId) { + Intent intent = new Intent(Intent.ACTION_VIEW); + Uri uri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_URI, String.valueOf(contactId)); + intent.setData(uri); + startActivity(intent); + } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/loader/IdentityDao.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/loader/IdentityDao.java index 8a3458a51..9bc8c7398 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/loader/IdentityDao.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/loader/IdentityDao.java @@ -23,7 +23,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -import android.content.ContentResolver; +import android.arch.persistence.db.SupportSQLiteDatabase; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; @@ -33,71 +33,45 @@ import android.graphics.drawable.Drawable; import android.support.annotation.Nullable; import com.google.auto.value.AutoValue; +import com.squareup.sqldelight.SqlDelightQuery; import org.openintents.openpgp.util.OpenPgpApi; import org.sufficientlysecure.keychain.linked.LinkedAttribute; import org.sufficientlysecure.keychain.linked.UriAttribute; -import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAutocryptPeer; -import org.sufficientlysecure.keychain.provider.KeychainContract.Certs; -import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets; +import org.sufficientlysecure.keychain.model.AutocryptPeer; +import org.sufficientlysecure.keychain.model.UserPacket; +import org.sufficientlysecure.keychain.model.UserPacket.UserAttribute; +import org.sufficientlysecure.keychain.model.UserPacket.UserId; +import org.sufficientlysecure.keychain.pgp.WrappedUserAttribute; +import org.sufficientlysecure.keychain.daos.AutocryptPeerDao; +import org.sufficientlysecure.keychain.KeychainDatabase; import org.sufficientlysecure.keychain.ui.util.PackageIconGetter; import timber.log.Timber; public class IdentityDao { - private static final String[] USER_PACKETS_PROJECTION = new String[]{ - UserPackets._ID, - UserPackets.TYPE, - UserPackets.USER_ID, - UserPackets.ATTRIBUTE_DATA, - UserPackets.RANK, - UserPackets.VERIFIED, - UserPackets.IS_PRIMARY, - UserPackets.IS_REVOKED, - UserPackets.NAME, - UserPackets.EMAIL, - UserPackets.COMMENT, - }; - private static final int INDEX_ID = 0; - private static final int INDEX_TYPE = 1; - private static final int INDEX_USER_ID = 2; - private static final int INDEX_ATTRIBUTE_DATA = 3; - private static final int INDEX_RANK = 4; - private static final int INDEX_VERIFIED = 5; - private static final int INDEX_IS_PRIMARY = 6; - private static final int INDEX_IS_REVOKED = 7; - private static final int INDEX_NAME = 8; - private static final int INDEX_EMAIL = 9; - private static final int INDEX_COMMENT = 10; - - private static final String USER_IDS_WHERE = UserPackets.IS_REVOKED + " = 0"; - - private static final String[] AUTOCRYPT_PEER_PROJECTION = new String[] { - ApiAutocryptPeer._ID, - ApiAutocryptPeer.PACKAGE_NAME, - ApiAutocryptPeer.IDENTIFIER, - }; - private static final int INDEX_PACKAGE_NAME = 1; - private static final int INDEX_IDENTIFIER = 2; - - - private final ContentResolver contentResolver; + private final SupportSQLiteDatabase db; private final PackageIconGetter packageIconGetter; private final PackageManager packageManager; + private final AutocryptPeerDao autocryptPeerDao; - static IdentityDao getInstance(Context context) { - ContentResolver contentResolver = context.getContentResolver(); + public static IdentityDao getInstance(Context context) { + SupportSQLiteDatabase db = KeychainDatabase.getInstance(context).getWritableDatabase(); PackageManager packageManager = context.getPackageManager(); PackageIconGetter iconGetter = PackageIconGetter.getInstance(context); - return new IdentityDao(contentResolver, packageManager, iconGetter); + AutocryptPeerDao autocryptPeerDao = AutocryptPeerDao.getInstance(context); + return new IdentityDao(db, packageManager, iconGetter, autocryptPeerDao); } - private IdentityDao(ContentResolver contentResolver, PackageManager packageManager, PackageIconGetter iconGetter) { + private IdentityDao(SupportSQLiteDatabase db, + PackageManager packageManager, PackageIconGetter iconGetter, + AutocryptPeerDao autocryptPeerDao) { + this.db = db; this.packageManager = packageManager; - this.contentResolver = contentResolver; this.packageIconGetter = iconGetter; + this.autocryptPeerDao = autocryptPeerDao; } - List getIdentityInfos(long masterKeyId, boolean showLinkedIds) { + public List getIdentityInfos(long masterKeyId, boolean showLinkedIds) { ArrayList identities = new ArrayList<>(); if (showLinkedIds) { @@ -110,35 +84,24 @@ public class IdentityDao { } private void correlateOrAddAutocryptPeers(ArrayList identities, long masterKeyId) { - Cursor cursor = contentResolver.query(ApiAutocryptPeer.buildByMasterKeyId(masterKeyId), - AUTOCRYPT_PEER_PROJECTION, null, null, null); - if (cursor == null) { - Timber.e("Error loading Autocrypt peers"); - return; - } + for (AutocryptPeer autocryptPeer : autocryptPeerDao.getAutocryptPeersForKey(masterKeyId)) { + String packageName = autocryptPeer.package_name(); + String autocryptId = autocryptPeer.identifier(); - try { - while (cursor.moveToNext()) { - String packageName = cursor.getString(INDEX_PACKAGE_NAME); - String autocryptPeer = cursor.getString(INDEX_IDENTIFIER); + Drawable drawable = packageIconGetter.getDrawableForPackageName(packageName); + Intent autocryptPeerIntent = getAutocryptPeerActivityIntentIfResolvable(packageName, autocryptId); - Drawable drawable = packageIconGetter.getDrawableForPackageName(packageName); - Intent autocryptPeerIntent = getAutocryptPeerActivityIntentIfResolvable(packageName, autocryptPeer); - - UserIdInfo associatedUserIdInfo = findUserIdMatchingAutocryptPeer(identities, autocryptPeer); - if (associatedUserIdInfo != null) { - int position = identities.indexOf(associatedUserIdInfo); - AutocryptPeerInfo autocryptPeerInfo = AutocryptPeerInfo - .create(associatedUserIdInfo, autocryptPeer, packageName, drawable, autocryptPeerIntent); - identities.set(position, autocryptPeerInfo); - } else { - AutocryptPeerInfo autocryptPeerInfo = AutocryptPeerInfo - .create(autocryptPeer, packageName, drawable, autocryptPeerIntent); - identities.add(autocryptPeerInfo); - } + UserIdInfo associatedUserIdInfo = findUserIdMatchingAutocryptPeer(identities, autocryptId); + if (associatedUserIdInfo != null) { + int position = identities.indexOf(associatedUserIdInfo); + AutocryptPeerInfo autocryptPeerInfo = AutocryptPeerInfo + .create(masterKeyId, associatedUserIdInfo, autocryptId, packageName, drawable, autocryptPeerIntent); + identities.set(position, autocryptPeerInfo); + } else { + AutocryptPeerInfo autocryptPeerInfo = AutocryptPeerInfo + .create(masterKeyId, autocryptId, packageName, drawable, autocryptPeerIntent); + identities.add(autocryptPeerInfo); } - } finally { - cursor.close(); } } @@ -170,73 +133,72 @@ public class IdentityDao { } private void loadLinkedIds(ArrayList identities, long masterKeyId) { - Cursor cursor = contentResolver.query(UserPackets.buildLinkedIdsUri(masterKeyId), - USER_PACKETS_PROJECTION, USER_IDS_WHERE, null, null); - if (cursor == null) { - Timber.e("Error loading key items!"); - return; - } - - try { + SqlDelightQuery query = UserPacket.FACTORY.selectUserAttributesByTypeAndMasterKeyId( + (long) WrappedUserAttribute.UAT_URI_ATTRIBUTE, masterKeyId); + try (Cursor cursor = db.query(query)) { while (cursor.moveToNext()) { - int rank = cursor.getInt(INDEX_RANK); - int verified = cursor.getInt(INDEX_VERIFIED); - boolean isPrimary = cursor.getInt(INDEX_IS_PRIMARY) != 0; + UserAttribute userAttribute = UserPacket.USER_ATTRIBUTE_MAPPER.map(cursor); - byte[] data = cursor.getBlob(INDEX_ATTRIBUTE_DATA); - try { - UriAttribute uriAttribute = LinkedAttribute.fromAttributeData(data); - if (uriAttribute instanceof LinkedAttribute) { - LinkedIdInfo identityInfo = LinkedIdInfo.create(rank, verified, isPrimary, uriAttribute); - identities.add(identityInfo); - } - } catch (IOException e) { - Timber.e(e, "Failed parsing uri attribute"); - } + LinkedIdInfo linkedIdInfo = parseLinkedIdInfo(userAttribute); + identities.add(linkedIdInfo); } - } finally { - cursor.close(); } } - private void loadUserIds(ArrayList identities, long masterKeyId) { - Cursor cursor = contentResolver.query(UserPackets.buildUserIdsUri(masterKeyId), - USER_PACKETS_PROJECTION, USER_IDS_WHERE, null, null); - if (cursor == null) { - Timber.e("Error loading key items!"); - return; + public LinkedIdInfo getLinkedIdInfo(long masterKeyId, int rank) { + SqlDelightQuery query = UserPacket.FACTORY.selectSpecificUserAttribute( + (long) WrappedUserAttribute.UAT_URI_ATTRIBUTE, masterKeyId, rank); + try (Cursor cursor = db.query(query)) { + if (cursor.moveToFirst()) { + UserAttribute userAttribute = UserPacket.USER_ATTRIBUTE_MAPPER.map(cursor); + + return parseLinkedIdInfo(userAttribute); + } } + return null; + } + @Nullable + private LinkedIdInfo parseLinkedIdInfo(UserAttribute userAttribute) { try { + UriAttribute uriAttribute = LinkedAttribute.fromAttributeData(userAttribute.attribute_data()); + if (uriAttribute instanceof LinkedAttribute) { + return LinkedIdInfo.create(userAttribute.master_key_id(), userAttribute.rank(), + userAttribute.isVerified(), userAttribute.is_primary(), (LinkedAttribute) uriAttribute); + } + } catch (IOException e) { + Timber.e(e, "Failed parsing uri attribute"); + } + return null; + } + + private void loadUserIds(ArrayList identities, long... masterKeyId) { + SqlDelightQuery query = UserPacket.FACTORY.selectUserIdsByMasterKeyId(masterKeyId); + try (Cursor cursor = db.query(query)) { while (cursor.moveToNext()) { - int rank = cursor.getInt(INDEX_RANK); - int verified = cursor.getInt(INDEX_VERIFIED); - boolean isPrimary = cursor.getInt(INDEX_IS_PRIMARY) != 0; + UserId userId = UserPacket.USER_ID_MAPPER.map(cursor); - if (!cursor.isNull(INDEX_NAME) || !cursor.isNull(INDEX_EMAIL)) { - String name = cursor.getString(INDEX_NAME); - String email = cursor.getString(INDEX_EMAIL); - String comment = cursor.getString(INDEX_COMMENT); - - IdentityInfo identityInfo = UserIdInfo.create(rank, verified, isPrimary, name, email, comment); + if (userId.name() != null || userId.email() != null) { + IdentityInfo identityInfo = UserIdInfo.create( + userId.master_key_id(), userId.rank(), userId.isVerified(), userId.is_primary(), userId.name(), userId.email(), userId.comment()); identities.add(identityInfo); } } - } finally { - cursor.close(); } } public interface IdentityInfo { + long getMasterKeyId(); int getRank(); - int getVerified(); + boolean isVerified(); boolean isPrimary(); } @AutoValue public abstract static class UserIdInfo implements IdentityInfo { + public abstract long getMasterKeyId(); public abstract int getRank(); - public abstract int getVerified(); + public abstract boolean isVerified(); public abstract boolean isPrimary(); @Nullable @@ -246,29 +208,31 @@ public class IdentityDao { @Nullable public abstract String getComment(); - static UserIdInfo create(int rank, int verified, boolean isPrimary, String name, String email, + static UserIdInfo create(long masterKeyId, int rank, boolean isVerified, boolean isPrimary, String name, String email, String comment) { - return new AutoValue_IdentityDao_UserIdInfo(rank, verified, isPrimary, name, email, comment); + return new AutoValue_IdentityDao_UserIdInfo(masterKeyId, rank, isVerified, isPrimary, name, email, comment); } } @AutoValue public abstract static class LinkedIdInfo implements IdentityInfo { + public abstract long getMasterKeyId(); public abstract int getRank(); - public abstract int getVerified(); + public abstract boolean isVerified(); public abstract boolean isPrimary(); - public abstract UriAttribute getUriAttribute(); + public abstract LinkedAttribute getLinkedAttribute(); - static LinkedIdInfo create(int rank, int verified, boolean isPrimary, UriAttribute uriAttribute) { - return new AutoValue_IdentityDao_LinkedIdInfo(rank, verified, isPrimary, uriAttribute); + static LinkedIdInfo create(long masterKeyId, int rank, boolean isVerified, boolean isPrimary, LinkedAttribute linkedAttribute) { + return new AutoValue_IdentityDao_LinkedIdInfo(masterKeyId, rank, isVerified, isPrimary, linkedAttribute); } } @AutoValue public abstract static class AutocryptPeerInfo implements IdentityInfo { + public abstract long getMasterKeyId(); public abstract int getRank(); - public abstract int getVerified(); + public abstract boolean isVerified(); public abstract boolean isPrimary(); public abstract String getIdentity(); @@ -280,15 +244,14 @@ public class IdentityDao { @Nullable public abstract Intent getAutocryptPeerIntent(); - static AutocryptPeerInfo create(UserIdInfo userIdInfo, String autocryptPeer, String packageName, + static AutocryptPeerInfo create(long masterKeyId, UserIdInfo userIdInfo, String autocryptPeer, String packageName, Drawable appIcon, Intent autocryptPeerIntent) { - return new AutoValue_IdentityDao_AutocryptPeerInfo(userIdInfo.getRank(), userIdInfo.getVerified(), + return new AutoValue_IdentityDao_AutocryptPeerInfo(masterKeyId, userIdInfo.getRank(), userIdInfo.isVerified(), userIdInfo.isPrimary(), autocryptPeer, packageName, appIcon, userIdInfo, autocryptPeerIntent); } - static AutocryptPeerInfo create(String autocryptPeer, String packageName, Drawable appIcon, Intent autocryptPeerIntent) { - return new AutoValue_IdentityDao_AutocryptPeerInfo( - 0, Certs.VERIFIED_SELF, false, autocryptPeer, packageName, appIcon, null, autocryptPeerIntent); + static AutocryptPeerInfo create(long masterKeyId, String autocryptPeer, String packageName, Drawable appIcon, Intent autocryptPeerIntent) { + return new AutoValue_IdentityDao_AutocryptPeerInfo(masterKeyId,0, false, false, autocryptPeer, packageName, appIcon, null, autocryptPeerIntent); } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/loader/KeyserverStatusDao.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/loader/KeyserverStatusDao.java deleted file mode 100644 index 16e6828c6..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/loader/KeyserverStatusDao.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright (C) 2017 Schürmann & Breitmoser GbR - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sufficientlysecure.keychain.ui.keyview.loader; - - -import java.util.Date; - -import android.content.ContentResolver; -import android.content.Context; -import android.database.Cursor; - -import org.sufficientlysecure.keychain.provider.KeychainContract.UpdatedKeys; -import timber.log.Timber; - - -public class KeyserverStatusDao { - public static final String[] PROJECTION = new String[] { - UpdatedKeys.LAST_UPDATED, - UpdatedKeys.SEEN_ON_KEYSERVERS - }; - private static final int INDEX_LAST_UPDATED = 0; - private static final int INDEX_SEEN_ON_KEYSERVERS = 1; - - - private final ContentResolver contentResolver; - - public static KeyserverStatusDao getInstance(Context context) { - ContentResolver contentResolver = context.getContentResolver(); - return new KeyserverStatusDao(contentResolver); - } - - private KeyserverStatusDao(ContentResolver contentResolver) { - this.contentResolver = contentResolver; - } - - public KeyserverStatus getKeyserverStatus(long masterKeyId) { - Cursor cursor = contentResolver.query(UpdatedKeys.CONTENT_URI, PROJECTION, - UpdatedKeys.MASTER_KEY_ID + " = ?", new String[] { Long.toString(masterKeyId) }, null); - if (cursor == null) { - Timber.e("Error loading key items!"); - return null; - } - - try { - if (cursor.moveToFirst()) { - if (cursor.isNull(INDEX_SEEN_ON_KEYSERVERS) || cursor.isNull(INDEX_LAST_UPDATED)) { - return new KeyserverStatus(masterKeyId); - } - - boolean isPublished = cursor.getInt(INDEX_SEEN_ON_KEYSERVERS) != 0; - Date lastUpdated = new Date(cursor.getLong(INDEX_LAST_UPDATED) * 1000); - - return new KeyserverStatus(masterKeyId, isPublished, lastUpdated); - } - - return new KeyserverStatus(masterKeyId); - } finally { - cursor.close(); - } - } - - public static class KeyserverStatus { - private final long masterKeyId; - private final boolean isPublished; - private final Date lastUpdated; - - KeyserverStatus(long masterKeyId, boolean isPublished, Date lastUpdated) { - this.masterKeyId = masterKeyId; - this.isPublished = isPublished; - this.lastUpdated = lastUpdated; - } - - KeyserverStatus(long masterKeyId) { - this.masterKeyId = masterKeyId; - this.isPublished = false; - this.lastUpdated = null; - } - - long getMasterKeyId() { - return masterKeyId; - } - - public boolean hasBeenUpdated() { - return lastUpdated != null; - } - - public boolean isPublished() { - if (lastUpdated == null) { - throw new IllegalStateException("Cannot get publication state if key has never been updated!"); - } - return isPublished; - } - - public Date getLastUpdated() { - return lastUpdated; - } - } -} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/loader/SubkeyStatusDao.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/loader/SubkeyStatusDao.java index 8caf8b261..2125527f7 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/loader/SubkeyStatusDao.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/loader/SubkeyStatusDao.java @@ -24,97 +24,138 @@ import java.util.Comparator; import java.util.Date; import java.util.List; -import android.content.ContentResolver; import android.content.Context; -import android.database.Cursor; import android.support.annotation.NonNull; +import org.sufficientlysecure.keychain.model.SubKey; import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey.SecretKeyType; import org.sufficientlysecure.keychain.pgp.PgpSecurityConstants; import org.sufficientlysecure.keychain.pgp.SecurityProblem.KeySecurityProblem; -import org.sufficientlysecure.keychain.provider.KeychainContract.Keys; -import timber.log.Timber; +import org.sufficientlysecure.keychain.daos.KeyRepository; public class SubkeyStatusDao { - public static final String[] PROJECTION = new String[] { - Keys.KEY_ID, - Keys.CREATION, - Keys.CAN_CERTIFY, - Keys.CAN_SIGN, - Keys.CAN_ENCRYPT, - Keys.HAS_SECRET, - Keys.EXPIRY, - Keys.IS_REVOKED, - Keys.ALGORITHM, - Keys.KEY_SIZE, - Keys.KEY_CURVE_OID - }; - private static final int INDEX_KEY_ID = 0; - private static final int INDEX_CREATION = 1; - private static final int INDEX_CAN_CERTIFY = 2; - private static final int INDEX_CAN_SIGN = 3; - private static final int INDEX_CAN_ENCRYPT = 4; - private static final int INDEX_HAS_SECRET = 5; - private static final int INDEX_EXPIRY = 6; - private static final int INDEX_IS_REVOKED = 7; - private static final int INDEX_ALGORITHM = 8; - private static final int INDEX_KEY_SIZE = 9; - private static final int INDEX_KEY_CURVE_OID = 10; - - - private final ContentResolver contentResolver; + private final KeyRepository keyRepository; public static SubkeyStatusDao getInstance(Context context) { - ContentResolver contentResolver = context.getContentResolver(); - return new SubkeyStatusDao(contentResolver); + KeyRepository keyRepository = KeyRepository.create(context); + return new SubkeyStatusDao(keyRepository); } - private SubkeyStatusDao(ContentResolver contentResolver) { - this.contentResolver = contentResolver; + private SubkeyStatusDao(KeyRepository keyRepository) { + this.keyRepository = keyRepository; } - KeySubkeyStatus getSubkeyStatus(long masterKeyId, Comparator comparator) { - Cursor cursor = contentResolver.query(Keys.buildKeysUri(masterKeyId), PROJECTION, null, null, null); - if (cursor == null) { - Timber.e("Error loading key items!"); + public KeySubkeyStatus getSubkeyStatus(long masterKeyId) { + SubKeyItem keyCertify = null; + ArrayList keysSign = new ArrayList<>(); + ArrayList keysEncrypt = new ArrayList<>(); + for (SubKey subKey : keyRepository.getSubKeysByMasterKeyId(masterKeyId)) { + SubKeyItem ski = new SubKeyItem(masterKeyId, subKey); + + if (ski.mKeyId == masterKeyId) { + keyCertify = ski; + } + + if (ski.mCanSign) { + keysSign.add(ski); + } + if (ski.mCanEncrypt) { + keysEncrypt.add(ski); + } + } + + if (keyCertify == null) { + if (!keysSign.isEmpty() || !keysEncrypt.isEmpty()) { + throw new IllegalStateException("Certification key can't be missing for a key that hasn't been deleted!"); + } return null; } - try { - SubKeyItem keyCertify = null; - ArrayList keysSign = new ArrayList<>(); - ArrayList keysEncrypt = new ArrayList<>(); - while (cursor.moveToNext()) { - SubKeyItem ski = new SubKeyItem(masterKeyId, cursor); + Collections.sort(keysSign, SUBKEY_COMPARATOR); + Collections.sort(keysEncrypt, SUBKEY_COMPARATOR); - if (ski.mKeyId == masterKeyId) { - keyCertify = ski; - } + KeyHealthStatus keyHealthStatus = determineKeyHealthStatus(keyCertify, keysSign, keysEncrypt); - if (ski.mCanSign) { - keysSign.add(ski); - } - if (ski.mCanEncrypt) { - keysEncrypt.add(ski); - } - } + return new KeySubkeyStatus(keyCertify, keysSign, keysEncrypt, keyHealthStatus); + } - if (keyCertify == null) { - if (!keysSign.isEmpty() || !keysEncrypt.isEmpty()) { - throw new IllegalStateException("Certification key can't be missing for a key that hasn't been deleted!"); - } - return null; - } - - Collections.sort(keysSign, comparator); - Collections.sort(keysEncrypt, comparator); - - return new KeySubkeyStatus(keyCertify, keysSign, keysEncrypt); - } finally { - cursor.close(); + private KeyHealthStatus determineKeyHealthStatus(SubKeyItem keyCertify, + ArrayList keysSign, + ArrayList keysEncrypt) { + if (keyCertify.mIsRevoked) { + return KeyHealthStatus.REVOKED; } + + if (keyCertify.mIsExpired) { + return KeyHealthStatus.EXPIRED; + } + + if (keyCertify.mSecurityProblem != null) { + return KeyHealthStatus.INSECURE; + } + + if (!keysSign.isEmpty() && keysEncrypt.isEmpty()) { + SubKeyItem keySign = keysSign.get(0); + if (!keySign.isValid()) { + return KeyHealthStatus.BROKEN; + } + + if (keySign.mSecurityProblem != null) { + return KeyHealthStatus.INSECURE; + } + + return KeyHealthStatus.SIGN_ONLY; + } + + if (keysSign.isEmpty() || keysEncrypt.isEmpty()) { + return KeyHealthStatus.BROKEN; + } + + SubKeyItem keySign = keysSign.get(0); + SubKeyItem keyEncrypt = keysEncrypt.get(0); + + if (keySign.mSecurityProblem != null && keySign.isValid() + || keyEncrypt.mSecurityProblem != null && keyEncrypt.isValid()) { + return KeyHealthStatus.INSECURE; + } + + if (!keySign.isValid() || !keyEncrypt.isValid()) { + return KeyHealthStatus.BROKEN; + } + + if (keyCertify.mSecretKeyType == SecretKeyType.GNU_DUMMY + && keySign.mSecretKeyType == SecretKeyType.GNU_DUMMY + && keyEncrypt.mSecretKeyType == SecretKeyType.GNU_DUMMY) { + return KeyHealthStatus.STRIPPED; + } + + if (keyCertify.mSecretKeyType == SecretKeyType.DIVERT_TO_CARD + && keySign.mSecretKeyType == SecretKeyType.DIVERT_TO_CARD + && keyEncrypt.mSecretKeyType == SecretKeyType.DIVERT_TO_CARD) { + return KeyHealthStatus.DIVERT; + } + + boolean containsDivertKeys = keyCertify.mSecretKeyType == SecretKeyType.DIVERT_TO_CARD || + keySign.mSecretKeyType == SecretKeyType.DIVERT_TO_CARD || + keyEncrypt.mSecretKeyType == SecretKeyType.DIVERT_TO_CARD; + if (containsDivertKeys) { + return KeyHealthStatus.DIVERT_PARTIAL; + } + + boolean containsStrippedKeys = keyCertify.mSecretKeyType == SecretKeyType.GNU_DUMMY + || keySign.mSecretKeyType == SecretKeyType.GNU_DUMMY + || keyEncrypt.mSecretKeyType == SecretKeyType.GNU_DUMMY; + if (containsStrippedKeys) { + return KeyHealthStatus.PARTIAL_STRIPPED; + } + + return KeyHealthStatus.OK; + } + + public enum KeyHealthStatus { + OK, DIVERT, DIVERT_PARTIAL, REVOKED, EXPIRED, INSECURE, SIGN_ONLY, STRIPPED, PARTIAL_STRIPPED, BROKEN } public static class KeySubkeyStatus { @@ -122,16 +163,18 @@ public class SubkeyStatusDao { public final SubKeyItem keyCertify; public final List keysSign; public final List keysEncrypt; + public final KeyHealthStatus keyHealthStatus; - KeySubkeyStatus(@NonNull SubKeyItem keyCertify, List keysSign, List keysEncrypt) { + KeySubkeyStatus(@NonNull SubKeyItem keyCertify, List keysSign, List keysEncrypt, + KeyHealthStatus keyHealthStatus) { this.keyCertify = keyCertify; this.keysSign = keysSign; this.keysEncrypt = keysEncrypt; + this.keyHealthStatus = keyHealthStatus; } } public static class SubKeyItem { - final int mPosition; final long mKeyId; final Date mCreation; public final SecretKeyType mSecretKeyType; @@ -140,25 +183,23 @@ public class SubkeyStatusDao { final boolean mCanCertify, mCanSign, mCanEncrypt; public final KeySecurityProblem mSecurityProblem; - SubKeyItem(long masterKeyId, Cursor cursor) { - mPosition = cursor.getPosition(); + SubKeyItem(long masterKeyId, SubKey subKey) { + mKeyId = subKey.key_id(); + mCreation = new Date(subKey.creation() * 1000); - mKeyId = cursor.getLong(INDEX_KEY_ID); - mCreation = new Date(cursor.getLong(INDEX_CREATION) * 1000); + mSecretKeyType = subKey.has_secret(); - mSecretKeyType = SecretKeyType.fromNum(cursor.getInt(INDEX_HAS_SECRET)); - - mIsRevoked = cursor.getInt(INDEX_IS_REVOKED) > 0; - mExpiry = cursor.isNull(INDEX_EXPIRY) ? null : new Date(cursor.getLong(INDEX_EXPIRY) * 1000); + mIsRevoked = subKey.is_revoked(); + mExpiry = subKey.expiry() == null ? null : new Date(subKey.expiry() * 1000); mIsExpired = mExpiry != null && mExpiry.before(new Date()); - mCanCertify = cursor.getInt(INDEX_CAN_CERTIFY) > 0; - mCanSign = cursor.getInt(INDEX_CAN_SIGN) > 0; - mCanEncrypt = cursor.getInt(INDEX_CAN_ENCRYPT) > 0; + mCanCertify = subKey.can_certify(); + mCanSign = subKey.can_sign(); + mCanEncrypt = subKey.can_encrypt(); - int algorithm = cursor.getInt(INDEX_ALGORITHM); - Integer bitStrength = cursor.isNull(INDEX_KEY_SIZE) ? null : cursor.getInt(INDEX_KEY_SIZE); - String curveOid = cursor.getString(INDEX_KEY_CURVE_OID); + int algorithm = subKey.algorithm(); + Integer bitStrength = subKey.key_size(); + String curveOid = subKey.key_curve_oid(); mSecurityProblem = PgpSecurityConstants.getKeySecurityProblem( masterKeyId, mKeyId, algorithm, bitStrength, curveOid); @@ -172,4 +213,24 @@ public class SubkeyStatusDao { return !mIsRevoked && !mIsExpired; } } + + private static final Comparator SUBKEY_COMPARATOR = (one, two) -> { + if (one == two) { + return 0; + } + // if one is valid and the other isn't, the valid one always comes first + if (one.isValid() ^ two.isValid()) { + return one.isValid() ? -1 : 1; + } + // compare usability, if one is "more usable" than the other, that one comes first + int usability = one.mSecretKeyType.compareUsability(two.mSecretKeyType); + if (usability != 0) { + return usability; + } + if ((one.mSecurityProblem == null) ^ (two.mSecurityProblem == null)) { + return one.mSecurityProblem == null ? -1 : 1; + } + // otherwise, the newer one comes first + return one.newerThan(two) ? -1 : 1; + }; } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/loader/SystemContactDao.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/loader/SystemContactDao.java index e666920c4..499f36347 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/loader/SystemContactDao.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/loader/SystemContactDao.java @@ -60,7 +60,7 @@ public class SystemContactDao { this.contentResolver = contentResolver; } - SystemContactInfo getSystemContactInfo(long masterKeyId, boolean isSecret) { + public SystemContactInfo getSystemContactInfo(long masterKeyId, boolean isSecret) { if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_DENIED) { Timber.w(Constants.TAG, "loading linked system contact not possible READ_CONTACTS permission denied!"); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/loader/ViewKeyLiveData.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/loader/ViewKeyLiveData.java deleted file mode 100644 index c9bb64dd9..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/loader/ViewKeyLiveData.java +++ /dev/null @@ -1,98 +0,0 @@ -package org.sufficientlysecure.keychain.ui.keyview.loader; - - -import java.util.Comparator; -import java.util.List; - -import android.content.Context; - -import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; -import org.sufficientlysecure.keychain.ui.keyview.loader.IdentityDao.IdentityInfo; -import org.sufficientlysecure.keychain.ui.keyview.loader.KeyserverStatusDao.KeyserverStatus; -import org.sufficientlysecure.keychain.ui.keyview.loader.SubkeyStatusDao.KeySubkeyStatus; -import org.sufficientlysecure.keychain.ui.keyview.loader.SubkeyStatusDao.SubKeyItem; -import org.sufficientlysecure.keychain.ui.keyview.loader.SystemContactDao.SystemContactInfo; - - -public class ViewKeyLiveData { - public static class IdentityLiveData extends AsyncTaskLiveData> { - private final IdentityDao identityDao; - - private final long masterKeyId; - private final boolean showLinkedIds; - - public IdentityLiveData(Context context, long masterKeyId, boolean showLinkedIds) { - super(context, KeyRings.buildGenericKeyRingUri(masterKeyId)); - - this.identityDao = IdentityDao.getInstance(context); - - this.masterKeyId = masterKeyId; - this.showLinkedIds = showLinkedIds; - } - - @Override - public List asyncLoadData() { - return identityDao.getIdentityInfos(masterKeyId, showLinkedIds); - } - } - - public static class SubkeyStatusLiveData extends AsyncTaskLiveData { - private final SubkeyStatusDao subkeyStatusDao; - - private final long masterKeyId; - private final Comparator comparator; - - public SubkeyStatusLiveData(Context context, long masterKeyId, Comparator comparator) { - super(context, KeyRings.buildGenericKeyRingUri(masterKeyId)); - - this.subkeyStatusDao = SubkeyStatusDao.getInstance(context); - - this.masterKeyId = masterKeyId; - this.comparator = comparator; - } - - @Override - public KeySubkeyStatus asyncLoadData() { - return subkeyStatusDao.getSubkeyStatus(masterKeyId, comparator); - } - } - - public static class SystemContactInfoLiveData extends AsyncTaskLiveData { - private final SystemContactDao systemContactDao; - - private final long masterKeyId; - private final boolean isSecret; - - public SystemContactInfoLiveData(Context context, long masterKeyId, boolean isSecret) { - super(context, null); - - this.systemContactDao = SystemContactDao.getInstance(context); - - this.masterKeyId = masterKeyId; - this.isSecret = isSecret; - } - - @Override - public SystemContactInfo asyncLoadData() { - return systemContactDao.getSystemContactInfo(masterKeyId, isSecret); - } - } - - public static class KeyserverStatusLiveData extends AsyncTaskLiveData { - private final KeyserverStatusDao keyserverStatusDao; - - private final long masterKeyId; - - public KeyserverStatusLiveData(Context context, long masterKeyId) { - super(context, KeyRings.buildGenericKeyRingUri(masterKeyId)); - - this.keyserverStatusDao = KeyserverStatusDao.getInstance(context); - this.masterKeyId = masterKeyId; - } - - @Override - public KeyserverStatus asyncLoadData() { - return keyserverStatusDao.getKeyserverStatus(masterKeyId); - } - } -} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/presenter/IdentitiesPresenter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/presenter/IdentitiesPresenter.java deleted file mode 100644 index 28cd69b5b..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/presenter/IdentitiesPresenter.java +++ /dev/null @@ -1,167 +0,0 @@ -/* - * Copyright (C) 2017 Schürmann & Breitmoser GbR - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sufficientlysecure.keychain.ui.keyview.presenter; - - -import java.io.IOException; -import java.util.List; - -import android.arch.lifecycle.LiveData; -import android.arch.lifecycle.Observer; -import android.content.Context; -import android.content.Intent; -import android.net.Uri; -import android.support.annotation.Nullable; -import android.view.View; - -import org.sufficientlysecure.keychain.provider.AutocryptPeerDataAccessObject; -import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; -import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets; -import org.sufficientlysecure.keychain.ui.adapter.IdentityAdapter; -import org.sufficientlysecure.keychain.ui.adapter.IdentityAdapter.IdentityClickListener; -import org.sufficientlysecure.keychain.ui.dialog.UserIdInfoDialogFragment; -import org.sufficientlysecure.keychain.ui.keyview.LinkedIdViewFragment; -import org.sufficientlysecure.keychain.ui.keyview.loader.IdentityDao.AutocryptPeerInfo; -import org.sufficientlysecure.keychain.ui.keyview.loader.IdentityDao.IdentityInfo; -import org.sufficientlysecure.keychain.ui.keyview.loader.IdentityDao.LinkedIdInfo; -import org.sufficientlysecure.keychain.ui.keyview.loader.IdentityDao.UserIdInfo; -import org.sufficientlysecure.keychain.ui.keyview.loader.ViewKeyLiveData.IdentityLiveData; -import org.sufficientlysecure.keychain.ui.linked.LinkedIdWizard; -import org.sufficientlysecure.keychain.util.Preferences; -import timber.log.Timber; - - -public class IdentitiesPresenter implements Observer> { - private final Context context; - private final IdentitiesMvpView view; - private final ViewKeyMvpView viewKeyMvpView; - - private final IdentityAdapter identitiesAdapter; - - private final long masterKeyId; - private final boolean isSecret; - private final boolean showLinkedIds; - - public IdentitiesPresenter(Context context, IdentitiesMvpView view, ViewKeyMvpView viewKeyMvpView, - long masterKeyId, boolean isSecret) { - this.context = context; - this.view = view; - this.viewKeyMvpView = viewKeyMvpView; - - this.masterKeyId = masterKeyId; - this.isSecret = isSecret; - - showLinkedIds = Preferences.getPreferences(context).getExperimentalEnableLinkedIdentities(); - - identitiesAdapter = new IdentityAdapter(context, isSecret, new IdentityClickListener() { - @Override - public void onClickIdentity(int position) { - showIdentityInfo(position); - } - - @Override - public void onClickIdentityMore(int position, View anchor) { - showIdentityContextMenu(position, anchor); - - } - }); - view.setIdentitiesAdapter(identitiesAdapter); - - view.setAddLinkedIdButtonVisible(showLinkedIds && isSecret); - - view.setIdentitiesCardListener(() -> addLinkedIdentity()); - } - - @Override - public void onChanged(@Nullable List identityInfos) { - viewKeyMvpView.setContentShown(true, false); - identitiesAdapter.setData(identityInfos); - } - - private void showIdentityInfo(final int position) { - IdentityInfo info = identitiesAdapter.getInfo(position); - if (info instanceof LinkedIdInfo) { - showLinkedId((LinkedIdInfo) info); - } else if (info instanceof UserIdInfo) { - showUserIdInfo((UserIdInfo) info); - } else if (info instanceof AutocryptPeerInfo) { - Intent autocryptPeerIntent = ((AutocryptPeerInfo) info).getAutocryptPeerIntent(); - if (autocryptPeerIntent != null) { - viewKeyMvpView.startActivity(autocryptPeerIntent); - } - } - } - - private void showIdentityContextMenu(int position, View anchor) { - viewKeyMvpView.showContextMenu(position, anchor); - } - - private void showLinkedId(final LinkedIdInfo info) { - final LinkedIdViewFragment frag; - try { - Uri dataUri = UserPackets.buildLinkedIdsUri(KeyRings.buildGenericKeyRingUri(masterKeyId)); - frag = LinkedIdViewFragment.newInstance(dataUri, info.getRank(), isSecret, masterKeyId); - } catch (IOException e) { - Timber.e(e, "IOException"); - return; - } - - viewKeyMvpView.switchToFragment(frag, "linked_id"); - } - - private void showUserIdInfo(UserIdInfo info) { - if (!isSecret) { - final int isVerified = info.getVerified(); - - UserIdInfoDialogFragment dialogFragment = UserIdInfoDialogFragment.newInstance(false, isVerified); - viewKeyMvpView.showDialogFragment(dialogFragment, "userIdInfoDialog"); - } - } - - private void addLinkedIdentity() { - Intent intent = new Intent(context, LinkedIdWizard.class); - intent.setData(KeyRings.buildUnifiedKeyRingUri(masterKeyId)); - context.startActivity(intent); - } - - public void onClickForgetIdentity(int position) { - AutocryptPeerInfo info = (AutocryptPeerInfo) identitiesAdapter.getInfo(position); - if (info == null) { - Timber.e("got a 'forget' click on a bad trust id"); - return; - } - - AutocryptPeerDataAccessObject autocryptPeerDao = - new AutocryptPeerDataAccessObject(context, info.getPackageName()); - autocryptPeerDao.delete(info.getIdentity()); - } - - public LiveData> getLiveDataInstance() { - return new IdentityLiveData(context, masterKeyId, showLinkedIds); - } - - public interface IdentitiesMvpView { - void setIdentitiesAdapter(IdentityAdapter userIdsAdapter); - void setIdentitiesCardListener(IdentitiesCardListener identitiesCardListener); - void setAddLinkedIdButtonVisible(boolean showLinkedIds); - } - - public interface IdentitiesCardListener { - void onClickAddIdentity(); - } -} \ No newline at end of file diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/presenter/KeyHealthPresenter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/presenter/KeyHealthPresenter.java deleted file mode 100644 index 3a9e11d63..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/presenter/KeyHealthPresenter.java +++ /dev/null @@ -1,279 +0,0 @@ -/* - * Copyright (C) 2017 Schürmann & Breitmoser GbR - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sufficientlysecure.keychain.ui.keyview.presenter; - - -import java.util.Comparator; -import java.util.Date; - -import android.app.Application; -import android.arch.lifecycle.LiveData; -import android.arch.lifecycle.Observer; -import android.content.Context; -import android.support.annotation.Nullable; - -import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey.SecretKeyType; -import org.sufficientlysecure.keychain.pgp.SecurityProblem.KeySecurityProblem; -import org.sufficientlysecure.keychain.ui.keyview.loader.SubkeyStatusDao.KeySubkeyStatus; -import org.sufficientlysecure.keychain.ui.keyview.loader.SubkeyStatusDao.SubKeyItem; -import org.sufficientlysecure.keychain.ui.keyview.loader.ViewKeyLiveData.SubkeyStatusLiveData; -import org.sufficientlysecure.keychain.ui.keyview.view.KeyStatusList.KeyDisplayStatus; - - -public class KeyHealthPresenter implements Observer { - private static final Comparator SUBKEY_COMPARATOR = new Comparator() { - @Override - public int compare(SubKeyItem one, SubKeyItem two) { - // if one is valid and the other isn't, the valid one always comes first - if (one.isValid() ^ two.isValid()) { - return one.isValid() ? -1 : 1; - } - // compare usability, if one is "more usable" than the other, that one comes first - int usability = one.mSecretKeyType.compareUsability(two.mSecretKeyType); - if (usability != 0) { - return usability; - } - if ((one.mSecurityProblem == null) ^ (two.mSecurityProblem == null)) { - return one.mSecurityProblem == null ? -1 : 1; - } - // otherwise, the newer one comes first - return one.newerThan(two) ? -1 : 1; - } - }; - - private final Context context; - private final KeyHealthMvpView view; - private final long masterKeyId; - - private KeySubkeyStatus subkeyStatus; - private boolean showingExpandedInfo; - - - public KeyHealthPresenter(Context context, KeyHealthMvpView view, long masterKeyId) { - this.context = context; - this.view = view; - this.masterKeyId = masterKeyId; - - view.setOnHealthClickListener(new KeyHealthClickListener() { - @Override - public void onKeyHealthClick() { - KeyHealthPresenter.this.onKeyHealthClick(); - } - }); - } - - @Override - public void onChanged(@Nullable KeySubkeyStatus subkeyStatus) { - this.subkeyStatus = subkeyStatus; - if (subkeyStatus == null) { - return; - } - - KeyHealthStatus keyHealthStatus = determineKeyHealthStatus(subkeyStatus); - - boolean isInsecure = keyHealthStatus == KeyHealthStatus.INSECURE; - boolean isExpired = keyHealthStatus == KeyHealthStatus.EXPIRED; - if (isInsecure) { - boolean primaryKeySecurityProblem = subkeyStatus.keyCertify.mSecurityProblem != null; - if (primaryKeySecurityProblem) { - view.setKeyStatus(keyHealthStatus); - view.setPrimarySecurityProblem(subkeyStatus.keyCertify.mSecurityProblem); - view.setShowExpander(false); - } else { - view.setKeyStatus(keyHealthStatus); - view.setShowExpander(false); - displayExpandedInfo(false); - } - } else if (isExpired) { - view.setKeyStatus(keyHealthStatus); - view.setPrimaryExpiryDate(subkeyStatus.keyCertify.mExpiry); - view.setShowExpander(false); - view.hideExpandedInfo(); - } else { - view.setKeyStatus(keyHealthStatus); - view.setShowExpander(keyHealthStatus != KeyHealthStatus.REVOKED); - view.hideExpandedInfo(); - } - } - - private KeyHealthStatus determineKeyHealthStatus(KeySubkeyStatus subkeyStatus) { - SubKeyItem keyCertify = subkeyStatus.keyCertify; - if (keyCertify.mIsRevoked) { - return KeyHealthStatus.REVOKED; - } - - if (keyCertify.mIsExpired) { - return KeyHealthStatus.EXPIRED; - } - - if (keyCertify.mSecurityProblem != null) { - return KeyHealthStatus.INSECURE; - } - - if (!subkeyStatus.keysSign.isEmpty() && subkeyStatus.keysEncrypt.isEmpty()) { - SubKeyItem keySign = subkeyStatus.keysSign.get(0); - if (!keySign.isValid()) { - return KeyHealthStatus.BROKEN; - } - - if (keySign.mSecurityProblem != null) { - return KeyHealthStatus.INSECURE; - } - - return KeyHealthStatus.SIGN_ONLY; - } - - if (subkeyStatus.keysSign.isEmpty() || subkeyStatus.keysEncrypt.isEmpty()) { - return KeyHealthStatus.BROKEN; - } - - SubKeyItem keySign = subkeyStatus.keysSign.get(0); - SubKeyItem keyEncrypt = subkeyStatus.keysEncrypt.get(0); - - if (keySign.mSecurityProblem != null && keySign.isValid() - || keyEncrypt.mSecurityProblem != null && keyEncrypt.isValid()) { - return KeyHealthStatus.INSECURE; - } - - if (!keySign.isValid() || !keyEncrypt.isValid()) { - return KeyHealthStatus.BROKEN; - } - - if (keyCertify.mSecretKeyType == SecretKeyType.GNU_DUMMY - && keySign.mSecretKeyType == SecretKeyType.GNU_DUMMY - && keyEncrypt.mSecretKeyType == SecretKeyType.GNU_DUMMY) { - return KeyHealthStatus.STRIPPED; - } - - if (keyCertify.mSecretKeyType == SecretKeyType.DIVERT_TO_CARD - && keySign.mSecretKeyType == SecretKeyType.DIVERT_TO_CARD - && keyEncrypt.mSecretKeyType == SecretKeyType.DIVERT_TO_CARD) { - return KeyHealthStatus.DIVERT; - } - - boolean containsDivertKeys = keyCertify.mSecretKeyType == SecretKeyType.DIVERT_TO_CARD || - keySign.mSecretKeyType == SecretKeyType.DIVERT_TO_CARD || - keyEncrypt.mSecretKeyType == SecretKeyType.DIVERT_TO_CARD; - if (containsDivertKeys) { - return KeyHealthStatus.DIVERT_PARTIAL; - } - - boolean containsStrippedKeys = keyCertify.mSecretKeyType == SecretKeyType.GNU_DUMMY - || keySign.mSecretKeyType == SecretKeyType.GNU_DUMMY - || keyEncrypt.mSecretKeyType == SecretKeyType.GNU_DUMMY; - if (containsStrippedKeys) { - return KeyHealthStatus.PARTIAL_STRIPPED; - } - - return KeyHealthStatus.OK; - } - - private void onKeyHealthClick() { - if (showingExpandedInfo) { - showingExpandedInfo = false; - view.hideExpandedInfo(); - } else { - showingExpandedInfo = true; - displayExpandedInfo(true); - } - } - - private void displayExpandedInfo(boolean displayAll) { - SubKeyItem keyCertify = subkeyStatus.keyCertify; - SubKeyItem keySign = subkeyStatus.keysSign.isEmpty() ? null : subkeyStatus.keysSign.get(0); - SubKeyItem keyEncrypt = subkeyStatus.keysEncrypt.isEmpty() ? null : subkeyStatus.keysEncrypt.get(0); - - KeyDisplayStatus certDisplayStatus = getKeyDisplayStatus(keyCertify); - KeyDisplayStatus signDisplayStatus = getKeyDisplayStatus(keySign); - KeyDisplayStatus encryptDisplayStatus = getKeyDisplayStatus(keyEncrypt); - - if (!displayAll) { - if (certDisplayStatus == KeyDisplayStatus.OK) { - certDisplayStatus = null; - } - if (certDisplayStatus == KeyDisplayStatus.INSECURE) { - signDisplayStatus = null; - encryptDisplayStatus = null; - } - if (signDisplayStatus == KeyDisplayStatus.OK) { - signDisplayStatus = null; - } - if (encryptDisplayStatus == KeyDisplayStatus.OK) { - encryptDisplayStatus = null; - } - } - - view.showExpandedState(certDisplayStatus, signDisplayStatus, encryptDisplayStatus); - } - - private KeyDisplayStatus getKeyDisplayStatus(SubKeyItem subKeyItem) { - if (subKeyItem == null) { - return KeyDisplayStatus.UNAVAILABLE; - } - - if (subKeyItem.mIsRevoked) { - return KeyDisplayStatus.REVOKED; - } - if (subKeyItem.mIsExpired) { - return KeyDisplayStatus.EXPIRED; - } - if (subKeyItem.mSecurityProblem != null) { - return KeyDisplayStatus.INSECURE; - } - if (subKeyItem.mSecretKeyType == SecretKeyType.GNU_DUMMY) { - return KeyDisplayStatus.STRIPPED; - } - if (subKeyItem.mSecretKeyType == SecretKeyType.DIVERT_TO_CARD) { - return KeyDisplayStatus.DIVERT; - } - - return KeyDisplayStatus.OK; - } - - public LiveData getLiveDataInstance() { - return new SubkeyStatusLiveData(context, masterKeyId, SUBKEY_COMPARATOR); - } - - public enum KeyHealthStatus { - OK, DIVERT, DIVERT_PARTIAL, REVOKED, EXPIRED, INSECURE, SIGN_ONLY, STRIPPED, PARTIAL_STRIPPED, BROKEN - } - - public interface KeyHealthMvpView { - void setKeyStatus(KeyHealthStatus keyHealthStatus); - void setPrimarySecurityProblem(KeySecurityProblem securityProblem); - void setPrimaryExpiryDate(Date expiry); - - void setShowExpander(boolean showExpander); - void showExpandedState(KeyDisplayStatus certifyStatus, KeyDisplayStatus signStatus, - KeyDisplayStatus encryptStatus); - void hideExpandedInfo(); - - void setOnHealthClickListener(KeyHealthClickListener keyHealthClickListener); - - } - - public interface KeyStatusMvpView { - void setCertifyStatus(KeyDisplayStatus unavailable); - void setSignStatus(KeyDisplayStatus signStatus); - void setDecryptStatus(KeyDisplayStatus encryptStatus); - } - - public interface KeyHealthClickListener { - void onKeyHealthClick(); - } -} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/presenter/KeyserverStatusPresenter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/presenter/KeyserverStatusPresenter.java deleted file mode 100644 index 76e63f15e..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/presenter/KeyserverStatusPresenter.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright (C) 2017 Schürmann & Breitmoser GbR - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sufficientlysecure.keychain.ui.keyview.presenter; - - -import java.util.Date; - -import android.arch.lifecycle.LiveData; -import android.arch.lifecycle.Observer; -import android.content.Context; -import android.support.annotation.Nullable; - -import org.sufficientlysecure.keychain.ui.keyview.loader.KeyserverStatusDao.KeyserverStatus; -import org.sufficientlysecure.keychain.ui.keyview.loader.ViewKeyLiveData.KeyserverStatusLiveData; - - -public class KeyserverStatusPresenter implements Observer { - private final Context context; - private final KeyserverStatusMvpView view; - - private final long masterKeyId; - private final boolean isSecret; - - - public KeyserverStatusPresenter(Context context, KeyserverStatusMvpView view, long masterKeyId, - boolean isSecret) { - this.context = context; - this.view = view; - - this.masterKeyId = masterKeyId; - this.isSecret = isSecret; - } - - public LiveData getLiveDataInstance() { - return new KeyserverStatusLiveData(context, masterKeyId); - } - - @Override - public void onChanged(@Nullable KeyserverStatus keyserverStatus) { - if (keyserverStatus == null) { - view.setDisplayStatusUnknown(); - return; - } - - if (keyserverStatus.hasBeenUpdated()) { - if (keyserverStatus.isPublished()) { - view.setDisplayStatusPublished(); - } else { - view.setDisplayStatusNotPublished(); - } - view.setLastUpdated(keyserverStatus.getLastUpdated()); - } else { - view.setDisplayStatusUnknown(); - } - } - - public interface KeyserverStatusMvpView { - void setDisplayStatusPublished(); - void setDisplayStatusNotPublished(); - void setLastUpdated(Date lastUpdated); - void setDisplayStatusUnknown(); - } -} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/presenter/SystemContactPresenter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/presenter/SystemContactPresenter.java deleted file mode 100644 index 4c4dfbd43..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/presenter/SystemContactPresenter.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright (C) 2017 Schürmann & Breitmoser GbR - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sufficientlysecure.keychain.ui.keyview.presenter; - - -import android.arch.lifecycle.LiveData; -import android.arch.lifecycle.Observer; -import android.content.Context; -import android.content.Intent; -import android.graphics.Bitmap; -import android.net.Uri; -import android.provider.ContactsContract; -import android.support.annotation.Nullable; - -import org.sufficientlysecure.keychain.ui.keyview.loader.SystemContactDao.SystemContactInfo; -import org.sufficientlysecure.keychain.ui.keyview.loader.ViewKeyLiveData.SystemContactInfoLiveData; - - -public class SystemContactPresenter implements Observer { - private final Context context; - private final SystemContactMvpView view; - - private final long masterKeyId; - private final boolean isSecret; - - private long contactId; - - - public SystemContactPresenter(Context context, SystemContactMvpView view, long masterKeyId, boolean isSecret) { - this.context = context; - this.view = view; - - this.masterKeyId = masterKeyId; - this.isSecret = isSecret; - - view.setSystemContactClickListener(SystemContactPresenter.this::onSystemContactClick); - } - - public LiveData getLiveDataInstance() { - return new SystemContactInfoLiveData(context, masterKeyId, isSecret); - } - - @Override - public void onChanged(@Nullable SystemContactInfo systemContactInfo) { - if (systemContactInfo == null) { - view.hideLinkedSystemContact(); - return; - } - - this.contactId = systemContactInfo.contactId; - view.showLinkedSystemContact(systemContactInfo.contactName, systemContactInfo.contactPicture); - } - - private void onSystemContactClick() { - launchAndroidContactActivity(contactId, context); - } - - public interface SystemContactMvpView { - void setSystemContactClickListener(SystemContactClickListener systemContactClickListener); - - void showLinkedSystemContact(String contactName, Bitmap picture); - void hideLinkedSystemContact(); - } - - public interface SystemContactClickListener { - void onSystemContactClick(); - } - - private static void launchAndroidContactActivity(long contactId, Context context) { - Intent intent = new Intent(Intent.ACTION_VIEW); - Uri uri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_URI, String.valueOf(contactId)); - intent.setData(uri); - context.startActivity(intent); - } -} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/presenter/ViewKeyMvpView.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/presenter/ViewKeyMvpView.java index b77e4bbc8..5898ae907 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/presenter/ViewKeyMvpView.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/presenter/ViewKeyMvpView.java @@ -30,7 +30,6 @@ public interface ViewKeyMvpView { void startActivity(Intent intent); void startActivityAndShowResultSnackbar(Intent intent); void showDialogFragment(DialogFragment dialogFragment, final String tag); - void setContentShown(boolean show, boolean animate); void showContextMenu(int position, View anchor); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/view/IdentitiesCardView.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/view/IdentitiesCardView.java index 26d8b5801..98b7ccdba 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/view/IdentitiesCardView.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/view/IdentitiesCardView.java @@ -29,15 +29,12 @@ import android.widget.Button; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.ui.adapter.IdentityAdapter; -import org.sufficientlysecure.keychain.ui.keyview.presenter.IdentitiesPresenter.IdentitiesCardListener; -import org.sufficientlysecure.keychain.ui.keyview.presenter.IdentitiesPresenter.IdentitiesMvpView; import org.sufficientlysecure.keychain.ui.util.recyclerview.DividerItemDecoration; -public class IdentitiesCardView extends CardView implements IdentitiesMvpView { +public class IdentitiesCardView extends CardView { private final RecyclerView vIdentities; - private IdentitiesCardListener identitiesCardListener; private final Button linkedIdsAddButton; public IdentitiesCardView(Context context, AttributeSet attrs) { @@ -50,24 +47,16 @@ public class IdentitiesCardView extends CardView implements IdentitiesMvpView { vIdentities.addItemDecoration(new DividerItemDecoration(context, DividerItemDecoration.VERTICAL_LIST, false)); linkedIdsAddButton = view.findViewById(R.id.view_key_card_linked_ids_add); - linkedIdsAddButton.setOnClickListener(v -> { - if (identitiesCardListener != null) { - identitiesCardListener.onClickAddIdentity(); - } - }); } - @Override public void setIdentitiesAdapter(IdentityAdapter identityAdapter) { vIdentities.setAdapter(identityAdapter); } - @Override - public void setIdentitiesCardListener(IdentitiesCardListener identitiesCardListener) { - this.identitiesCardListener = identitiesCardListener; + public void setIdentitiesCardListener(OnClickListener identitiesCardListener) { + linkedIdsAddButton.setOnClickListener(identitiesCardListener); } - @Override public void setAddLinkedIdButtonVisible(boolean show) { linkedIdsAddButton.setVisibility(show ? View.VISIBLE : View.GONE); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/view/KeyHealthView.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/view/KeyHealthView.java index 191c6c6da..9b94195b3 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/view/KeyHealthView.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/view/KeyHealthView.java @@ -39,14 +39,12 @@ import org.sufficientlysecure.keychain.pgp.SecurityProblem.InsecureBitStrength; import org.sufficientlysecure.keychain.pgp.SecurityProblem.KeySecurityProblem; import org.sufficientlysecure.keychain.pgp.SecurityProblem.NotWhitelistedCurve; import org.sufficientlysecure.keychain.pgp.SecurityProblem.UnidentifiedKeyProblem; -import org.sufficientlysecure.keychain.ui.keyview.presenter.KeyHealthPresenter.KeyHealthClickListener; -import org.sufficientlysecure.keychain.ui.keyview.presenter.KeyHealthPresenter.KeyHealthMvpView; -import org.sufficientlysecure.keychain.ui.keyview.presenter.KeyHealthPresenter.KeyHealthStatus; +import org.sufficientlysecure.keychain.ui.keyview.loader.SubkeyStatusDao.KeyHealthStatus; import org.sufficientlysecure.keychain.ui.keyview.view.KeyStatusList.KeyDisplayStatus; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; -public class KeyHealthView extends LinearLayout implements KeyHealthMvpView, OnClickListener { +public class KeyHealthView extends LinearLayout implements OnClickListener { private final View vLayout; private final TextView vTitle, vSubtitle; private final ImageView vIcon; @@ -59,7 +57,7 @@ public class KeyHealthView extends LinearLayout implements KeyHealthMvpView, OnC private final View vExpiryLayout; private final TextView vExpiryText; - private KeyHealthClickListener keyHealthClickListener; + private OnClickListener keyHealthClickListener; public KeyHealthView(Context context, AttributeSet attrs) { super(context, attrs); @@ -125,7 +123,6 @@ public class KeyHealthView extends LinearLayout implements KeyHealthMvpView, OnC } } - @Override public void setKeyStatus(KeyHealthStatus keyHealthStatus) { switch (keyHealthStatus) { case OK: @@ -161,7 +158,6 @@ public class KeyHealthView extends LinearLayout implements KeyHealthMvpView, OnC } } - @Override public void setPrimarySecurityProblem(KeySecurityProblem securityProblem) { if (securityProblem == null) { vInsecureLayout.setVisibility(View.GONE); @@ -190,7 +186,6 @@ public class KeyHealthView extends LinearLayout implements KeyHealthMvpView, OnC } - @Override public void setPrimaryExpiryDate(Date expiry) { if (expiry == null) { vExpiryLayout.setVisibility(View.GONE); @@ -205,23 +200,20 @@ public class KeyHealthView extends LinearLayout implements KeyHealthMvpView, OnC @Override public void onClick(View view) { if (keyHealthClickListener != null) { - keyHealthClickListener.onKeyHealthClick(); + keyHealthClickListener.onClick(view); } } - @Override - public void setOnHealthClickListener(KeyHealthClickListener keyHealthClickListener) { + public void setOnHealthClickListener(OnClickListener keyHealthClickListener) { this.keyHealthClickListener = keyHealthClickListener; vLayout.setClickable(keyHealthClickListener != null); } - @Override public void setShowExpander(boolean showExpander) { vLayout.setClickable(showExpander); vExpander.setVisibility(showExpander ? View.VISIBLE : View.GONE); } - @Override public void showExpandedState(KeyDisplayStatus certifyStatus, KeyDisplayStatus signStatus, KeyDisplayStatus encryptStatus) { if (certifyStatus == null && signStatus == null && encryptStatus == null) { @@ -240,7 +232,6 @@ public class KeyHealthView extends LinearLayout implements KeyHealthMvpView, OnC } - @Override public void hideExpandedInfo() { showExpandedState(null, null, null); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/view/KeyStatusList.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/view/KeyStatusList.java index e1a0c3e6a..b4bb2669d 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/view/KeyStatusList.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/view/KeyStatusList.java @@ -30,10 +30,9 @@ import android.widget.LinearLayout; import android.widget.TextView; import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.ui.keyview.presenter.KeyHealthPresenter.KeyStatusMvpView; -public class KeyStatusList extends LinearLayout implements KeyStatusMvpView { +public class KeyStatusList extends LinearLayout { private final TextView vCertText, vSignText, vDecryptText; private final ImageView vCertIcon, vSignIcon, vDecryptIcon; private final View vCertToken, vSignToken, vDecryptToken; @@ -107,7 +106,6 @@ public class KeyStatusList extends LinearLayout implements KeyStatusMvpView { } - @Override public void setCertifyStatus(KeyDisplayStatus keyDisplayStatus) { if (keyDisplayStatus == null) { vCertifyLayout.setVisibility(View.GONE); @@ -121,7 +119,6 @@ public class KeyStatusList extends LinearLayout implements KeyStatusMvpView { vCertifyLayout.setVisibility(View.VISIBLE); } - @Override public void setSignStatus(KeyDisplayStatus keyDisplayStatus) { if (keyDisplayStatus == null) { vSignLayout.setVisibility(View.GONE); @@ -134,7 +131,6 @@ public class KeyStatusList extends LinearLayout implements KeyStatusMvpView { vSignLayout.setVisibility(View.VISIBLE); } - @Override public void setDecryptStatus(KeyDisplayStatus keyDisplayStatus) { if (keyDisplayStatus == null) { vDecryptLayout.setVisibility(View.GONE); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/view/KeyserverStatusView.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/view/KeyserverStatusView.java index 641153bc1..1b353e729 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/view/KeyserverStatusView.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/view/KeyserverStatusView.java @@ -34,10 +34,9 @@ import android.widget.ImageView; import android.widget.TextView; import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.ui.keyview.presenter.KeyserverStatusPresenter.KeyserverStatusMvpView; -public class KeyserverStatusView extends FrameLayout implements KeyserverStatusMvpView { +public class KeyserverStatusView extends FrameLayout { private final View vLayout; private final TextView vTitle; private final TextView vSubtitle; @@ -75,23 +74,19 @@ public class KeyserverStatusView extends FrameLayout implements KeyserverStatusM } } - @Override public void setDisplayStatusPublished() { setDisplayStatus(KeyserverDisplayStatus.PUBLISHED); } - @Override public void setDisplayStatusNotPublished() { setDisplayStatus(KeyserverDisplayStatus.NOT_PUBLISHED); } - @Override public void setDisplayStatusUnknown() { setDisplayStatus(KeyserverDisplayStatus.UNKNOWN); vSubtitle.setText(R.string.keyserver_last_updated_never); } - @Override public void setLastUpdated(Date lastUpdated) { String lastUpdatedText = DateFormat.getMediumDateFormat(getContext()).format(lastUpdated); vSubtitle.setText(getResources().getString(R.string.keyserver_last_updated, lastUpdatedText)); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/view/SystemContactCardView.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/view/SystemContactCardView.java index 61fb46ed8..2cfd01440 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/view/SystemContactCardView.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/view/SystemContactCardView.java @@ -24,23 +24,18 @@ import android.support.v7.widget.CardView; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; -import android.view.View.OnClickListener; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.ui.keyview.presenter.SystemContactPresenter.SystemContactClickListener; -import org.sufficientlysecure.keychain.ui.keyview.presenter.SystemContactPresenter.SystemContactMvpView; -public class SystemContactCardView extends CardView implements SystemContactMvpView, OnClickListener { +public class SystemContactCardView extends CardView { private LinearLayout vSystemContactLayout; private ImageView vSystemContactPicture; private TextView vSystemContactName; - private SystemContactClickListener systemContactClickListener; - public SystemContactCardView(Context context, AttributeSet attrs) { super(context, attrs); @@ -49,28 +44,16 @@ public class SystemContactCardView extends CardView implements SystemContactMvpV vSystemContactLayout = view.findViewById(R.id.system_contact_layout); vSystemContactName = view.findViewById(R.id.system_contact_name); vSystemContactPicture = view.findViewById(R.id.system_contact_picture); - - vSystemContactLayout.setOnClickListener(this); } - @Override - public void onClick(View view) { - if (systemContactClickListener != null) { - systemContactClickListener.onSystemContactClick(); - } - } - - @Override - public void setSystemContactClickListener(SystemContactClickListener systemContactClickListener) { - this.systemContactClickListener = systemContactClickListener; - vSystemContactLayout.setClickable(systemContactClickListener != null); + public void setSystemContactClickListener(OnClickListener onClickListener) { + vSystemContactLayout.setOnClickListener(onClickListener); } public void hideLinkedSystemContact() { setVisibility(View.GONE); } - @Override public void showLinkedSystemContact(String contactName, Bitmap picture) { vSystemContactName.setText(contactName); if (picture != null) { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateFinalFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateFinalFragment.java index ee24386cb..55552ae03 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateFinalFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateFinalFragment.java @@ -17,6 +17,8 @@ package org.sufficientlysecure.keychain.ui.linked; + +import android.arch.lifecycle.ViewModelProviders; import android.graphics.PorterDuff; import android.os.AsyncTask; import android.os.Bundle; @@ -25,7 +27,6 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.view.LayoutInflater; import android.view.View; -import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; @@ -34,6 +35,7 @@ import android.widget.ViewAnimator; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.linked.LinkedAttribute; import org.sufficientlysecure.keychain.linked.LinkedTokenResource; +import org.sufficientlysecure.keychain.model.SubKey.UnifiedKeyInfo; import org.sufficientlysecure.keychain.operations.results.LinkedVerifyResult; import org.sufficientlysecure.keychain.operations.results.OperationResult; import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; @@ -41,68 +43,57 @@ import org.sufficientlysecure.keychain.pgp.WrappedUserAttribute; import org.sufficientlysecure.keychain.service.SaveKeyringParcel; import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import org.sufficientlysecure.keychain.ui.base.CryptoOperationFragment; +import org.sufficientlysecure.keychain.ui.keyview.UnifiedKeyInfoViewModel; import org.sufficientlysecure.keychain.ui.util.Notify; public abstract class LinkedIdCreateFinalFragment extends CryptoOperationFragment { - - protected LinkedIdWizard mLinkedIdWizard; - private ImageView mVerifyImage; private TextView mVerifyStatus; private ViewAnimator mVerifyAnimator; + private long masterKeyId; + byte[] fingerprint; + // This is a resource, set AFTER it has been verified LinkedTokenResource mVerifiedResource = null; private ViewAnimator mVerifyButtonAnimator; - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - mLinkedIdWizard = (LinkedIdWizard) getActivity(); - } - protected abstract View newView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState); - @Override @NonNull - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + UnifiedKeyInfoViewModel viewModel = ViewModelProviders.of(requireActivity()).get(UnifiedKeyInfoViewModel.class); + viewModel.getUnifiedKeyInfoLiveData(requireContext()).observe(this, this::onLoadUnifiedKeyInfo); + } + + private void onLoadUnifiedKeyInfo(UnifiedKeyInfo unifiedKeyInfo) { + this.masterKeyId = unifiedKeyInfo.master_key_id(); + this.fingerprint = unifiedKeyInfo.fingerprint(); + } + + @NonNull + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { final View view = newView(inflater, container, savedInstanceState); View nextButton = view.findViewById(R.id.next_button); if (nextButton != null) { - nextButton.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - cryptoOperation(); - } - }); + nextButton.setOnClickListener(v -> cryptoOperation()); } - view.findViewById(R.id.back_button).setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - mLinkedIdWizard.loadFragment(null, null, LinkedIdWizard.FRAG_ACTION_TO_LEFT); - } - }); + view.findViewById(R.id.back_button).setOnClickListener( + v -> ((LinkedIdWizard) requireActivity()).loadFragment(null, LinkedIdWizard.FRAG_ACTION_TO_LEFT)); mVerifyAnimator = view.findViewById(R.id.verify_progress); mVerifyImage = view.findViewById(R.id.verify_image); mVerifyStatus = view.findViewById(R.id.verify_status); mVerifyButtonAnimator = view.findViewById(R.id.verify_buttons); - view.findViewById(R.id.button_verify).setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - proofVerify(); - } - }); + view.findViewById(R.id.button_verify).setOnClickListener(v -> proofVerify()); - view.findViewById(R.id.button_retry).setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - proofVerify(); - } - }); + view.findViewById(R.id.button_retry).setOnClickListener(v -> proofVerify()); setVerifyProgress(false, null); mVerifyStatus.setText(R.string.linked_verify_pending); @@ -154,7 +145,7 @@ public abstract class LinkedIdCreateFinalFragment extends CryptoOperationFragmen return new LinkedVerifyResult(LinkedVerifyResult.RESULT_ERROR, log); } - LinkedVerifyResult result = resource.verify(getActivity(), mLinkedIdWizard.mFingerprint); + LinkedVerifyResult result = resource.verify(getActivity(), fingerprint); // ux flow: this operation should take at last a second timer = System.currentTimeMillis() -timer; @@ -211,7 +202,7 @@ public abstract class LinkedIdCreateFinalFragment extends CryptoOperationFragmen @Override public Parcelable createOperationInput() { SaveKeyringParcel.Builder builder= - SaveKeyringParcel.buildChangeKeyringParcel(mLinkedIdWizard.mMasterKeyId, mLinkedIdWizard.mFingerprint); + SaveKeyringParcel.buildChangeKeyringParcel(masterKeyId, fingerprint); WrappedUserAttribute ua = LinkedAttribute.fromResource(mVerifiedResource).toUserAttribute(); builder.addUserAttribute(ua); return builder.build(); @@ -219,7 +210,7 @@ public abstract class LinkedIdCreateFinalFragment extends CryptoOperationFragmen @Override public void onCryptoOperationSuccess(OperationResult result) { - getActivity().finish(); + requireActivity().finish(); } @Override diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateGithubFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateGithubFragment.java index 624a51055..81594bcb4 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateGithubFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateGithubFragment.java @@ -17,6 +17,7 @@ package org.sufficientlysecure.keychain.ui.linked; + import java.io.BufferedInputStream; import java.io.BufferedReader; import java.io.BufferedWriter; @@ -32,13 +33,11 @@ import java.util.Random; import android.app.Activity; import android.app.Dialog; +import android.arch.lifecycle.ViewModelProviders; import android.content.Context; -import android.content.DialogInterface; -import android.content.DialogInterface.OnDismissListener; import android.content.Intent; import android.net.Uri; import android.os.AsyncTask; -import android.os.Build; import android.os.Build.VERSION; import android.os.Build.VERSION_CODES; import android.os.Bundle; @@ -46,11 +45,9 @@ import android.os.Handler; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.app.ActivityOptionsCompat; -import android.support.v4.app.FragmentActivity; import android.util.Base64; import android.view.LayoutInflater; import android.view.View; -import android.view.View.OnClickListener; import android.view.ViewGroup; import android.webkit.CookieManager; import android.webkit.WebView; @@ -61,19 +58,20 @@ import android.widget.TextView; import android.widget.ViewAnimator; import javax.net.ssl.HttpsURLConnection; +import org.bouncycastle.util.encoders.Hex; import org.json.JSONException; import org.json.JSONObject; -import org.bouncycastle.util.encoders.Hex; import org.sufficientlysecure.keychain.BuildConfig; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.linked.LinkedAttribute; import org.sufficientlysecure.keychain.linked.resources.GithubResource; +import org.sufficientlysecure.keychain.model.SubKey.UnifiedKeyInfo; import org.sufficientlysecure.keychain.operations.results.EditKeyResult; import org.sufficientlysecure.keychain.pgp.WrappedUserAttribute; -import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; import org.sufficientlysecure.keychain.service.SaveKeyringParcel; -import org.sufficientlysecure.keychain.ui.keyview.ViewKeyActivity; import org.sufficientlysecure.keychain.ui.base.CryptoOperationFragment; +import org.sufficientlysecure.keychain.ui.keyview.UnifiedKeyInfoViewModel; +import org.sufficientlysecure.keychain.ui.keyview.ViewKeyActivity; import org.sufficientlysecure.keychain.ui.util.Notify; import org.sufficientlysecure.keychain.ui.util.Notify.Style; import org.sufficientlysecure.keychain.ui.widget.StatusIndicator; @@ -109,7 +107,7 @@ public class LinkedIdCreateGithubFragment extends CryptoOperationFragment { + LinkedIdWizard activity = (LinkedIdWizard) requireActivity(); + activity.loadFragment(null, LinkedIdWizard.FRAG_ACTION_TO_LEFT); }); - view.findViewById(R.id.button_send).setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - step1GetOAuthCode(); - // for animation testing - // onCryptoOperationSuccess(null); - } + view.findViewById(R.id.button_send).setOnClickListener(v -> { + step1GetOAuthCode(); + // for animation testing + // onCryptoOperationSuccess(null); }); return view; @@ -152,34 +141,29 @@ public class LinkedIdCreateGithubFragment extends CryptoOperationFragment oAuthRequest("github.com/login/oauth/authorize", BuildConfig.GITHUB_CLIENT_ID, "gist"), 300); } private void showRetryForOAuth() { - - mRetryButton.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - v.setOnClickListener(null); - step1GetOAuthCode(); - } + mRetryButton.setOnClickListener(v -> { + v.setOnClickListener(null); + step1GetOAuthCode(); }); mButtonContainer.setDisplayedChild(3); @@ -402,14 +386,11 @@ public class LinkedIdCreateGithubFragment extends CryptoOperationFragment { + WrappedUserAttribute ua = LinkedAttribute.fromResource(resource).toUserAttribute(); + mSkpBuilder = SaveKeyringParcel.buildChangeKeyringParcel(mMasterKeyId, mFingerprint); + mSkpBuilder.addUserAttribute(ua); + cryptoOperation(); }, 250); } @@ -429,32 +410,28 @@ public class LinkedIdCreateGithubFragment extends CryptoOperationFragment { + Activity activity = requireActivity(); + Intent intent = ViewKeyActivity.getViewKeyActivityIntent(requireActivity(), mMasterKeyId); + // intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - intent.putExtra(ViewKeyActivity.EXTRA_LINKED_TRANSITION, true); - View linkedItem = mButtonContainer.getChildAt(2); + if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { + intent.putExtra(ViewKeyActivity.EXTRA_LINKED_TRANSITION, true); + View linkedItem = mButtonContainer.getChildAt(2); - Bundle options = ActivityOptionsCompat.makeSceneTransitionAnimation( - activity, linkedItem, linkedItem.getTransitionName()).toBundle(); - activity.startActivity(intent, options); - mFinishOnStop = true; - } else { - activity.startActivity(intent); - activity.finish(); - } + Bundle options = ActivityOptionsCompat.makeSceneTransitionAnimation( + activity, linkedItem, linkedItem.getTransitionName()).toBundle(); + activity.startActivity(intent, options); + mFinishOnStop = true; + } else { + activity.startActivity(intent); + activity.finish(); } }, 1000); } @Override - public void onSaveInstanceState(Bundle outState) { + public void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); // cookies are automatically saved, we don't want that @@ -464,7 +441,7 @@ public class LinkedIdCreateGithubFragment extends CryptoOperationFragment { + v.setOnClickListener(null); + mButtonContainer.setDisplayedChild(1); + setState(State.LID_PROCESS); + cryptoOperation(); }); mButtonContainer.setDisplayedChild(3); setState(State.LID_ERROR); @@ -574,12 +548,7 @@ public class LinkedIdCreateGithubFragment extends CryptoOperationFragment step1GetOAuthToken()); auth_dialog.show(); web.loadUrl("https://" + hostAndPath + diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateHttpsStep1Fragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateHttpsStep1Fragment.java index 8a1fd1cc1..4ec67e948 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateHttpsStep1Fragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateHttpsStep1Fragment.java @@ -17,24 +17,22 @@ package org.sufficientlysecure.keychain.ui.linked; + import android.os.Bundle; +import android.support.annotation.NonNull; import android.support.v4.app.Fragment; import android.text.Editable; import android.text.TextWatcher; import android.util.Patterns; import android.view.LayoutInflater; import android.view.View; -import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.EditText; import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.linked.resources.GenericHttpsResource; + public class LinkedIdCreateHttpsStep1Fragment extends Fragment { - - LinkedIdWizard mLinkedIdWizard; - EditText mEditUri; public static LinkedIdCreateHttpsStep1Fragment newInstance() { @@ -47,44 +45,23 @@ public class LinkedIdCreateHttpsStep1Fragment extends Fragment { } @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - - mLinkedIdWizard = (LinkedIdWizard) getActivity(); - - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { final View view = inflater.inflate(R.layout.linked_create_https_fragment_step1, container, false); - view.findViewById(R.id.next_button).setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - - String uri = "https://" + mEditUri.getText(); - - if (!checkUri(uri)) { - return; - } - - String proofText = GenericHttpsResource.generateText(getActivity(), - mLinkedIdWizard.mFingerprint); - - LinkedIdCreateHttpsStep2Fragment frag = - LinkedIdCreateHttpsStep2Fragment.newInstance(uri, proofText); - - mLinkedIdWizard.loadFragment(null, frag, LinkedIdWizard.FRAG_ACTION_TO_RIGHT); + view.findViewById(R.id.next_button).setOnClickListener(v -> { + String uri = "https://" + mEditUri.getText(); + if (!checkUri(uri)) { + return; } + + LinkedIdCreateHttpsStep2Fragment frag = LinkedIdCreateHttpsStep2Fragment.newInstance(uri); + + ((LinkedIdWizard) requireActivity()).loadFragment(frag, LinkedIdWizard.FRAG_ACTION_TO_RIGHT); }); - view.findViewById(R.id.back_button).setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - mLinkedIdWizard.loadFragment(null, null, LinkedIdWizard.FRAG_ACTION_TO_LEFT); - } - }); + view.findViewById(R.id.back_button).setOnClickListener( + v -> ((LinkedIdWizard) requireActivity()).loadFragment(null, LinkedIdWizard.FRAG_ACTION_TO_LEFT)); mEditUri = view.findViewById(R.id.linked_create_https_uri); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateHttpsStep2Fragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateHttpsStep2Fragment.java index 42e6ebd44..e8b350058 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateHttpsStep2Fragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateHttpsStep2Fragment.java @@ -17,47 +17,46 @@ package org.sufficientlysecure.keychain.ui.linked; -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; -import android.os.Environment; -import android.view.LayoutInflater; -import android.view.View; -import android.view.View.OnClickListener; -import android.view.ViewGroup; -import android.widget.EditText; - -import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; -import org.sufficientlysecure.keychain.linked.resources.GenericHttpsResource; -import org.sufficientlysecure.keychain.ui.util.Notify; -import org.sufficientlysecure.keychain.ui.util.Notify.Style; -import org.sufficientlysecure.keychain.util.FileHelper; import java.io.FileNotFoundException; import java.io.PrintWriter; import java.net.URI; import java.net.URISyntaxException; -public class LinkedIdCreateHttpsStep2Fragment extends LinkedIdCreateFinalFragment { +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.os.Environment; +import android.support.annotation.NonNull; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.EditText; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.linked.resources.GenericHttpsResource; +import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; +import org.sufficientlysecure.keychain.ui.util.Notify; +import org.sufficientlysecure.keychain.ui.util.Notify.Style; +import org.sufficientlysecure.keychain.util.FileHelper; +import timber.log.Timber; + +public class LinkedIdCreateHttpsStep2Fragment extends LinkedIdCreateFinalFragment { private static final int REQUEST_CODE_OUTPUT = 0x00007007; - public static final String ARG_URI = "uri", ARG_TEXT = "text"; + public static final String ARG_URI = "uri"; EditText mEditUri; URI mResourceUri; String mResourceString; - public static LinkedIdCreateHttpsStep2Fragment newInstance - (String uri, String proofText) { + public static LinkedIdCreateHttpsStep2Fragment newInstance(String uri) { LinkedIdCreateHttpsStep2Fragment frag = new LinkedIdCreateHttpsStep2Fragment(); Bundle args = new Bundle(); args.putString(ARG_URI, uri); - args.putString(ARG_TEXT, proofText); frag.setArguments(args); return frag; @@ -75,47 +74,33 @@ public class LinkedIdCreateHttpsStep2Fragment extends LinkedIdCreateFinalFragmen try { mResourceUri = new URI(getArguments().getString(ARG_URI)); } catch (URISyntaxException e) { - e.printStackTrace(); - getActivity().finish(); + Timber.e(e); + requireActivity().finish(); } - mResourceString = getArguments().getString(ARG_TEXT); - - } - - protected View newView(LayoutInflater inflater, - ViewGroup container, Bundle savedInstanceState) { - return inflater.inflate(R.layout.linked_create_https_fragment_step2, container, false); + mResourceString = GenericHttpsResource.generateText(requireActivity(), fingerprint); } @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + protected View newView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + return inflater.inflate(R.layout.linked_create_https_fragment_step2, container, false); + } + + @NonNull + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = super.onCreateView(inflater, container, savedInstanceState); - if (view != null) { + view.findViewById(R.id.button_send).setOnClickListener(v -> proofSend()); + view.findViewById(R.id.button_save).setOnClickListener(v -> proofSave()); - view.findViewById(R.id.button_send).setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - proofSend(); - } - }); - - view.findViewById(R.id.button_save).setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - proofSave(); - } - }); - - mEditUri = view.findViewById(R.id.linked_create_https_uri); - mEditUri.setText(mResourceUri.toString()); - } + mEditUri = view.findViewById(R.id.linked_create_https_uri); + mEditUri.setText(mResourceUri.toString()); return view; } - private void proofSend () { + private void proofSend() { Intent sendIntent = new Intent(); sendIntent.setAction(Intent.ACTION_SEND); sendIntent.putExtra(Intent.EXTRA_TEXT, mResourceString); @@ -123,7 +108,7 @@ public class LinkedIdCreateHttpsStep2Fragment extends LinkedIdCreateFinalFragmen startActivity(sendIntent); } - private void proofSave () { + private void proofSave() { String state = Environment.getExternalStorageState(); if (!Environment.MEDIA_MOUNTED.equals(state)) { Notify.create(getActivity(), "External storage not available!", Style.ERROR).show(); @@ -138,8 +123,7 @@ public class LinkedIdCreateHttpsStep2Fragment extends LinkedIdCreateFinalFragmen private void saveFile(Uri uri) { try { - PrintWriter out = - new PrintWriter(getActivity().getContentResolver().openOutputStream(uri)); + PrintWriter out = new PrintWriter(requireActivity().getContentResolver().openOutputStream(uri)); out.print(mResourceString); if (out.checkError()) { Notify.create(getActivity(), "Error writing file!", Style.ERROR).show(); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateTwitterStep1Fragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateTwitterStep1Fragment.java index e93a2c7fd..41d3f2adb 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateTwitterStep1Fragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateTwitterStep1Fragment.java @@ -19,6 +19,7 @@ package org.sufficientlysecure.keychain.ui.linked; import android.os.AsyncTask; import android.os.Bundle; +import android.support.annotation.NonNull; import android.support.v4.app.Fragment; import android.view.LayoutInflater; import android.view.View; @@ -30,30 +31,14 @@ import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.ui.util.Notify; public class LinkedIdCreateTwitterStep1Fragment extends Fragment { - - LinkedIdWizard mLinkedIdWizard; - EditText mEditHandle; public static LinkedIdCreateTwitterStep1Fragment newInstance() { - LinkedIdCreateTwitterStep1Fragment frag = new LinkedIdCreateTwitterStep1Fragment(); - - Bundle args = new Bundle(); - frag.setArguments(args); - - return frag; + return new LinkedIdCreateTwitterStep1Fragment(); } @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - - mLinkedIdWizard = (LinkedIdWizard) getActivity(); - - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { final View view = inflater.inflate(R.layout.linked_create_twitter_fragment_step1, container, false); view.findViewById(R.id.next_button).setOnClickListener(new OnClickListener() { @@ -96,19 +81,15 @@ public class LinkedIdCreateTwitterStep1Fragment extends Fragment { LinkedIdCreateTwitterStep2Fragment frag = LinkedIdCreateTwitterStep2Fragment.newInstance(handle); - mLinkedIdWizard.loadFragment(null, frag, LinkedIdWizard.FRAG_ACTION_TO_RIGHT); + ((LinkedIdWizard) requireActivity()).loadFragment(frag, LinkedIdWizard.FRAG_ACTION_TO_RIGHT); } }.execute(); } }); - view.findViewById(R.id.back_button).setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - mLinkedIdWizard.loadFragment(null, null, LinkedIdWizard.FRAG_ACTION_TO_LEFT); - } - }); + view.findViewById(R.id.back_button).setOnClickListener( + v -> ((LinkedIdWizard) requireActivity()).loadFragment(null, LinkedIdWizard.FRAG_ACTION_TO_LEFT)); mEditHandle = view.findViewById(R.id.linked_create_twitter_handle); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateTwitterStep2Fragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateTwitterStep2Fragment.java index 9bd00c0db..7e5ca0051 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateTwitterStep2Fragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateTwitterStep2Fragment.java @@ -17,20 +17,23 @@ package org.sufficientlysecure.keychain.ui.linked; + import android.content.Intent; import android.net.Uri; import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.text.Html; +import android.text.Spanned; import android.view.LayoutInflater; import android.view.View; -import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.TextView; import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; import org.sufficientlysecure.keychain.linked.LinkedTokenResource; import org.sufficientlysecure.keychain.linked.resources.TwitterResource; +import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; public class LinkedIdCreateTwitterStep2Fragment extends LinkedIdCreateFinalFragment { @@ -39,9 +42,7 @@ public class LinkedIdCreateTwitterStep2Fragment extends LinkedIdCreateFinalFragm String mResourceHandle; String mResourceString; - public static LinkedIdCreateTwitterStep2Fragment newInstance - (String handle) { - + public static LinkedIdCreateTwitterStep2Fragment newInstance(String handle) { LinkedIdCreateTwitterStep2Fragment frag = new LinkedIdCreateTwitterStep2Fragment(); Bundle args = new Bundle(); @@ -52,39 +53,23 @@ public class LinkedIdCreateTwitterStep2Fragment extends LinkedIdCreateFinalFragm } @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - mResourceString = - TwitterResource.generate(mLinkedIdWizard.mFingerprint); + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + mResourceString = TwitterResource.generate(fingerprint); mResourceHandle = getArguments().getString(ARG_HANDLE); - } + @NonNull @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = super.onCreateView(inflater, container, savedInstanceState); - if (view != null) { - view.findViewById(R.id.button_send).setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - proofSend(); - } - }); + view.findViewById(R.id.button_send).setOnClickListener(v -> proofSend()); + view.findViewById(R.id.button_share).setOnClickListener(v -> proofShare()); - view.findViewById(R.id.button_share).setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - proofShare(); - } - }); - - ((TextView) view.findViewById(R.id.linked_tweet_published)).setText( - Html.fromHtml(getString(R.string.linked_create_twitter_2_3, mResourceHandle)) - ); - } + Spanned tweetText = Html.fromHtml(getString(R.string.linked_create_twitter_2_3, mResourceHandle)); + ((TextView) view.findViewById(R.id.linked_tweet_published)).setText(tweetText); return view; } @@ -109,13 +94,12 @@ public class LinkedIdCreateTwitterStep2Fragment extends LinkedIdCreateFinalFragm } private void proofSend() { - Uri.Builder builder = Uri.parse("https://twitter.com/intent/tweet").buildUpon(); builder.appendQueryParameter("text", mResourceString); Uri uri = builder.build(); Intent intent = new Intent(Intent.ACTION_VIEW, uri); - getActivity().startActivity(intent); + startActivity(intent); } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdSelectFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdSelectFragment.java index b7c00552e..cd8262aaf 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdSelectFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdSelectFragment.java @@ -17,7 +17,9 @@ package org.sufficientlysecure.keychain.ui.linked; + import android.os.Bundle; +import android.support.annotation.NonNull; import android.support.v4.app.Fragment; import android.view.LayoutInflater; import android.view.View; @@ -25,81 +27,33 @@ import android.view.ViewGroup; import org.sufficientlysecure.keychain.R; + public class LinkedIdSelectFragment extends Fragment { - - LinkedIdWizard mLinkedIdWizard; - - /** - * Creates new instance of this fragment - */ public static LinkedIdSelectFragment newInstance() { - LinkedIdSelectFragment frag = new LinkedIdSelectFragment(); - - Bundle args = new Bundle(); - frag.setArguments(args); - - return frag; + return new LinkedIdSelectFragment(); } @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.linked_select_fragment, container, false); - view.findViewById(R.id.linked_create_https_button) - .setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - LinkedIdCreateHttpsStep1Fragment frag = - LinkedIdCreateHttpsStep1Fragment.newInstance(); + view.findViewById(R.id.linked_create_https_button).setOnClickListener(v -> { + LinkedIdCreateHttpsStep1Fragment frag = LinkedIdCreateHttpsStep1Fragment.newInstance(); + ((LinkedIdWizard) requireActivity()).loadFragment(frag, LinkedIdWizard.FRAG_ACTION_TO_RIGHT); + }); - mLinkedIdWizard.loadFragment(null, frag, LinkedIdWizard.FRAG_ACTION_TO_RIGHT); - } - }); + view.findViewById(R.id.linked_create_twitter_button).setOnClickListener(v -> { + LinkedIdCreateTwitterStep1Fragment frag = LinkedIdCreateTwitterStep1Fragment.newInstance(); + ((LinkedIdWizard) requireActivity()).loadFragment(frag, LinkedIdWizard.FRAG_ACTION_TO_RIGHT); + }); - /* - view.findViewById(R.id.linked_create_dns_button) - .setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - LinkedIdCreateDnsStep1Fragment frag = - LinkedIdCreateDnsStep1Fragment.newInstance(); - - mLinkedIdWizard.loadFragment(null, frag, LinkedIdWizard.FRAG_ACTION_TO_RIGHT); - } - }); - */ - - view.findViewById(R.id.linked_create_twitter_button) - .setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - LinkedIdCreateTwitterStep1Fragment frag = - LinkedIdCreateTwitterStep1Fragment.newInstance(); - - mLinkedIdWizard.loadFragment(null, frag, LinkedIdWizard.FRAG_ACTION_TO_RIGHT); - } - }); - - view.findViewById(R.id.linked_create_github_button) - .setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - LinkedIdCreateGithubFragment frag = - LinkedIdCreateGithubFragment.newInstance(); - - mLinkedIdWizard.loadFragment(null, frag, LinkedIdWizard.FRAG_ACTION_TO_RIGHT); - } - }); + view.findViewById(R.id.linked_create_github_button).setOnClickListener(v -> { + LinkedIdCreateGithubFragment frag = LinkedIdCreateGithubFragment.newInstance(); + ((LinkedIdWizard) requireActivity()).loadFragment(frag, LinkedIdWizard.FRAG_ACTION_TO_RIGHT); + }); return view; } - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - - mLinkedIdWizard = (LinkedIdWizard) getActivity(); - } - } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdWizard.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdWizard.java index 28d9a22c5..bea6df04b 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdWizard.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdWizard.java @@ -18,64 +18,60 @@ package org.sufficientlysecure.keychain.ui.linked; +import android.arch.lifecycle.ViewModelProviders; import android.content.Context; -import android.content.Intent; -import android.net.Uri; import android.os.Bundle; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentTransaction; -import android.support.v4.app.NavUtils; -import android.support.v4.app.TaskStackBuilder; -import android.view.MenuItem; import android.view.View; import android.view.inputmethod.InputMethodManager; import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; -import org.sufficientlysecure.keychain.provider.CachedPublicKeyRing; -import org.sufficientlysecure.keychain.provider.KeyRepository; -import org.sufficientlysecure.keychain.provider.KeychainContract; -import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; +import org.sufficientlysecure.keychain.model.SubKey.UnifiedKeyInfo; import org.sufficientlysecure.keychain.ui.base.BaseActivity; +import org.sufficientlysecure.keychain.ui.keyview.UnifiedKeyInfoViewModel; import timber.log.Timber; public class LinkedIdWizard extends BaseActivity { + public static final String EXTRA_MASTER_KEY_ID = "master_key_id"; public static final int FRAG_ACTION_START = 0; public static final int FRAG_ACTION_TO_RIGHT = 1; public static final int FRAG_ACTION_TO_LEFT = 2; - long mMasterKeyId; - byte[] mFingerprint; - @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setTitle(getString(R.string.title_linked_id_create)); - try { - Uri uri = getIntent().getData(); - uri = KeychainContract.KeyRings.buildUnifiedKeyRingUri(uri); - CachedPublicKeyRing ring = KeyRepository.create(this).getCachedPublicKeyRing(uri); - if (!ring.hasAnySecret()) { - Timber.e("Linked Identities can only be added to secret keys!"); - finish(); - return; - } - - mMasterKeyId = ring.extractOrGetMasterKeyId(); - mFingerprint = ring.getFingerprint(); - } catch (PgpKeyNotFoundException e) { - Timber.e("Invalid uri given, key does not exist!"); + Bundle extras = getIntent().getExtras(); + if (extras == null || !extras.containsKey(EXTRA_MASTER_KEY_ID)) { + Timber.e("Missing required extra master_key_id!"); finish(); return; } + long masterKeyId = extras.getLong(EXTRA_MASTER_KEY_ID); + UnifiedKeyInfoViewModel viewModel = ViewModelProviders.of(this).get(UnifiedKeyInfoViewModel.class); + viewModel.setMasterKeyId(masterKeyId); + viewModel.getUnifiedKeyInfoLiveData(this).observe(this, this::onLoadUnifiedKeyInfo); + + hideKeyboard(); + // pass extras into fragment - LinkedIdSelectFragment frag = LinkedIdSelectFragment.newInstance(); - loadFragment(null, frag, FRAG_ACTION_START); + if (savedInstanceState == null) { + LinkedIdSelectFragment frag = LinkedIdSelectFragment.newInstance(); + loadFragment(frag, FRAG_ACTION_START); + } + } + + private void onLoadUnifiedKeyInfo(UnifiedKeyInfo unifiedKeyInfo) { + if (!unifiedKeyInfo.has_any_secret()) { + Timber.e("Linked Identities can only be added to secret keys!"); + finish(); + } } @Override @@ -83,16 +79,7 @@ public class LinkedIdWizard extends BaseActivity { setContentView(R.layout.create_key_activity); } - public void loadFragment(Bundle savedInstanceState, Fragment fragment, int action) { - // However, if we're being restored from a previous state, - // then we don't need to do anything and should return or else - // we could end up with overlapping fragments. - if (savedInstanceState != null) { - return; - } - - hideKeyboard(); - + public void loadFragment(Fragment fragment, int action) { // Add the fragment to the 'fragment_container' FrameLayout // NOTE: We use commitAllowingStateLoss() to prevent weird crashes! FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); @@ -115,50 +102,17 @@ public class LinkedIdWizard extends BaseActivity { break; } - // do it immediately! getSupportFragmentManager().executePendingTransactions(); } private void hideKeyboard() { - InputMethodManager inputManager = (InputMethodManager) - getSystemService(Context.INPUT_METHOD_SERVICE); + InputMethodManager inputManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); - // check if no view has focus View v = getCurrentFocus(); - if (v == null) + if (v == null || inputManager == null) { return; + } inputManager.hideSoftInputFromWindow(v.getWindowToken(), 0); } - - @Override - public void onBackPressed() { - if (!getFragmentManager().popBackStackImmediate()) { - navigateBack(); - } - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - // Respond to the action bar's Up/Home button - case android.R.id.home: - navigateBack(); - return true; - } - return super.onOptionsItemSelected(item); - } - - private void navigateBack() { - Intent upIntent = NavUtils.getParentActivityIntent(this); - upIntent.setData(KeyRings.buildGenericKeyRingUri(mMasterKeyId)); - // This activity is NOT part of this app's task, so create a new task - // when navigating up, with a synthesized back stack. - TaskStackBuilder.create(this) - // Add all of this activity's parents to the back stack - .addNextIntentWithParentStack(upIntent) - // Navigate up to the closest parent - .startActivities(); - } - } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/token/ManageSecurityTokenFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/token/ManageSecurityTokenFragment.java index 7371e486d..bcbac6e14 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/token/ManageSecurityTokenFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/token/ManageSecurityTokenFragment.java @@ -183,11 +183,7 @@ public class ManageSecurityTokenFragment extends Fragment implements ManageSecur public void finishAndShowKey(long masterKeyId) { Activity activity = getActivity(); - Intent viewKeyIntent = new Intent(activity, ViewKeyActivity.class); - // use the imported masterKeyId, not the one from the token, because - // that one might* just have been a subkey of the imported key - viewKeyIntent.setData(KeyRings.buildGenericKeyRingUri(masterKeyId)); - + Intent viewKeyIntent = ViewKeyActivity.getViewKeyActivityIntent(requireActivity(), masterKeyId); if (activity instanceof CreateKeyActivity) { ((CreateKeyActivity) activity).finishWithFirstTimeHandling(viewKeyIntent); } else { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/token/PublicKeyRetrievalLoader.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/token/PublicKeyRetrievalLoader.java index 3d9170064..1257a232e 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/token/PublicKeyRetrievalLoader.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/token/PublicKeyRetrievalLoader.java @@ -29,14 +29,12 @@ import android.os.SystemClock; import android.support.annotation.Nullable; import android.support.v4.content.AsyncTaskLoader; import android.text.TextUtils; -import android.util.Log; import com.google.auto.value.AutoValue; import okhttp3.Call; import okhttp3.HttpUrl; import okhttp3.Request.Builder; import okhttp3.Response; -import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.keyimport.HkpKeyserverAddress; import org.sufficientlysecure.keychain.keyimport.HkpKeyserverClient; import org.sufficientlysecure.keychain.keyimport.KeyserverClient.QueryFailedException; @@ -48,11 +46,8 @@ import org.sufficientlysecure.keychain.operations.results.OperationResult.Operat import org.sufficientlysecure.keychain.pgp.UncachedKeyRing; import org.sufficientlysecure.keychain.pgp.UncachedKeyRing.IteratorWithIOThrow; import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; -import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; -import org.sufficientlysecure.keychain.provider.CachedPublicKeyRing; -import org.sufficientlysecure.keychain.provider.KeyRepository; -import org.sufficientlysecure.keychain.provider.KeyRepository.NotFoundException; -import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; +import org.sufficientlysecure.keychain.daos.KeyRepository; +import org.sufficientlysecure.keychain.daos.KeyRepository.NotFoundException; import org.sufficientlysecure.keychain.ui.token.PublicKeyRetrievalLoader.KeyRetrievalResult; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; import org.sufficientlysecure.keychain.util.ParcelableProxy; @@ -115,11 +110,12 @@ public abstract class PublicKeyRetrievalLoader extends AsyncTaskLoader. - */ - -package org.sufficientlysecure.keychain.ui.transfer.loader; - - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import android.content.ContentResolver; -import android.content.Context; -import android.database.Cursor; -import android.support.v4.content.AsyncTaskLoader; -import android.util.Log; - -import org.sufficientlysecure.keychain.Constants; -import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; -import org.sufficientlysecure.keychain.ui.transfer.loader.SecretKeyLoader.SecretKeyItem; -import timber.log.Timber; - - -public class SecretKeyLoader extends AsyncTaskLoader> { - public static final String[] PROJECTION = new String[] { - KeyRings.MASTER_KEY_ID, - KeyRings.CREATION, - KeyRings.NAME, - KeyRings.EMAIL, - KeyRings.HAS_ANY_SECRET - }; - private static final int INDEX_KEY_ID = 0; - private static final int INDEX_CREATION = 1; - private static final int INDEX_NAME = 2; - private static final int INDEX_EMAIL = 3; - - - private final ContentResolver contentResolver; - - private List cachedResult; - - - public SecretKeyLoader(Context context, ContentResolver contentResolver) { - super(context); - - this.contentResolver = contentResolver; - } - - @Override - public List loadInBackground() { - String where = KeyRings.HAS_ANY_SECRET + " = 1"; - Cursor cursor = contentResolver.query(KeyRings.buildUnifiedKeyRingsUri(), PROJECTION, where, null, null); - if (cursor == null) { - Timber.e("Error loading key items!"); - return null; - } - - try { - ArrayList secretKeyItems = new ArrayList<>(); - while (cursor.moveToNext()) { - SecretKeyItem secretKeyItem = new SecretKeyItem(cursor); - secretKeyItems.add(secretKeyItem); - } - - return Collections.unmodifiableList(secretKeyItems); - } finally { - cursor.close(); - } - } - - @Override - public void deliverResult(List keySubkeyStatus) { - cachedResult = keySubkeyStatus; - - if (isStarted()) { - super.deliverResult(keySubkeyStatus); - } - } - - @Override - protected void onStartLoading() { - if (cachedResult != null) { - deliverResult(cachedResult); - } - - if (takeContentChanged() || cachedResult == null) { - forceLoad(); - } - } - - public static class SecretKeyItem { - final int position; - public final long masterKeyId; - public final long creationMillis; - public final String name; - public final String email; - - SecretKeyItem(Cursor cursor) { - position = cursor.getPosition(); - - masterKeyId = cursor.getLong(INDEX_KEY_ID); - creationMillis = cursor.getLong(INDEX_CREATION) * 1000; - - name = cursor.getString(INDEX_NAME); - email = cursor.getString(INDEX_EMAIL); - } - } -} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/transfer/presenter/TransferPresenter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/transfer/presenter/TransferPresenter.java index a17f636db..a89a61067 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/transfer/presenter/TransferPresenter.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/transfer/presenter/TransferPresenter.java @@ -22,6 +22,8 @@ import java.io.IOException; import java.net.URISyntaxException; import java.util.List; +import android.arch.lifecycle.LifecycleOwner; +import android.arch.lifecycle.LiveData; import android.content.Context; import android.graphics.Bitmap; import android.net.ConnectivityManager; @@ -30,31 +32,28 @@ import android.net.Uri; import android.net.wifi.WifiInfo; import android.net.wifi.WifiManager; import android.os.Build.VERSION_CODES; -import android.os.Bundle; import android.os.Handler; import android.os.Parcelable; import android.support.annotation.RequiresApi; -import android.support.v4.app.LoaderManager; -import android.support.v4.app.LoaderManager.LoaderCallbacks; -import android.support.v4.content.Loader; import android.support.v7.widget.RecyclerView.Adapter; import android.view.LayoutInflater; import org.openintents.openpgp.util.OpenPgpUtils; import org.openintents.openpgp.util.OpenPgpUtils.UserId; import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing; +import org.sufficientlysecure.keychain.model.SubKey.UnifiedKeyInfo; import org.sufficientlysecure.keychain.network.KeyTransferInteractor; import org.sufficientlysecure.keychain.network.KeyTransferInteractor.KeyTransferCallback; import org.sufficientlysecure.keychain.operations.results.ImportKeyResult; import org.sufficientlysecure.keychain.operations.results.OperationResult; import org.sufficientlysecure.keychain.pgp.UncachedKeyRing; import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; -import org.sufficientlysecure.keychain.provider.KeyRepository; -import org.sufficientlysecure.keychain.provider.KeyRepository.NotFoundException; +import org.sufficientlysecure.keychain.daos.KeyRepository; +import org.sufficientlysecure.keychain.daos.KeyRepository.NotFoundException; import org.sufficientlysecure.keychain.service.ImportKeyringParcel; import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper; import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper.Callback; -import org.sufficientlysecure.keychain.ui.transfer.loader.SecretKeyLoader.SecretKeyItem; +import org.sufficientlysecure.keychain.ui.keyview.GenericViewModel; import org.sufficientlysecure.keychain.ui.transfer.view.ReceivedSecretKeyList.OnClickImportKeyListener; import org.sufficientlysecure.keychain.ui.transfer.view.ReceivedSecretKeyList.ReceivedKeyAdapter; import org.sufficientlysecure.keychain.ui.transfer.view.ReceivedSecretKeyList.ReceivedKeyItem; @@ -65,20 +64,19 @@ import timber.log.Timber; @RequiresApi(api = VERSION_CODES.LOLLIPOP) -public class TransferPresenter implements KeyTransferCallback, LoaderCallbacks>, - OnClickTransferKeyListener, OnClickImportKeyListener { +public class TransferPresenter implements KeyTransferCallback, OnClickTransferKeyListener, OnClickImportKeyListener { private static final String DELIMITER_START = "-----BEGIN PGP PRIVATE KEY BLOCK-----"; private static final String DELIMITER_END = "-----END PGP PRIVATE KEY BLOCK-----"; private static final String BACKSTACK_TAG_TRANSFER = "transfer"; private final Context context; private final TransferMvpView view; - private final LoaderManager loaderManager; - private final int loaderId; - private final KeyRepository databaseInteractor; + private final KeyRepository keyRepository; private final TransferKeyAdapter secretKeyAdapter; private final ReceivedKeyAdapter receivedKeyAdapter; + private final LifecycleOwner lifecycleOwner; + private final GenericViewModel viewModel; private KeyTransferInteractor keyTransferClientInteractor; @@ -90,12 +88,13 @@ public class TransferPresenter implements KeyTransferCallback, LoaderCallbacks> liveData = + viewModel.getGenericLiveData(context, keyRepository::getAllUnifiedKeyInfoWithSecret); + liveData.observe(lifecycleOwner, this::onLoadSecretUnifiedKeyInfo); if (keyTransferServerInteractor == null && keyTransferClientInteractor == null && !wasConnected) { checkWifiResetAndStartListen(); } } + private void onLoadSecretUnifiedKeyInfo(List data) { + secretKeyAdapter.setData(data); + view.setShowSecretKeyEmptyView(data.isEmpty()); + } + public void onUiStop() { connectionClear(); @@ -288,12 +294,9 @@ public class TransferPresenter implements KeyTransferCallback, LoaderCallbacks { + secretKeyAdapter.focusItem(null); + secretKeyAdapter.addToFinishedItems(masterKeyId); }, 750); } @@ -376,6 +379,10 @@ public class TransferPresenter implements KeyTransferCallback, LoaderCallbacks> onCreateLoader(int id, Bundle args) { - return secretKeyAdapter.createLoader(context); - } - - @Override - public void onLoadFinished(Loader> loader, List data) { - secretKeyAdapter.setData(data); - view.setShowSecretKeyEmptyView(data.isEmpty()); - } - - @Override - public void onLoaderReset(Loader> loader) { - secretKeyAdapter.setData(null); - } - - public interface TransferMvpView { void showNotOnWifi(); void showWaitingForConnection(); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/transfer/view/TransferFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/transfer/view/TransferFragment.java index b9eb8f7bc..263205da7 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/transfer/view/TransferFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/transfer/view/TransferFragment.java @@ -19,6 +19,7 @@ package org.sufficientlysecure.keychain.ui.transfer.view; import android.app.Activity; +import android.arch.lifecycle.ViewModelProviders; import android.content.BroadcastReceiver; import android.content.Context; import android.content.DialogInterface; @@ -62,6 +63,7 @@ import org.sufficientlysecure.keychain.ui.MainActivity; import org.sufficientlysecure.keychain.ui.QrCodeCaptureActivity; import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper; import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper.Callback; +import org.sufficientlysecure.keychain.ui.keyview.GenericViewModel; import org.sufficientlysecure.keychain.ui.transfer.presenter.TransferPresenter; import org.sufficientlysecure.keychain.ui.transfer.presenter.TransferPresenter.TransferMvpView; import org.sufficientlysecure.keychain.ui.util.Notify; @@ -140,7 +142,8 @@ public class TransferFragment extends Fragment implements TransferMvpView { } }); - presenter = new TransferPresenter(getContext(), getLoaderManager(), LOADER_ID, this); + GenericViewModel genericViewModel = ViewModelProviders.of(this).get(GenericViewModel.class); + presenter = new TransferPresenter(getContext(), this, genericViewModel, this); setHasOptionsMenu(true); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/transfer/view/TransferSecretKeyList.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/transfer/view/TransferSecretKeyList.java index 6bca7cd05..5cdcd8842 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/transfer/view/TransferSecretKeyList.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/transfer/view/TransferSecretKeyList.java @@ -22,8 +22,8 @@ import java.util.ArrayList; import java.util.List; import android.content.Context; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import android.support.v4.content.Loader; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.text.format.DateUtils; @@ -35,8 +35,7 @@ import android.widget.TextView; import android.widget.ViewAnimator; import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.ui.transfer.loader.SecretKeyLoader; -import org.sufficientlysecure.keychain.ui.transfer.loader.SecretKeyLoader.SecretKeyItem; +import org.sufficientlysecure.keychain.model.SubKey.UnifiedKeyInfo; import org.sufficientlysecure.keychain.ui.util.recyclerview.DividerItemDecoration; @@ -75,7 +74,7 @@ public class TransferSecretKeyList extends RecyclerView { private final OnClickTransferKeyListener onClickTransferKeyListener; private Long focusedMasterKeyId; - private List data; + private List data; private ArrayList finishedItems = new ArrayList<>(); private boolean allItemsDisabled; @@ -87,15 +86,16 @@ public class TransferSecretKeyList extends RecyclerView { this.onClickTransferKeyListener = onClickTransferKeyListener; } + @NonNull @Override - public TransferKeyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + public TransferKeyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return new TransferKeyViewHolder(layoutInflater.inflate(R.layout.key_transfer_item, parent, false)); } @Override - public void onBindViewHolder(TransferKeyViewHolder holder, int position) { - SecretKeyItem item = data.get(position); - boolean isFinished = finishedItems.contains(item.masterKeyId); + public void onBindViewHolder(@NonNull TransferKeyViewHolder holder, int position) { + UnifiedKeyInfo item = data.get(position); + boolean isFinished = finishedItems.contains(item.master_key_id()); holder.bind(context, item, onClickTransferKeyListener, focusedMasterKeyId, isFinished, allItemsDisabled); } @@ -106,10 +106,10 @@ public class TransferSecretKeyList extends RecyclerView { @Override public long getItemId(int position) { - return data.get(position).masterKeyId; + return data.get(position).master_key_id(); } - public void setData(List data) { + public void setData(List data) { this.data = data; notifyDataSetChanged(); } @@ -129,10 +129,6 @@ public class TransferSecretKeyList extends RecyclerView { notifyItemRangeChanged(0, getItemCount()); } - public Loader> createLoader(Context context) { - return new SecretKeyLoader(context, context.getContentResolver()); - } - public void setAllDisabled(boolean allItemsdisablde) { allItemsDisabled = allItemsdisablde; notifyItemRangeChanged(0, getItemCount()); @@ -157,23 +153,23 @@ public class TransferSecretKeyList extends RecyclerView { vState = itemView.findViewById(R.id.transfer_state); } - private void bind(Context context, final SecretKeyItem item, + private void bind(Context context, UnifiedKeyInfo item, final OnClickTransferKeyListener onClickTransferKeyListener, Long focusedMasterKeyId, boolean isFinished, boolean disableAll) { - if (item.name != null) { - vName.setText(item.name); + if (item.name() != null) { + vName.setText(item.name()); vName.setVisibility(View.VISIBLE); } else { vName.setVisibility(View.GONE); } - if (item.email != null) { - vEmail.setText(item.email); + if (item.email() != null) { + vEmail.setText(item.email()); vEmail.setVisibility(View.VISIBLE); } else { vEmail.setVisibility(View.GONE); } - String dateTime = DateUtils.formatDateTime(context, item.creationMillis, + String dateTime = DateUtils.formatDateTime(context, item.creation() * 1000, DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_ABBREV_MONTH); vCreation.setText(context.getString(R.string.label_key_created, dateTime)); @@ -186,7 +182,7 @@ public class TransferSecretKeyList extends RecyclerView { } if (focusedMasterKeyId != null) { - if (focusedMasterKeyId != item.masterKeyId) { + if (focusedMasterKeyId != item.master_key_id()) { itemView.animate().alpha(0.2f).start(); vState.setDisplayedChild(isFinished ? STATE_TRANSFERRED : STATE_INVISIBLE); } else { @@ -199,12 +195,8 @@ public class TransferSecretKeyList extends RecyclerView { } if (focusedMasterKeyId == null && onClickTransferKeyListener != null) { - vSendButton.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - onClickTransferKeyListener.onUiClickTransferKey(item.masterKeyId); - } - }); + vSendButton.setOnClickListener( + v -> onClickTransferKeyListener.onUiClickTransferKey(item.master_key_id())); } else { vSendButton.setOnClickListener(null); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/Highlighter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/Highlighter.java index e1488e09d..39ea04187 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/Highlighter.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/Highlighter.java @@ -30,9 +30,12 @@ public class Highlighter { private Context mContext; private String mQuery; - public Highlighter(Context context, String query) { + public Highlighter(Context context) { mContext = context; - mQuery = query; + } + + public void setQuery(String mQuery) { + this.mQuery = mQuery; } public Spannable highlight(CharSequence text) { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/KeyFormattingUtils.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/KeyFormattingUtils.java index bb5f7b105..c9ef239d6 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/KeyFormattingUtils.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/KeyFormattingUtils.java @@ -363,6 +363,11 @@ public class KeyFormattingUtils { return idHex; } + public static String beautifyKeyId(byte[] fingerprint) { + long keyId = KeyFormattingUtils.convertFingerprintToKeyId(fingerprint); + return beautifyKeyId(keyId); + } + /** * Makes a human-readable version of a key ID, which is usually 64 bits: lower-case, no * leading 0x, space-separated quartets (for keys whose length in hex is divisible by 4) diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/KeyInfoFormatter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/KeyInfoFormatter.java new file mode 100644 index 000000000..940f01776 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/KeyInfoFormatter.java @@ -0,0 +1,173 @@ +package org.sufficientlysecure.keychain.ui.util; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.support.annotation.NonNull; +import android.support.v4.content.ContextCompat; +import android.text.format.DateUtils; +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; + +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.model.SubKey; +import org.sufficientlysecure.keychain.model.SubKey.UnifiedKeyInfo; + +import java.util.List; + +public class KeyInfoFormatter { + + private static final long JUST_NOW_THRESHOLD = DateUtils.MINUTE_IN_MILLIS * 5; + + private Context context; + private Highlighter highlighter; + private UnifiedKeyInfo keyInfo; + + public KeyInfoFormatter(Context context) { + this.context = context; + highlighter = new Highlighter(context); + } + + public void setKeyInfo(UnifiedKeyInfo keyInfo) { + this.keyInfo = keyInfo; + } + + public void setHighlightString(String highlight) { + highlighter.setQuery(highlight); + } + + public void formatUserId(TextView name, TextView email) { + if (keyInfo.name() == null) { + if (keyInfo.email() != null) { + name.setText(highlighter.highlight(keyInfo.email())); + email.setVisibility(View.GONE); + } else { + name.setText(R.string.user_id_no_name); + } + } else { + name.setText(highlighter.highlight(keyInfo.name())); + if (keyInfo.email() != null) { + email.setText(highlighter.highlight(keyInfo.email())); + email.setVisibility(View.VISIBLE); + } else { + email.setVisibility(View.GONE); + } + } + } + + public void formatCreationDate(TextView creationDate) { + if (keyInfo.has_duplicate() || keyInfo.has_any_secret()) { + creationDate.setText(getSecretKeyReadableTime(context, keyInfo)); + creationDate.setVisibility(View.VISIBLE); + } else { + creationDate.setVisibility(View.GONE); + } + } + + public void greyInvalidKeys(List textviews) { + int textColor; + + // Note: order is important! + if (keyInfo.is_revoked()) { + textColor = ContextCompat.getColor(context, R.color.key_flag_gray); + } else if (keyInfo.is_expired()) { + textColor = ContextCompat.getColor(context, R.color.key_flag_gray); + } else if (!keyInfo.is_secure()) { + textColor = ContextCompat.getColor(context, R.color.key_flag_gray); + } else if (keyInfo.has_any_secret()) { + textColor = FormattingUtils.getColorFromAttr(context, R.attr.colorText); + } else { + textColor = FormattingUtils.getColorFromAttr(context, R.attr.colorText); + } + for (TextView textView : textviews) { + textView.setTextColor(textColor); + } + } + + public void formatStatusIcon(ImageView statusIcon) { + + // Note: order is important! + if (keyInfo.is_revoked()) { + KeyFormattingUtils.setStatusImage( + context, + statusIcon, + null, + KeyFormattingUtils.State.REVOKED, + R.color.key_flag_gray + ); + + statusIcon.setVisibility(View.VISIBLE); + } else if (keyInfo.is_expired()) { + KeyFormattingUtils.setStatusImage( + context, + statusIcon, + null, + KeyFormattingUtils.State.EXPIRED, + R.color.key_flag_gray + ); + + statusIcon.setVisibility(View.VISIBLE); + } else if (!keyInfo.is_secure()) { + KeyFormattingUtils.setStatusImage( + context, + statusIcon, + null, + KeyFormattingUtils.State.INSECURE, + R.color.key_flag_gray + ); + + statusIcon.setVisibility(View.VISIBLE); + } else if (keyInfo.has_any_secret()) { + statusIcon.setVisibility(View.GONE); + } else { + // this is a public key - show if it's verified + if (keyInfo.is_verified()) { + KeyFormattingUtils.setStatusImage( + context, + statusIcon, + KeyFormattingUtils.State.VERIFIED + ); + + statusIcon.setVisibility(View.VISIBLE); + } else { + statusIcon.setVisibility(View.GONE); + } + } + } + + public void formatTrustIcon(ImageView trustIdIcon) { + if (!keyInfo.has_any_secret() && !keyInfo.autocrypt_package_names().isEmpty()) { + String packageName = keyInfo.autocrypt_package_names().get(0); + Drawable drawable = PackageIconGetter.getInstance(context).getDrawableForPackageName(packageName); + if (drawable != null) { + trustIdIcon.setImageDrawable(drawable); + trustIdIcon.setVisibility(View.VISIBLE); + } else { + trustIdIcon.setVisibility(View.GONE); + } + } else { + trustIdIcon.setVisibility(View.GONE); + } + } + + @NonNull + private String getSecretKeyReadableTime(Context context, SubKey.UnifiedKeyInfo keyInfo) { + long creationMillis = keyInfo.creation() * 1000; + + boolean allowRelativeTimestamp = keyInfo.has_duplicate(); + if (allowRelativeTimestamp) { + long creationAgeMillis = System.currentTimeMillis() - creationMillis; + if (creationAgeMillis < JUST_NOW_THRESHOLD) { + return context.getString(R.string.label_key_created_just_now); + } + } + + String dateTime = DateUtils.formatDateTime(context, + creationMillis, + DateUtils.FORMAT_SHOW_DATE + | DateUtils.FORMAT_SHOW_TIME + | DateUtils.FORMAT_SHOW_YEAR + | DateUtils.FORMAT_ABBREV_MONTH); + return context.getString(R.string.label_key_created, dateTime); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/adapter/CursorAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/adapter/CursorAdapter.java deleted file mode 100644 index 562f83383..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/adapter/CursorAdapter.java +++ /dev/null @@ -1,467 +0,0 @@ -/* - * Copyright (C) 2017 Schürmann & Breitmoser GbR - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sufficientlysecure.keychain.ui.util.adapter; - -import android.content.Context; -import android.database.ContentObserver; -import android.database.Cursor; -import android.database.CursorWrapper; -import android.database.DataSetObserver; -import android.os.Handler; -import android.support.v7.widget.RecyclerView; - -import org.sufficientlysecure.keychain.provider.KeychainContract; -import org.sufficientlysecure.keychain.ui.util.adapter.CursorAdapter.SimpleCursor; -import timber.log.Timber; - -import java.lang.reflect.Constructor; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Date; -import java.util.HashMap; - -public abstract class CursorAdapter - extends RecyclerView.Adapter { - public static final String TAG = "CursorAdapter"; - - private C mCursor; - private Context mContext; - private boolean mDataValid; - - private ChangeObserver mChangeObserver; - private DataSetObserver mDataSetObserver; - - /** - * If set the adapter will register a content observer on the cursor and will call - * {@link #onContentChanged()} when a notification comes in. Be careful when - * using this flag: you will need to unset the current Cursor from the adapter - * to avoid leaks due to its registered observers. This flag is not needed - * when using a CursorAdapter with a - * {@link android.content.CursorLoader}. - */ - public static final int FLAG_REGISTER_CONTENT_OBSERVER = 0x02; - - /** - * Constructor that allows control over auto-requery. It is recommended - * you not use this, but instead {@link #CursorAdapter(Context, SimpleCursor, int)}. - * When using this constructor, {@link #FLAG_REGISTER_CONTENT_OBSERVER} - * will always be set. - * - * @param c The cursor from which to get the data. - * @param context The context - */ - public CursorAdapter(Context context, C c) { - setHasStableIds(true); - init(context, c, FLAG_REGISTER_CONTENT_OBSERVER); - } - - /** - * Recommended constructor. - * - * @param c The cursor from which to get the data. - * @param context The context - * @param flags Flags used to determine the behavior of the adapter - * @see #FLAG_REGISTER_CONTENT_OBSERVER - */ - public CursorAdapter(Context context, C c, int flags) { - setHasStableIds(true); - init(context, c, flags); - } - - private void init(Context context, C c, int flags) { - boolean cursorPresent = c != null; - mCursor = c; - mDataValid = cursorPresent; - mContext = context; - if ((flags & FLAG_REGISTER_CONTENT_OBSERVER) == FLAG_REGISTER_CONTENT_OBSERVER) { - mChangeObserver = new ChangeObserver(); - mDataSetObserver = new MyDataSetObserver(); - } else { - mChangeObserver = null; - mDataSetObserver = null; - } - - if (cursorPresent) { - if (mChangeObserver != null) c.registerContentObserver(mChangeObserver); - if (mDataSetObserver != null) c.registerDataSetObserver(mDataSetObserver); - } - - setHasStableIds(true); - } - - /** - * Returns the cursor. - * - * @return the cursor. - */ - public C getCursor() { - return mCursor; - } - - public Context getContext() { - return mContext; - } - - /** - * @see android.support.v7.widget.RecyclerView.Adapter#getItemCount() - */ - @Override - public int getItemCount() { - if (mDataValid && mCursor != null) { - return mCursor.getCount(); - } else { - return 0; - } - } - - public boolean hasValidData() { - mDataValid = hasOpenCursor(); - return mDataValid; - } - - private boolean hasOpenCursor() { - Cursor cursor = getCursor(); - if (cursor != null && cursor.isClosed()) { - swapCursor(null); - return false; - } - - return cursor != null; - } - - /** - * @param position Adapter position to query - * @return the id of the item - * @see android.support.v7.widget.RecyclerView.Adapter#getItemId(int) - */ - @Override - public long getItemId(int position) { - if (mDataValid && mCursor != null) { - if (moveCursor(position)) { - return getIdFromCursor(mCursor); - } else { - return RecyclerView.NO_ID; - } - } else { - return RecyclerView.NO_ID; - } - } - - /** - * Return the id of the item represented by the row the cursor - * is currently moved to. - * - * @param cursor The cursor moved to the correct position. - * @return The id of the dataset - */ - public long getIdFromCursor(C cursor) { - if (cursor != null) { - return cursor.getEntryId(); - } else { - return RecyclerView.NO_ID; - } - } - - public void moveCursorOrThrow(int position) - throws IndexOutOfBoundsException, IllegalStateException { - - if (position >= getItemCount() || position < -1) { - throw new IndexOutOfBoundsException("Position: " + position - + " is invalid for this data set!"); - } - - if (!mDataValid) { - throw new IllegalStateException("Attempt to move cursor over invalid data set!"); - } - - if (!mCursor.moveToPosition(position)) { - throw new IllegalStateException("Couldn't move cursor from position: " - + mCursor.getPosition() + " to position: " + position + "!"); - } - } - - public boolean moveCursor(int position) { - if (position >= getItemCount() || position < -1) { - Timber.w("Position: %d is invalid for this data set!"); - return false; - } - - if (!mDataValid) { - Timber.d("Attempt to move cursor over invalid data set!"); - } - - return mCursor.moveToPosition(position); - } - - /** - * Change the underlying cursor to a new cursor. If there is an existing cursor it will be - * closed. - * - * @param cursor The new cursor to be used - */ - public void changeCursor(C cursor) { - Cursor old = swapCursor(cursor); - if (old != null) { - old.close(); - } - } - - /** - * Swap in a new Cursor, returning the old Cursor. Unlike - * {@link #changeCursor(SimpleCursor)}, the returned old Cursor is not - * closed. - * - * @param newCursor The new cursor to be used. - * @return Returns the previously set Cursor, or null if there wasa not one. - * If the given new Cursor is the same instance is the previously set - * Cursor, null is also returned. - */ - public C swapCursor(C newCursor) { - if (newCursor == mCursor) { - return null; - } - - C oldCursor = mCursor; - if (oldCursor != null) { - if (mChangeObserver != null) oldCursor.unregisterContentObserver(mChangeObserver); - if (mDataSetObserver != null) oldCursor.unregisterDataSetObserver(mDataSetObserver); - } - - mCursor = newCursor; - if (newCursor != null) { - if (mChangeObserver != null) newCursor.registerContentObserver(mChangeObserver); - if (mDataSetObserver != null) newCursor.registerDataSetObserver(mDataSetObserver); - mDataValid = true; - // notify the observers about the new cursor - onContentChanged(); - } else { - mDataValid = false; - // notify the observers about the lack of a data set - onContentChanged(); - } - - return oldCursor; - } - - /** - *

Converts the cursor into a CharSequence. Subclasses should override this - * method to convert their results. The default implementation returns an - * empty String for null values or the default String representation of - * the value.

- * - * @param cursor the cursor to convert to a CharSequence - * @return a CharSequence representing the value - */ - public CharSequence convertToString(Cursor cursor) { - return cursor == null ? "" : cursor.toString(); - } - - /** - * Called when the {@link ContentObserver} on the cursor receives a change notification. - * The default implementation provides the auto-requery logic, but may be overridden by - * sub classes. - * - * @see ContentObserver#onChange(boolean) - */ - protected void onContentChanged() { - notifyDataSetChanged(); - } - - private class ChangeObserver extends ContentObserver { - public ChangeObserver() { - super(new Handler()); - } - - @Override - public boolean deliverSelfNotifications() { - return true; - } - - @Override - public void onChange(boolean selfChange) { - onContentChanged(); - } - } - - private class MyDataSetObserver extends DataSetObserver { - @Override - public void onChanged() { - mDataValid = true; - onContentChanged(); - } - - @Override - public void onInvalidated() { - mDataValid = false; - onContentChanged(); - } - } - - public static class SimpleCursor extends CursorWrapper { - public static final String[] PROJECTION = {"_id"}; - - public static T wrap(Cursor cursor, Class type) { - if (cursor != null) { - try { - Constructor constructor = type.getConstructor(Cursor.class); - return constructor.newInstance(cursor); - } catch (Exception e) { - Timber.e(e, "Could not create instance of cursor wrapper!"); - } - } - - return null; - } - - private HashMap mColumnIndices; - - /** - * Creates a cursor wrapper. - * - * @param cursor The underlying cursor to wrap. - */ - public SimpleCursor(Cursor cursor) { - super(cursor); - mColumnIndices = new HashMap<>(cursor.getColumnCount() * 4 / 3, 0.75f); - } - - @Override - public void close() { - mColumnIndices.clear(); - super.close(); - } - - public final int getEntryId() { - int index = getColumnIndexOrThrow("_id"); - return getInt(index); - } - - @Override - public final int getColumnIndexOrThrow(String colName) { - Integer colIndex = mColumnIndices.get(colName); - if (colIndex == null) { - colIndex = super.getColumnIndexOrThrow(colName); - mColumnIndices.put(colName, colIndex); - } else if (colIndex < 0) { - throw new IllegalArgumentException("Could not get column index for name: \"" + colName + "\""); - } - - return colIndex; - } - - @Override - public final int getColumnIndex(String colName) { - Integer colIndex = mColumnIndices.get(colName); - if (colIndex == null) { - colIndex = super.getColumnIndex(colName); - mColumnIndices.put(colName, colIndex); - } - - return colIndex; - } - } - - public static class KeyCursor extends SimpleCursor { - public static final String[] PROJECTION; - - static { - ArrayList arr = new ArrayList<>(); - arr.addAll(Arrays.asList(SimpleCursor.PROJECTION)); - arr.addAll(Arrays.asList( - KeychainContract.KeyRings.MASTER_KEY_ID, - KeychainContract.KeyRings.USER_ID, - KeychainContract.KeyRings.IS_REVOKED, - KeychainContract.KeyRings.IS_EXPIRED, - KeychainContract.KeyRings.IS_SECURE, - KeychainContract.KeyRings.HAS_DUPLICATE_USER_ID, - KeychainContract.KeyRings.CREATION, - KeychainContract.KeyRings.NAME, - KeychainContract.KeyRings.EMAIL, - KeychainContract.KeyRings.COMMENT - )); - - PROJECTION = arr.toArray(new String[arr.size()]); - } - - public static KeyCursor wrap(Cursor cursor) { - if (cursor != null) { - return new KeyCursor(cursor); - } else { - return null; - } - } - - /** - * Creates a cursor wrapper. - * - * @param cursor The underlying cursor to wrap. - */ - protected KeyCursor(Cursor cursor) { - super(cursor); - } - - public long getKeyId() { - int index = getColumnIndexOrThrow(KeychainContract.KeyRings.MASTER_KEY_ID); - return getLong(index); - } - - public String getName() { - int index = getColumnIndexOrThrow(KeychainContract.KeyRings.NAME); - return getString(index); - } - - public String getEmail() { - int index = getColumnIndexOrThrow(KeychainContract.KeyRings.EMAIL); - return getString(index); - } - - public String getComment() { - int index = getColumnIndexOrThrow(KeychainContract.KeyRings.COMMENT); - return getString(index); - } - - public boolean hasDuplicate() { - int index = getColumnIndexOrThrow(KeychainContract.KeyRings.HAS_DUPLICATE_USER_ID); - return getLong(index) > 0L; - } - - public boolean isRevoked() { - int index = getColumnIndexOrThrow(KeychainContract.KeyRings.IS_REVOKED); - return getInt(index) > 0; - } - - public boolean isExpired() { - int index = getColumnIndexOrThrow(KeychainContract.KeyRings.IS_EXPIRED); - return getInt(index) > 0; - } - - public boolean isSecure() { - int index = getColumnIndexOrThrow(KeychainContract.KeyRings.IS_SECURE); - return getInt(index) > 0; - } - - public long getCreationTime() { - int index = getColumnIndexOrThrow(KeychainContract.KeyRings.CREATION); - return getLong(index) * 1000; - } - - public Date getCreationDate() { - return new Date(getCreationTime()); - } - } -} \ No newline at end of file diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/adapter/SectionCursorAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/adapter/SectionCursorAdapter.java deleted file mode 100644 index 4af5faf82..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/adapter/SectionCursorAdapter.java +++ /dev/null @@ -1,337 +0,0 @@ -/* - * Copyright (C) 2017 Schürmann & Breitmoser GbR - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sufficientlysecure.keychain.ui.util.adapter; - -import android.content.Context; -import android.support.v4.util.SparseArrayCompat; -import android.support.v7.widget.RecyclerView; -import android.view.View; -import android.view.ViewGroup; - -import com.tonicartos.superslim.LayoutManager; - -import org.sufficientlysecure.keychain.ui.util.adapter.CursorAdapter.SimpleCursor; -import timber.log.Timber; - - -/** - * @param section type. - * @param the view holder extending {@code BaseViewHolder} that is bound to the cursor data. - * @param the view holder extending {@code BaseViewHolder<>} that is bound to the section data. - */ -public abstract class SectionCursorAdapter extends CursorAdapter { - - public static final String TAG = "SectionCursorAdapter"; - - private static final short VIEW_TYPE_ITEM = 0x1; - private static final short VIEW_TYPE_SECTION = 0x2; - - private SparseArrayCompat mSectionMap = new SparseArrayCompat<>(); - private Comparator mSectionComparator; - - public SectionCursorAdapter(Context context, C cursor, int flags) { - this(context, cursor, flags, new Comparator() { - @Override - public boolean equal(T obj1, T obj2) { - return (obj1 == null) ? - obj2 == null : obj1.equals(obj2); - } - }); - } - - public SectionCursorAdapter(Context context, C cursor, int flags, Comparator comparator) { - super(context, cursor, flags); - setSectionComparator(comparator); - } - - @Override - public void onContentChanged() { - if (hasValidData()) { - buildSections(); - } else { - mSectionMap.clear(); - } - - super.onContentChanged(); - } - - /** - * Assign a comparator which will be used to check whether - * a section is contained in the list of sections. The default implementation - * will check for null pointers and compare sections using the {@link #equals(Object)} method. - * @param comparator The comparator to compare section objects. - */ - public void setSectionComparator(Comparator comparator) { - this.mSectionComparator = comparator; - buildSections(); - } - - /** - * If the adapter's cursor is not null then this method will call buildSections(Cursor cursor). - */ - private void buildSections() { - if (hasValidData()) { - moveCursor(-1); - try { - mSectionMap.clear(); - appendSections(getCursor()); - } catch (IllegalStateException e) { - Timber.e(e, "Couldn't build sections. Perhaps you're moving the cursor" + - "in #getSectionFromCursor(Cursor)?"); - swapCursor(null); - - mSectionMap.clear(); - } - } - } - - private void appendSections(C cursor) throws IllegalStateException { - int cursorPosition = 0; - while(hasValidData() && cursor.moveToNext()) { - T section = getSectionFromCursor(cursor); - if (cursor.getPosition() != cursorPosition) { - throw new IllegalStateException("Do not move the cursor's position in getSectionFromCursor."); - } - if (!hasSection(section)) { - mSectionMap.append(cursorPosition + mSectionMap.size(), section); - } - cursorPosition++; - } - } - - public boolean hasSection(T section) { - for(int i = 0; i < mSectionMap.size(); i++) { - T obj = mSectionMap.valueAt(i); - if(mSectionComparator.equal(obj, section)) - return true; - } - - return false; - } - - /** - * The object which is return will determine what section this cursor position will be in. - * @return the section from the cursor at its current position. - * This object will be passed to newSectionView and bindSectionView. - */ - protected abstract T getSectionFromCursor(C cursor) throws IllegalStateException; - - /** - * Return the id of the item represented by the row the cursor - * is currently moved to. - * @param section The section item to get the id from - * @return The id of the dataset - */ - public long getIdFromSection(T section) { - return section != null ? section.hashCode() : 0L; - } - - @Override - public int getItemCount() { - return super.getItemCount() + mSectionMap.size(); - } - - @Override - public final long getItemId(int listPosition) { - int index = mSectionMap.indexOfKey(listPosition); - if (index < 0) { - int cursorPosition = getCursorPositionWithoutSections(listPosition); - return super.getItemId(cursorPosition); - } else { - T section = mSectionMap.valueAt(index); - return getIdFromSection(section); - } - } - - /** - * @param listPosition the position of the current item in the list with mSectionMap included - * @return Whether or not the listPosition points to a section. - */ - public boolean isSection(int listPosition) { - return mSectionMap.indexOfKey(listPosition) >= 0; - } - - /** - * This will map a position in the list adapter (which includes mSectionMap) to a position in - * the cursor (which does not contain mSectionMap). - * - * @param listPosition the position of the current item in the list with mSectionMap included - * @return the correct position to use with the cursor - */ - public int getCursorPositionWithoutSections(int listPosition) { - if (mSectionMap.size() == 0) { - return listPosition; - } else if (!isSection(listPosition)) { - int sectionIndex = getSectionForPosition(listPosition); - if (isListPositionBeforeFirstSection(listPosition, sectionIndex)) { - return listPosition; - } else { - return listPosition - (sectionIndex + 1); - } - } else { - return -1; - } - } - - public int getListPosition(int cursorPosition) { - for(int i = 0; i < mSectionMap.size(); i++) { - int sectionIndex = mSectionMap.keyAt(i); - if (sectionIndex > cursorPosition) { - return cursorPosition; - } - - cursorPosition +=1; - } - - return cursorPosition; - } - - /** - * Given the list position of an item in the adapter, returns the - * adapter position of the first item of the section the given item belongs to. - * @param listPosition The absolute list position. - * @return The position of the first item of the section. - */ - public int getFirstSectionPosition(int listPosition) { - int start = 0; - for(int i = 0; i <= listPosition; i++) { - if(isSection(i)) { - start = i; - } - } - - return start; - } - - - public int getSectionForPosition(int listPosition) { - boolean isSection = false; - int numPrecedingSections = 0; - for (int i = 0; i < mSectionMap.size(); i++) { - int sectionPosition = mSectionMap.keyAt(i); - - if (listPosition > sectionPosition) { - numPrecedingSections++; - } else if (listPosition == sectionPosition) { - isSection = true; - } else { - break; - } - } - - return isSection ? numPrecedingSections : Math.max(numPrecedingSections - 1, 0); - } - - private boolean isListPositionBeforeFirstSection(int listPosition, int sectionIndex) { - boolean hasSections = mSectionMap != null && mSectionMap.size() > 0; - return sectionIndex == 0 && hasSections && listPosition < mSectionMap.keyAt(0); - } - - @Override - public final int getItemViewType(int listPosition) { - int sectionIndex = mSectionMap.indexOfKey(listPosition); - if(sectionIndex < 0) { - int cursorPosition = getCursorPositionWithoutSections(listPosition); - return (getSectionItemViewType(cursorPosition) << 16) | VIEW_TYPE_ITEM; - } else { - return (getSectionHeaderViewType(sectionIndex) << 16) | VIEW_TYPE_SECTION; - } - } - - protected short getSectionHeaderViewType(int sectionIndex) { - return 0; - } - - protected short getSectionItemViewType(int position) { - return 0; - } - - @Override - @SuppressWarnings("unchecked") - public final void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { - LayoutManager.LayoutParams layoutParams = LayoutManager.LayoutParams - .from(holder.itemView.getLayoutParams()); - - // assign first position of section to each item - layoutParams.setFirstPosition(getFirstSectionPosition(position)); - - int viewType = holder.getItemViewType() & 0xFF; - switch (viewType) { - case VIEW_TYPE_ITEM : - moveCursorOrThrow(getCursorPositionWithoutSections(position)); - onBindItemViewHolder((VH) holder, getCursor()); - - layoutParams.isHeader = false; - break; - - case VIEW_TYPE_SECTION: - T section = mSectionMap.get(position); - onBindSectionViewHolder((SH) holder, section); - - layoutParams.isHeader = true; - break; - } - - holder.itemView.setLayoutParams(layoutParams); - } - - @Override - public final RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - switch (viewType & 0xFF) { - case VIEW_TYPE_SECTION: - return onCreateSectionViewHolder(parent, viewType >> 16); - - case VIEW_TYPE_ITEM: - return onCreateItemViewHolder(parent, viewType >> 16); - - default: - return null; - } - } - - protected abstract SH onCreateSectionViewHolder(ViewGroup parent, int viewType); - protected abstract VH onCreateItemViewHolder(ViewGroup parent, int viewType); - - protected abstract void onBindSectionViewHolder(SH holder, T section); - protected abstract void onBindItemViewHolder(VH holder, C cursor); - - public interface Comparator { - boolean equal(T obj1, T obj2); - } - - public static class ViewHolder extends RecyclerView.ViewHolder { - public ViewHolder(View itemView) { - super(itemView); - } - - /** - * Returns the view type assigned in - * {@link SectionCursorAdapter#getSectionHeaderViewType(int)} or - * {@link SectionCursorAdapter#getSectionItemViewType(int)} - * - * Note that a call to {@link #getItemViewType()} will return a value that contains - * internal stuff necessary to distinguish sections from items. - * @return The view type you set. - */ - public short getItemViewTypeWithoutSections(){ - return (short) (getItemViewType() >> 16); - } - } -} - diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/CertListWidget.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/CertListWidget.java index 7606f4ba6..f84c51367 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/CertListWidget.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/CertListWidget.java @@ -33,43 +33,14 @@ import android.widget.TextView; import android.widget.ViewAnimator; import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.model.Certification.CertDetails; import org.sufficientlysecure.keychain.provider.KeychainContract; import org.sufficientlysecure.keychain.provider.KeychainContract.Certs; -public class CertListWidget extends ViewAnimator - implements LoaderManager.LoaderCallbacks { - - public static final int LOADER_ID_LINKED_CERTS = 38572; - - public static final String ARG_URI = "uri"; - public static final String ARG_IS_SECRET = "is_secret"; - - - // These are the rows that we will retrieve. - static final String[] CERTS_PROJECTION = new String[]{ - KeychainContract.Certs._ID, - KeychainContract.Certs.MASTER_KEY_ID, - KeychainContract.Certs.VERIFIED, - KeychainContract.Certs.TYPE, - KeychainContract.Certs.RANK, - KeychainContract.Certs.KEY_ID_CERTIFIER, - KeychainContract.Certs.USER_ID, - KeychainContract.Certs.SIGNER_UID, - KeychainContract.Certs.CREATION - }; - public static final int INDEX_MASTER_KEY_ID = 1; - public static final int INDEX_VERIFIED = 2; - public static final int INDEX_TYPE = 3; - public static final int INDEX_RANK = 4; - public static final int INDEX_KEY_ID_CERTIFIER = 5; - public static final int INDEX_USER_ID = 6; - public static final int INDEX_SIGNER_UID = 7; - public static final int INDEX_CREATION = 8; - +public class CertListWidget extends ViewAnimator { private TextView vCollapsed; private ListView vExpanded; private View vExpandButton; - private boolean mIsSecret; public CertListWidget(Context context, AttributeSet attrs) { super(context, attrs); @@ -105,41 +76,11 @@ public class CertListWidget extends ViewAnimator setDisplayedChild(expanded ? 1 : 0); } - @Override - public Loader onCreateLoader(int id, Bundle args) { - Uri uri = args.getParcelable(ARG_URI); - mIsSecret = args.getBoolean(ARG_IS_SECRET, false); - return new CursorLoader(getContext(), uri, - CERTS_PROJECTION, null, null, null); - } - - @Override - public void onLoadFinished(Loader loader, Cursor data) { - - if (data == null || !data.moveToFirst()) { - return; - } - - // TODO support external certificates - Long certTime = null; - while (!data.isAfterLast()) { - - int verified = data.getInt(INDEX_VERIFIED); - long creation = data.getLong(INDEX_CREATION) * 1000; - - if (verified == Certs.VERIFIED_SECRET) { - if (certTime == null || certTime > creation) { - certTime = creation; - } - } - - data.moveToNext(); - } - - if (certTime != null) { + public void setData(CertDetails certDetails, boolean isSecret) { + if (certDetails != null) { CharSequence relativeTimeStr = DateUtils - .getRelativeTimeSpanString(certTime, System.currentTimeMillis(), 0, DateUtils.FORMAT_ABBREV_ALL); - if (mIsSecret) { + .getRelativeTimeSpanString(certDetails.creation(), System.currentTimeMillis(), 0, DateUtils.FORMAT_ABBREV_ALL); + if (isSecret) { vCollapsed.setText("You created this identity " + relativeTimeStr + "."); } else { vCollapsed.setText("You verified and confirmed this identity " + relativeTimeStr + "."); @@ -150,9 +91,4 @@ public class CertListWidget extends ViewAnimator } - @Override - public void onLoaderReset(Loader loader) { - setVisibility(View.GONE); - } - } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/CertifyKeySpinner.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/CertifyKeySpinner.java deleted file mode 100644 index 3284448a0..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/CertifyKeySpinner.java +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Copyright (C) 2017 Schürmann & Breitmoser GbR - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sufficientlysecure.keychain.ui.widget; - -import android.content.Context; -import android.database.Cursor; -import android.net.Uri; -import android.os.Bundle; -import android.support.annotation.StringRes; -import android.support.v4.content.CursorLoader; -import android.support.v4.content.Loader; -import android.util.AttributeSet; - -import org.sufficientlysecure.keychain.Constants; -import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.provider.KeychainContract; -import org.sufficientlysecure.keychain.provider.KeychainDatabase; -import org.sufficientlysecure.keychain.ui.adapter.KeyAdapter; - -public class CertifyKeySpinner extends KeySpinner { - private long mHiddenMasterKeyId = Constants.key.none; - private boolean mIsSingle; - - public CertifyKeySpinner(Context context) { - super(context); - } - - public CertifyKeySpinner(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public CertifyKeySpinner(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - } - - public void setHiddenMasterKeyId(long hiddenMasterKeyId) { - this.mHiddenMasterKeyId = hiddenMasterKeyId; - reload(); - } - - @Override - public Loader onCreateLoader(int loaderId, Bundle data) { - // This is called when a new Loader needs to be created. This - // sample only has one Loader, so we don't care about the ID. - Uri baseUri = KeychainContract.KeyRings.buildUnifiedKeyRingsUri(); - - String[] projection = KeyAdapter.getProjectionWith(new String[] { - KeychainContract.KeyRings.HAS_CERTIFY_SECRET, - }); - - String where = KeychainContract.KeyRings.HAS_ANY_SECRET + " = 1 AND " - + KeychainDatabase.Tables.KEYS + "." + KeychainContract.KeyRings.MASTER_KEY_ID - + " != " + mHiddenMasterKeyId; - - // Now create and return a CursorLoader that will take care of - // creating a Cursor for the data being displayed. - return new CursorLoader(getContext(), baseUri, projection, where, null, null); - } - - private int mIndexHasCertify; - - @Override - public void onLoadFinished(Loader loader, Cursor data) { - super.onLoadFinished(loader, data); - - if (loader.getId() == LOADER_ID) { - mIndexHasCertify = data.getColumnIndex(KeychainContract.KeyRings.HAS_CERTIFY_SECRET); - - // If: - // - no key has been pre-selected (e.g. by SageSlinger) - // - there are actually keys (not just "none" entry) - // Then: - // - select key that is capable of certifying, but only if there is only one key capable of it - if (mPreSelectedKeyId == Constants.key.none && mAdapter.getCount() > 1) { - // preselect if key can certify - int selection = -1; - while (data.moveToNext()) { - if (!data.isNull(mIndexHasCertify)) { - if (selection == -1) { - selection = data.getPosition() + 1; - mIsSingle = true; - } else { - // if selection is already set, we have more than one certify key! - // get back to "none"! - mIsSingle = false; - selection = 0; - } - } - } - setSelection(selection); - } - } - } - - public boolean isSingleEntry() { - return mIsSingle && getSelectedItemPosition() != 0; - } - - @Override - boolean isItemEnabled(Cursor cursor) { - if (cursor.getInt(KeyAdapter.INDEX_IS_REVOKED) != 0) { - return false; - } - if (cursor.getInt(KeyAdapter.INDEX_IS_EXPIRED) != 0) { - return false; - } - if (cursor.isNull(mIndexHasCertify)) { - return false; - } - - // valid key - return true; - } - - @Override - public @StringRes int getNoneString() { - return R.string.choice_select_cert; - } - - -} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/EncryptKeyCompletionView.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/EncryptKeyCompletionView.java deleted file mode 100644 index 95b68a9f6..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/EncryptKeyCompletionView.java +++ /dev/null @@ -1,179 +0,0 @@ -/* - * Copyright (C) 2017 Schürmann & Breitmoser GbR - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sufficientlysecure.keychain.ui.widget; - - -import android.content.Context; -import android.database.Cursor; -import android.graphics.Color; -import android.graphics.Rect; -import android.net.Uri; -import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.v4.app.FragmentActivity; -import android.support.v4.app.LoaderManager; -import android.support.v4.app.LoaderManager.LoaderCallbacks; -import android.support.v4.content.CursorLoader; -import android.support.v4.content.Loader; -import android.text.TextUtils; -import android.util.AttributeSet; -import android.view.LayoutInflater; -import android.view.View; -import android.view.inputmethod.InputMethodManager; -import android.widget.TextView; - -import com.tokenautocomplete.TokenCompleteTextView; -import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.provider.KeychainContract; -import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; -import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables; -import org.sufficientlysecure.keychain.ui.adapter.KeyAdapter; -import org.sufficientlysecure.keychain.ui.adapter.KeyAdapter.KeyItem; -import timber.log.Timber; - - -public class EncryptKeyCompletionView extends TokenCompleteTextView - implements LoaderCallbacks { - - public static final String ARG_QUERY = "query"; - - private KeyAdapter mAdapter; - private LoaderManager mLoaderManager; - - public EncryptKeyCompletionView(Context context) { - super(context); - initView(); - } - - public EncryptKeyCompletionView(Context context, AttributeSet attrs) { - super(context, attrs); - initView(); - } - - public EncryptKeyCompletionView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - initView(); - } - - private void initView() { - allowDuplicates(false); - - mAdapter = new KeyAdapter(getContext(), null, 0); - setAdapter(mAdapter); - } - - @Override - protected View getViewForObject(KeyItem keyItem) { - LayoutInflater l = LayoutInflater.from(getContext()); - View view = l.inflate(R.layout.recipient_box_entry, null); - ((TextView) view.findViewById(android.R.id.text1)).setText(keyItem.getReadableName()); - - if (keyItem.mIsRevoked || !keyItem.mHasEncrypt || keyItem.mIsExpired) { - ((TextView) view.findViewById(android.R.id.text1)).setTextColor(Color.RED); - } - - return view; - } - - @Override - protected KeyItem defaultObject(String completionText) { - return null; - } - - @Override - protected void onAttachedToWindow() { - super.onAttachedToWindow(); - - if (getContext() instanceof FragmentActivity) { - mLoaderManager = ((FragmentActivity) getContext()).getSupportLoaderManager(); - } else { - Timber.e("EncryptKeyCompletionView must be attached to a FragmentActivity, this is " + - getContext().getClass()); - } - } - - @Override - protected void onDetachedFromWindow() { - super.onDetachedFromWindow(); - mLoaderManager = null; - } - - @Override - public Loader onCreateLoader(int id, Bundle args) { - // These are the rows that we will retrieve. - Uri baseUri = KeyRings.buildUnifiedKeyRingsUri(); - - 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"; - - String query = args.getString(ARG_QUERY); - mAdapter.setSearchQuery(query); - - where += " AND " + KeyRings.USER_ID + " LIKE ?"; - - return new CursorLoader(getContext(), baseUri, projection, where, - new String[]{"%" + query + "%"}, null); - - } - - @Override - public void onLoadFinished(Loader loader, Cursor data) { - mAdapter.swapCursor(data); - } - - @Override - public void onLoaderReset(Loader loader) { - mAdapter.swapCursor(null); - } - - @Override - public void showDropDown() { - if (mAdapter == null || mAdapter.getCursor() == null || mAdapter.getCursor().isClosed()) { - return; - } - super.showDropDown(); - } - - @Override - public void onFocusChanged(boolean hasFocus, int direction, Rect previous) { - super.onFocusChanged(hasFocus, direction, previous); - if (hasFocus) { - ((InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE)) - .showSoftInput(this, InputMethodManager.SHOW_IMPLICIT); - } - } - - @Override - protected void performFiltering(@NonNull CharSequence text, int start, int end, int keyCode) { -// super.performFiltering(text, start, end, keyCode); - String query = text.subSequence(start, end).toString(); - if (TextUtils.isEmpty(query) || query.length() < 2) { - mAdapter.swapCursor(null); - return; - } - Bundle args = new Bundle(); - args.putString(ARG_QUERY, query); - mLoaderManager.restartLoader(0, args, this); - } - -} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/KeyChoiceSpinnerAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/KeyChoiceSpinnerAdapter.java new file mode 100644 index 000000000..b41b7adc8 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/KeyChoiceSpinnerAdapter.java @@ -0,0 +1,146 @@ +package org.sufficientlysecure.keychain.ui.widget; + + +import android.content.Context; +import android.support.annotation.StringRes; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.ImageView; +import android.widget.TextView; + +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.model.SubKey.UnifiedKeyInfo; +import org.sufficientlysecure.keychain.ui.util.KeyInfoFormatter; + +import java.util.Arrays; +import java.util.List; + + +class KeyChoiceSpinnerAdapter extends BaseAdapter { + private final LayoutInflater layoutInflater; + private final KeyInfoFormatter keyInfoFormatter; + + private Integer noneItemString; + private List data; + + KeyChoiceSpinnerAdapter(Context context) { + super(); + + layoutInflater = LayoutInflater.from(context); + keyInfoFormatter = new KeyInfoFormatter(context); + } + + public void setData(List data) { + this.data = data; + notifyDataSetChanged(); + } + + public void setNoneItemString(@StringRes Integer noneItemString) { + this.noneItemString = noneItemString; + notifyDataSetChanged(); + } + + public boolean hasNoneItem() { + return noneItemString != null; + } + + @Override + public int getCount() { + return (data != null ? data.size() : 0) + (noneItemString != null ? 1 : 0); + } + + public boolean isSingleEntry() { + return data != null && data.size() == 1; + } + + @Override + public UnifiedKeyInfo getItem(int position) { + if (noneItemString != null) { + if (position == 0) { + return null; + } + position -= 1; + } + return data.get(position); + } + + @Override + public long getItemId(int position) { + if (noneItemString != null) { + if (position == 0) { + return 0; + } + position -= 1; + } + return data != null ? data.get(position).master_key_id() : 0; + } + + @Override + public boolean hasStableIds() { + return true; + } + + @Override + public int getItemViewType(int position) { + if (noneItemString != null && position == 0) { + return 1; + } else { + return super.getItemViewType(position); + } + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + if (noneItemString != null) { + if (position == 0) { + if (convertView != null && convertView.getTag() == null) { + return convertView; + } else { + View view = layoutInflater.inflate(R.layout.keyspinner_item_none, parent, false); + view.findViewById(R.id.keyspinner_key_name).setText(noneItemString); + return view; + } + } + } + + View view; + KeyChoiceViewHolder viewHolder; + if (convertView == null || !(convertView.getTag() instanceof KeyChoiceViewHolder)) { + view = layoutInflater.inflate(R.layout.key_list_item, parent, false); + viewHolder = new KeyChoiceViewHolder(view); + view.setTag(viewHolder); + } else { + view = convertView; + viewHolder = ((KeyChoiceViewHolder) view.getTag()); + } + + UnifiedKeyInfo keyInfo = getItem(position); + viewHolder.bind(keyInfo, isEnabled(position)); + + return view; + } + + public class KeyChoiceViewHolder { + private TextView mMainUserId; + private TextView mMainUserIdRest; + private TextView mCreationDate; + private ImageView mStatus; + + KeyChoiceViewHolder(View view) { + mMainUserId = view.findViewById(R.id.key_list_item_name); + mMainUserIdRest = view.findViewById(R.id.key_list_item_email); + mStatus = view.findViewById(R.id.key_list_item_status_icon); + mCreationDate = view.findViewById(R.id.key_list_item_creation); + } + + public void bind(UnifiedKeyInfo keyInfo, boolean enabled) { + keyInfoFormatter.setKeyInfo(keyInfo); + keyInfoFormatter.formatUserId(mMainUserId, mMainUserIdRest); + keyInfoFormatter.formatCreationDate(mCreationDate); + keyInfoFormatter.formatStatusIcon(mStatus); + keyInfoFormatter.greyInvalidKeys(Arrays.asList(mMainUserId, mMainUserIdRest, mCreationDate)); + } + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/KeySpinner.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/KeySpinner.java index d57f78d19..8ac45c6d8 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/KeySpinner.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/KeySpinner.java @@ -18,39 +18,22 @@ package org.sufficientlysecure.keychain.ui.widget; +import java.util.List; + import android.content.Context; -import android.database.Cursor; import android.os.Bundle; import android.os.Parcelable; import android.support.annotation.NonNull; import android.support.annotation.StringRes; -import android.support.v4.app.FragmentActivity; -import android.support.v4.app.LoaderManager; -import android.support.v4.content.Loader; import android.support.v7.widget.AppCompatSpinner; import android.util.AttributeSet; -import android.view.LayoutInflater; import android.view.View; -import android.view.ViewGroup; import android.widget.AdapterView; -import android.widget.BaseAdapter; -import android.widget.SpinnerAdapter; -import android.widget.TextView; import org.sufficientlysecure.keychain.Constants; -import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.provider.KeychainContract; -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. - * Related: http://stackoverflow.com/a/27713090 - */ -public abstract class KeySpinner extends AppCompatSpinner implements - LoaderManager.LoaderCallbacks { +import org.sufficientlysecure.keychain.model.SubKey.UnifiedKeyInfo; +public class KeySpinner extends AppCompatSpinner { public static final String ARG_SUPER_STATE = "super_state"; public static final String ARG_KEY_ID = "key_id"; @@ -58,13 +41,10 @@ public abstract class KeySpinner extends AppCompatSpinner implements void onKeyChanged(long masterKeyId); } - protected long mPreSelectedKeyId = Constants.key.none; - protected SelectKeyAdapter mAdapter = new SelectKeyAdapter(); + protected Long preSelectedKeyId; + protected KeyChoiceSpinnerAdapter spinnerAdapter; protected OnKeyChangedListener mListener; - // this shall note collide with other loaders inside the activity - protected int LOADER_ID = 2343; - public KeySpinner(Context context) { super(context); initView(); @@ -81,7 +61,9 @@ public abstract class KeySpinner extends AppCompatSpinner implements } private void initView() { - setAdapter(mAdapter); + spinnerAdapter = new KeyChoiceSpinnerAdapter(getContext()); + + setAdapter(spinnerAdapter); super.setOnItemSelectedListener(new OnItemSelectedListener() { @Override public void onItemSelected(AdapterView parent, View view, int position, long id) { @@ -100,6 +82,10 @@ public abstract class KeySpinner extends AppCompatSpinner implements }); } + public void setShowNone(@StringRes Integer noneStringRes) { + spinnerAdapter.setNoneItemString(noneStringRes); + } + @Override public void setOnItemSelectedListener(OnItemSelectedListener listener) { throw new UnsupportedOperationException(); @@ -109,32 +95,32 @@ public abstract class KeySpinner extends AppCompatSpinner implements mListener = listener; } - @Override - protected void onAttachedToWindow() { - super.onAttachedToWindow(); - reload(); + public void setData(List keyInfos) { + spinnerAdapter.setData(keyInfos); + maybeSelectPreselection(keyInfos); } - public void reload() { - if (getContext() instanceof FragmentActivity) { - ((FragmentActivity) getContext()).getSupportLoaderManager().restartLoader(LOADER_ID, null, this); - } else { - // ignore, this happens during preview! we use fragmentactivities everywhere either way + private void maybeSelectPreselection(List keyInfos) { + if (spinnerAdapter.hasNoneItem() && keyInfos.size() == 1) { + setSelection(1); + return; + } + if (preSelectedKeyId == null) { + return; + } + for (UnifiedKeyInfo keyInfo : keyInfos) { + if (keyInfo.master_key_id() == preSelectedKeyId) { + int position = keyInfos.indexOf(keyInfo); + if (spinnerAdapter.hasNoneItem()) { + position += 1; + } + setSelection(position); + } } } - @Override - public void onLoadFinished(Loader loader, Cursor data) { - if (loader.getId() == LOADER_ID) { - mAdapter.swapCursor(data); - } - } - - @Override - public void onLoaderReset(Loader loader) { - if (loader.getId() == LOADER_ID) { - mAdapter.swapCursor(null); - } + public boolean isSingleEntry() { + return spinnerAdapter.isSingleEntry(); } public long getSelectedKeyId() { @@ -143,108 +129,21 @@ public abstract class KeySpinner extends AppCompatSpinner implements } public long getSelectedKeyId(Object item) { - if (item instanceof KeyItem) { - return ((KeyItem) item).mKeyId; + if (item instanceof UnifiedKeyInfo) { + return ((UnifiedKeyInfo) item).master_key_id(); } return Constants.key.none; } public void setPreSelectedKeyId(long selectedKeyId) { - mPreSelectedKeyId = selectedKeyId; - } - - protected class SelectKeyAdapter extends BaseAdapter implements SpinnerAdapter { - private KeyAdapter inner; - private int mIndexMasterKeyId; - - public SelectKeyAdapter() { - inner = new KeyAdapter(getContext(), null, 0) { - - @Override - public boolean isEnabled(Cursor cursor) { - return KeySpinner.this.isItemEnabled(cursor); - } - - }; - } - - public Cursor swapCursor(Cursor newCursor) { - if (newCursor == null) return inner.swapCursor(null); - - mIndexMasterKeyId = newCursor.getColumnIndex(KeychainContract.KeyRings.MASTER_KEY_ID); - - Cursor oldCursor = inner.swapCursor(newCursor); - - // pre-select key if mPreSelectedKeyId is given - if (mPreSelectedKeyId != Constants.key.none && newCursor.moveToFirst()) { - do { - if (newCursor.getLong(mIndexMasterKeyId) == mPreSelectedKeyId) { - setSelection(newCursor.getPosition() +1); - } - } while (newCursor.moveToNext()); - } - return oldCursor; - } - - @Override - public int getCount() { - return inner.getCount() +1; - } - - @Override - public KeyItem getItem(int position) { - if (position == 0) { - return null; - } - return inner.getItem(position -1); - } - - @Override - public long getItemId(int position) { - if (position == 0) { - return Constants.key.none; - } - return inner.getItemId(position -1); - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - - // Unfortunately, SpinnerAdapter does not support multiple view - // types. For this reason, we throw away convertViews of a bad - // type. This is sort of a hack, but since the number of elements - // we deal with in KeySpinners is usually very small (number of - // secret keys), this is the easiest solution. (I'm sorry.) - 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; - } - } - - if (position > 0) { - return inner.getView(position -1, convertView, parent); - } - - View view = convertView != null ? convertView : - LayoutInflater.from(getContext()).inflate( - R.layout.keyspinner_item_none, parent, false); - ((TextView) view.findViewById(R.id.keyspinner_key_name)).setText(getNoneString()); - return view; - } - - } - - boolean isItemEnabled(Cursor cursor) { - return true; + preSelectedKeyId = selectedKeyId; } @Override public void onRestoreInstanceState(Parcelable state) { Bundle bundle = (Bundle) state; - mPreSelectedKeyId = bundle.getLong(ARG_KEY_ID); + preSelectedKeyId = bundle.getLong(ARG_KEY_ID); // restore super state super.onRestoreInstanceState(bundle.getParcelable(ARG_SUPER_STATE)); @@ -262,9 +161,4 @@ public abstract class KeySpinner extends AppCompatSpinner implements bundle.putLong(ARG_KEY_ID, getSelectedKeyId()); return bundle; } - - public @StringRes int getNoneString() { - return R.string.cert_none; - } - } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/SignKeySpinner.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/SignKeySpinner.java deleted file mode 100644 index 55b3537be..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/SignKeySpinner.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright (C) 2017 Schürmann & Breitmoser GbR - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sufficientlysecure.keychain.ui.widget; - - -import android.content.Context; -import android.database.Cursor; -import android.net.Uri; -import android.os.Bundle; -import android.support.v4.content.CursorLoader; -import android.support.v4.content.Loader; -import android.util.AttributeSet; - -import org.sufficientlysecure.keychain.provider.KeychainContract; -import org.sufficientlysecure.keychain.ui.adapter.KeyAdapter; - -public class SignKeySpinner extends KeySpinner { - public SignKeySpinner(Context context) { - super(context); - } - - public SignKeySpinner(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public SignKeySpinner(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - } - - @Override - public Loader onCreateLoader(int loaderId, Bundle data) { - // This is called when a new Loader needs to be created. This - // sample only has one Loader, so we don't care about the ID. - Uri baseUri = KeychainContract.KeyRings.buildUnifiedKeyRingsUri(); - - String[] projection = KeyAdapter.getProjectionWith(new String[] { - KeychainContract.KeyRings.HAS_SIGN_SECRET, - }); - - String where = KeychainContract.KeyRings.HAS_ANY_SECRET + " = 1"; - - // Now create and return a CursorLoader that will take care of - // creating a Cursor for the data being displayed. - return new CursorLoader(getContext(), baseUri, projection, where, null, null); - } - - private int mIndexHasSign; - - @Override - public void onLoadFinished(Loader loader, Cursor data) { - super.onLoadFinished(loader, data); - - if (loader.getId() == LOADER_ID) { - mIndexHasSign = data.getColumnIndex(KeychainContract.KeyRings.HAS_SIGN_SECRET); - } - } - - @Override - boolean isItemEnabled(Cursor cursor) { - if (cursor.getInt(KeyAdapter.INDEX_IS_REVOKED) != 0) { - return false; - } - if (cursor.getInt(KeyAdapter.INDEX_IS_EXPIRED) != 0) { - return false; - } - if (cursor.getInt(KeyAdapter.INDEX_IS_SECURE) == 0) { - return false; - } - if (cursor.isNull(mIndexHasSign)) { - return false; - } - - // valid key - return true; - } - -} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ContactHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ContactHelper.java index 40bc57519..64f26fe7f 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ContactHelper.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ContactHelper.java @@ -20,11 +20,9 @@ package org.sufficientlysecure.keychain.util; import java.io.InputStream; import java.util.ArrayList; -import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; -import java.util.Map; import java.util.Set; import java.util.regex.Matcher; @@ -42,20 +40,20 @@ import android.graphics.BitmapFactory; import android.net.Uri; import android.os.Build; import android.provider.ContactsContract; +import android.provider.ContactsContract.CommonDataKinds.Email; +import android.provider.ContactsContract.Data; import android.support.v4.content.ContextCompat; import android.util.Patterns; -import org.openintents.openpgp.util.OpenPgpUtils; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.pgp.KeyRing; -import org.sufficientlysecure.keychain.provider.KeychainContract; -import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets; +import org.sufficientlysecure.keychain.model.SubKey.UnifiedKeyInfo; +import org.sufficientlysecure.keychain.model.UserPacket.UserId; +import org.sufficientlysecure.keychain.daos.KeyRepository; import timber.log.Timber; public class ContactHelper { - - private static final Map photoCache = new HashMap<>(); + private final KeyRepository keyRepository; private Context mContext; private ContentResolver mContentResolver; @@ -63,6 +61,7 @@ public class ContactHelper { public ContactHelper(Context context) { mContext = context; mContentResolver = context.getContentResolver(); + keyRepository = KeyRepository.create(context); } public List getPossibleUserEmails() { @@ -319,7 +318,7 @@ public class ContactHelper { return new ArrayList<>(names); } - public Uri dataUriFromContactUri(Uri contactUri) { + public Long masterKeyIdFromContactsDataUri(Uri contactUri) { if (!isContactsPermissionGranted()) { return null; } @@ -329,7 +328,7 @@ public class ContactHelper { if (contactMasterKey != null) { try { if (contactMasterKey.moveToNext()) { - return KeychainContract.KeyRings.buildGenericKeyRingUri(contactMasterKey.getLong(0)); + return contactMasterKey.getLong(0); } } finally { contactMasterKey.close(); @@ -393,16 +392,6 @@ public class ContactHelper { return contactName; } - private Bitmap getCachedPhotoByMasterKeyId(long masterKeyId) { - if (masterKeyId == -1) { - return null; - } - if (!photoCache.containsKey(masterKeyId)) { - photoCache.put(masterKeyId, loadPhotoByMasterKeyId(masterKeyId, false)); - } - return photoCache.get(masterKeyId); - } - public Bitmap loadPhotoByMasterKeyId(long masterKeyId, boolean highRes) { if (!isContactsPermissionGranted()) { return null; @@ -446,29 +435,6 @@ public class ContactHelper { return BitmapFactory.decodeStream(photoInputStream); } - public static final String[] KEYS_TO_CONTACT_PROJECTION = new String[]{ - KeychainContract.KeyRings.MASTER_KEY_ID, - KeychainContract.KeyRings.USER_ID, - KeychainContract.KeyRings.IS_EXPIRED, - KeychainContract.KeyRings.IS_REVOKED, - KeychainContract.KeyRings.VERIFIED, - KeychainContract.KeyRings.HAS_SECRET, - KeychainContract.KeyRings.HAS_ANY_SECRET, - KeychainContract.KeyRings.NAME, - KeychainContract.KeyRings.EMAIL, - KeychainContract.KeyRings.COMMENT }; - - public static final int INDEX_MASTER_KEY_ID = 0; - public static final int INDEX_USER_ID = 1; - public static final int INDEX_IS_EXPIRED = 2; - public static final int INDEX_IS_REVOKED = 3; - public static final int INDEX_VERIFIED = 4; - public static final int INDEX_HAS_SECRET = 5; - public static final int INDEX_HAS_ANY_SECRET = 6; - public static final int INDEX_NAME = 7; - public static final int INDEX_EMAIL = 8; - public static final int INDEX_COMMENT = 9; - /** * Write/Update the current OpenKeychain keys to the contact db */ @@ -503,35 +469,28 @@ public class ContactHelper { Set deletedKeys = getRawContactMasterKeyIds(); // Load all public Keys from OK - // TODO: figure out why using selectionArgs does not work in this case - Cursor cursor = mContentResolver.query(KeychainContract.KeyRings.buildUnifiedKeyRingsUri(), - KEYS_TO_CONTACT_PROJECTION, - KeychainContract.KeyRings.HAS_ANY_SECRET + "=0", - null, null); + for (UnifiedKeyInfo keyInfo : keyRepository.getAllUnifiedKeyInfo()) { + if (keyInfo.has_any_secret()) { + continue; + } - if (cursor != null) { - while (cursor.moveToNext()) { - long masterKeyId = cursor.getLong(INDEX_MASTER_KEY_ID); - String name = cursor.getString(INDEX_NAME); - boolean isExpired = cursor.getInt(INDEX_IS_EXPIRED) != 0; - boolean isRevoked = cursor.getInt(INDEX_IS_REVOKED) > 0; - boolean isVerified = cursor.getInt(INDEX_VERIFIED) > 0; + long masterKeyId = keyInfo.master_key_id(); + String name = keyInfo.name(); - Timber.d("masterKeyId: " + masterKeyId); + deletedKeys.remove(masterKeyId); - deletedKeys.remove(masterKeyId); + ArrayList ops = new ArrayList<>(); - ArrayList ops = new ArrayList<>(); - - // Do not store expired or revoked or unverified keys in contact db - and - // remove them if they already exist. Secret keys do not reach this point - if (isExpired || isRevoked || !isVerified) { - Timber.d("Expired or revoked or unverified: Deleting masterKeyId " - + masterKeyId); - if (masterKeyId != -1) { - deleteRawContactByMasterKeyId(masterKeyId); - } - } else if (name != null) { + // Do not store expired or revoked or unverified keys in contact db - and + // remove them if they already exist. Secret keys do not reach this point + if (keyInfo.is_expired() || keyInfo.is_revoked() || !keyInfo.is_verified()) { + Timber.d("Expired or revoked or unverified: Deleting masterKeyId " + + masterKeyId); + if (masterKeyId != -1) { + deleteRawContactByMasterKeyId(masterKeyId); + } + } else { + if (name != null) { // get raw contact to this master key id long rawContactId = findRawContactId(masterKeyId); @@ -556,7 +515,6 @@ public class ContactHelper { } } } - cursor.close(); } // Delete master key ids that are no longer present in OK @@ -578,40 +536,29 @@ public class ContactHelper { // get all keys which have associated secret keys // TODO: figure out why using selectionArgs does not work in this case - Cursor cursor = mContentResolver.query(KeychainContract.KeyRings.buildUnifiedKeyRingsUri(), - KEYS_TO_CONTACT_PROJECTION, - KeychainContract.KeyRings.HAS_ANY_SECRET + "!=0", - null, null); - if (cursor != null) try { - while (cursor.moveToNext()) { - long masterKeyId = cursor.getLong(INDEX_MASTER_KEY_ID); - boolean isExpired = cursor.getInt(INDEX_IS_EXPIRED) != 0; - boolean isRevoked = cursor.getInt(INDEX_IS_REVOKED) > 0; - String name = cursor.getString(INDEX_NAME); + for (UnifiedKeyInfo keyInfo : keyRepository.getAllUnifiedKeyInfoWithSecret()) { + long masterKeyId = keyInfo.master_key_id(); - if (!isExpired && !isRevoked && name != null) { - // if expired or revoked will not be removed from keysToDelete or inserted - // into main profile ("me" contact) - boolean existsInMainProfile = keysToDelete.remove(masterKeyId); - if (!existsInMainProfile) { - long rawContactId = -1;//new raw contact + if (!keyInfo.is_expired() && !keyInfo.is_revoked() && keyInfo.name() != null) { + // if expired or revoked will not be removed from keysToDelete or inserted + // into main profile ("me" contact) + boolean existsInMainProfile = keysToDelete.remove(masterKeyId); + if (!existsInMainProfile) { + long rawContactId = -1;//new raw contact - Timber.d("masterKeyId with secret " + masterKeyId); + Timber.d("masterKeyId with secret " + masterKeyId); - ArrayList ops = new ArrayList<>(); - insertMainProfileRawContact(ops, masterKeyId); - writeContactKey(ops, rawContactId, masterKeyId, name); + ArrayList ops = new ArrayList<>(); + insertMainProfileRawContact(ops, masterKeyId); + writeContactKey(ops, rawContactId, masterKeyId, keyInfo.name()); - try { - mContentResolver.applyBatch(ContactsContract.AUTHORITY, ops); - } catch (Exception e) { - Timber.w(e); - } + try { + mContentResolver.applyBatch(ContactsContract.AUTHORITY, ops); + } catch (Exception e) { + Timber.w(e); } } } - } finally { - cursor.close(); } for (long masterKeyId : keysToDelete) { @@ -838,29 +785,17 @@ public class ContactHelper { */ private void writeContactEmail(ArrayList ops, long rawContactId, long masterKeyId) { - ops.add(selectByRawContactAndItemType( - ContentProviderOperation.newDelete(ContactsContract.Data.CONTENT_URI), - rawContactId, ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE).build()); - Cursor ids = mContentResolver.query(UserPackets.buildUserIdsUri(masterKeyId), - new String[]{ - UserPackets.USER_ID - }, - UserPackets.IS_REVOKED + "=0", - null, null); - if (ids != null) { - while (ids.moveToNext()) { - OpenPgpUtils.UserId userId = KeyRing.splitUserId(ids.getString(0)); - if (userId.email != null) { - ops.add(referenceRawContact( - ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI), - rawContactId) - .withValue(ContactsContract.Data.MIMETYPE, - ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE) - .withValue(ContactsContract.CommonDataKinds.Email.DATA, userId.email) - .build()); - } - } - ids.close(); + ContentProviderOperation deleteOp = selectByRawContactAndItemType( + ContentProviderOperation.newDelete(Data.CONTENT_URI), rawContactId, Email.CONTENT_ITEM_TYPE).build(); + ops.add(deleteOp); + + for (UserId userId : keyRepository.getUserIds(masterKeyId)) { + ContentProviderOperation insertOp = + referenceRawContact(ContentProviderOperation.newInsert(Data.CONTENT_URI), rawContactId) + .withValue(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE) + .withValue(Email.DATA, userId.email()) + .build(); + ops.add(insertOp); } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/DatabaseUtil.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/DatabaseUtil.java index dd21accd6..d9cffe702 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/DatabaseUtil.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/DatabaseUtil.java @@ -17,8 +17,9 @@ package org.sufficientlysecure.keychain.util; + +import android.arch.persistence.db.SupportSQLiteDatabase; import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; import android.text.TextUtils; import timber.log.Timber; @@ -56,8 +57,8 @@ public class DatabaseUtil { return result; } - public static void explainQuery(SQLiteDatabase db, String sql) { - Cursor explainCursor = db.rawQuery("EXPLAIN QUERY PLAN " + sql, new String[0]); + public static void explainQuery(SupportSQLiteDatabase db, String sql) { + Cursor explainCursor = db.query("EXPLAIN QUERY PLAN " + sql, new String[0]); // this is a debugging feature, we can be a little careless explainCursor.moveToFirst(); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/FilterCursorWrapper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/FilterCursorWrapper.java deleted file mode 100644 index 583c294ee..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/FilterCursorWrapper.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright (C) 2017 Schürmann & Breitmoser GbR - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sufficientlysecure.keychain.util; - - -import android.database.Cursor; -import android.database.CursorWrapper; - -public abstract class FilterCursorWrapper extends CursorWrapper { - private int[] mIndex; - private int mCount = 0; - private int mPos = 0; - - public abstract boolean isVisible(Cursor cursor); - - public FilterCursorWrapper(Cursor cursor) { - super(cursor); - mCount = super.getCount(); - mIndex = new int[mCount]; - for (int i = 0; i < mCount; i++) { - super.moveToPosition(i); - if (isVisible(cursor)) { - mIndex[mPos++] = i; - } - } - mCount = mPos; - mPos = 0; - super.moveToFirst(); - } - - @Override - public boolean move(int offset) { - return this.moveToPosition(mPos + offset); - } - - @Override - public boolean moveToNext() { - return this.moveToPosition(mPos + 1); - } - - @Override - public boolean moveToPrevious() { - return this.moveToPosition(mPos - 1); - } - - @Override - public boolean moveToFirst() { - return this.moveToPosition(0); - } - - @Override - public boolean moveToLast() { - return this.moveToPosition(mCount - 1); - } - - @Override - public boolean moveToPosition(int position) { - if (position >= mCount || position < 0) { - return false; - } - return super.moveToPosition(mIndex[position]); - } - - @Override - public int getCount() { - return mCount; - } - - public int getHiddenCount() { - return super.getCount() - mCount; - } - - @Override - public int getPosition() { - return mPos; - } - -} \ No newline at end of file diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/KeyUpdateHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/KeyUpdateHelper.java deleted file mode 100644 index cb294b6c1..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/KeyUpdateHelper.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright (C) 2017 Schürmann & Breitmoser GbR - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sufficientlysecure.keychain.util; - -public class KeyUpdateHelper { - - /* - - public void updateAllKeys(Context context, KeychainIntentServiceHandler finishedHandler) { - UpdateTask updateTask = new UpdateTask(context, finishedHandler); - updateTask.execute(); - } - - private class UpdateTask extends AsyncTask { - private Context mContext; - private KeychainIntentServiceHandler mHandler; - - public UpdateTask(Context context, KeychainIntentServiceHandler handler) { - this.mContext = context; - this.mHandler = handler; - } - - @Override - protected Void doInBackground(Void... voids) { - ProviderHelper providerHelper = new ProviderHelper(mContext); - List keys = new ArrayList(); - String[] servers = Preferences.getPreferences(mContext).getKeyServers(); - - if (servers != null && servers.length > 0) { - // Load all the fingerprints in the database and prepare to import them - for (String fprint : providerHelper.getAllFingerprints(KeychainContract.KeyRings.buildUnifiedKeyRingsUri())) { - ImportKeysListEntry key = new ImportKeysListEntry(); - key.setFingerprintHex(fprint); - key.addOrigin(servers[0]); - keys.add(key); - } - - // Start the service and update the keys - Intent importIntent = new Intent(mContext, KeychainService.class); - importIntent.setAction(KeychainService.ACTION_DOWNLOAD_AND_IMPORT_KEYS); - - Bundle importData = new Bundle(); - importData.putParcelableArrayList(KeychainService.DOWNLOAD_KEY_LIST, - new ArrayList(keys)); - importIntent.putExtra(KeychainService.EXTRA_SERVICE_INTENT, importData); - - importIntent.putExtra(KeychainService.EXTRA_MESSENGER, new Messenger(mHandler)); - - mContext.startService(importIntent); - } - return null; - } - } - */ - -} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Preferences.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Preferences.java index a44c101fb..38773c116 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Preferences.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Preferences.java @@ -18,27 +18,24 @@ package org.sufficientlysecure.keychain.util; -import android.accounts.Account; +import java.net.Proxy; +import java.util.ArrayList; +import java.util.ListIterator; + import android.annotation.SuppressLint; -import android.content.ContentResolver; import android.content.Context; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.os.Parcelable; import android.preference.PreferenceManager; import android.support.annotation.Nullable; + import com.google.auto.value.AutoValue; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.Constants.Pref; -import org.sufficientlysecure.keychain.KeychainApplication; import org.sufficientlysecure.keychain.keyimport.HkpKeyserverAddress; -import org.sufficientlysecure.keychain.service.KeyserverSyncAdapterService; import timber.log.Timber; -import java.net.Proxy; -import java.util.ArrayList; -import java.util.ListIterator; - /** * Singleton Implementation of a Preference Helper @@ -324,23 +321,6 @@ public class Preferences { } } - /** - * @return true if a periodic sync exists and is set to run automatically, false otherwise - */ - public static boolean getKeyserverSyncEnabled(Context context) { - Account account = KeychainApplication.createAccountIfNecessary(context); - - if (account == null) { - // if the account could not be created for some reason, we can't have a sync - return false; - } - - String authority = Constants.PROVIDER_AUTHORITY; - - return ContentResolver.getSyncAutomatically(account, authority) && - !ContentResolver.getPeriodicSyncs(account, authority).isEmpty(); - } - // cloud prefs public CloudSearchPrefs getCloudSearchPrefs() { @@ -361,6 +341,18 @@ public class Preferences { editor.commit(); } + public boolean isKeyserverSyncEnabled() { + return mSharedPreferences.getBoolean(Pref.SYNC_KEYSERVER, true); + } + + public boolean isKeyserverSyncScheduled() { + return mSharedPreferences.getBoolean(Pref.SYNC_IS_SCHEDULED, false); + } + + public void setKeyserverSyncScheduled(boolean isScheduled) { + mSharedPreferences.edit().putBoolean(Pref.SYNC_IS_SCHEDULED, isScheduled).apply(); + } + @AutoValue public static abstract class CloudSearchPrefs implements Parcelable { public abstract boolean isKeyserverEnabled(); @@ -431,7 +423,7 @@ public class Preferences { editor.commit(); } - public void upgradePreferences(Context context) { + public void upgradePreferences() { int oldVersion = mSharedPreferences.getInt(Constants.Pref.PREF_VERSION, 0); boolean requiresUpgrade = oldVersion < Constants.Defaults.PREF_CURRENT_VERSION; @@ -447,9 +439,7 @@ public class Preferences { case 4: { setTheme(Constants.Pref.Theme.DEFAULT); } - case 5: { - KeyserverSyncAdapterService.enableKeyserverSync(context); - } + case 5: case 6: case 7: { addOnionToSks(); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ResourceUtils.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ResourceUtils.java new file mode 100644 index 000000000..b5432ff4e --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ResourceUtils.java @@ -0,0 +1,29 @@ +package org.sufficientlysecure.keychain.util; + + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; +import android.support.annotation.DrawableRes; +import android.support.annotation.NonNull; +import android.support.v4.content.ContextCompat; + + +public class ResourceUtils { + public static Bitmap getDrawableAsNotificationBitmap(@NonNull Context context, @DrawableRes int iconRes) { + Drawable iconDrawable = ContextCompat.getDrawable(context, iconRes); + if (iconDrawable == null) { + return null; + } + Resources resources = context.getResources(); + int largeIconWidth = resources.getDimensionPixelSize(android.R.dimen.notification_large_icon_width); + int largeIconHeight = resources.getDimensionPixelSize(android.R.dimen.notification_large_icon_height); + Bitmap b = Bitmap.createBitmap(largeIconWidth, largeIconHeight, Bitmap.Config.ARGB_8888); + Canvas c = new Canvas(b); + iconDrawable.setBounds(0, 0, largeIconWidth, largeIconHeight); + iconDrawable.draw(c); + return b; + } +} diff --git a/OpenKeychain/src/main/res/drawable-v21/list_item_ripple.xml b/OpenKeychain/src/main/res/drawable-v21/list_item_ripple.xml index 32d726ac1..d76e28dbb 100644 --- a/OpenKeychain/src/main/res/drawable-v21/list_item_ripple.xml +++ b/OpenKeychain/src/main/res/drawable-v21/list_item_ripple.xml @@ -7,6 +7,7 @@ + \ No newline at end of file diff --git a/OpenKeychain/src/main/res/layout/api_remote_deduplicate.xml b/OpenKeychain/src/main/res/layout/api_remote_deduplicate.xml index 3621f5c5d..b86c109ed 100644 --- a/OpenKeychain/src/main/res/layout/api_remote_deduplicate.xml +++ b/OpenKeychain/src/main/res/layout/api_remote_deduplicate.xml @@ -101,7 +101,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:id="@+id/duplicate_key_list" - tools:listitem="@layout/duplicate_key_item" + tools:listitem="@layout/key_choice_item" tools:layout_height="100dp" /> @@ -132,7 +132,6 @@ android:layout_height="wrap_content" android:text="Select" android:id="@+id/button_select" - android:enabled="false" style="?buttonBarButtonStyle" /> diff --git a/OpenKeychain/src/main/res/layout/api_remote_select_authentication_key.xml b/OpenKeychain/src/main/res/layout/api_remote_select_authentication_key.xml index 72dda13b0..3246f1801 100644 --- a/OpenKeychain/src/main/res/layout/api_remote_select_authentication_key.xml +++ b/OpenKeychain/src/main/res/layout/api_remote_select_authentication_key.xml @@ -85,7 +85,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:id="@+id/authentication_key_list" - tools:listitem="@layout/authentication_key_item" + tools:listitem="@layout/key_choice_item" tools:layout_height="100dp" /> @@ -116,7 +116,6 @@ android:layout_height="wrap_content" android:text="Select" android:id="@+id/button_select" - android:enabled="false" style="?buttonBarButtonStyle" /> diff --git a/OpenKeychain/src/main/res/layout/certify_key_fragment.xml b/OpenKeychain/src/main/res/layout/certify_key_fragment.xml index d8527e408..aa7875ae9 100644 --- a/OpenKeychain/src/main/res/layout/certify_key_fragment.xml +++ b/OpenKeychain/src/main/res/layout/certify_key_fragment.xml @@ -49,7 +49,7 @@ android:paddingRight="8dp" android:gravity="center_vertical" /> - - - - - - - - - - - - - - diff --git a/OpenKeychain/src/main/res/layout/edit_key_fragment.xml b/OpenKeychain/src/main/res/layout/edit_key_fragment.xml index 84686b2ad..10bb4566f 100644 --- a/OpenKeychain/src/main/res/layout/edit_key_fragment.xml +++ b/OpenKeychain/src/main/res/layout/edit_key_fragment.xml @@ -40,16 +40,6 @@ android:text="@string/section_user_ids" android:layout_weight="1" /> - - - - - - - - + > + android:layout_marginRight="16dp" + android:layout_marginLeft="16dp" + android:background="?android:attr/editTextBackground"> + android:outAnimation="@anim/fade_out" + > - + app:hint="@string/label_to" + app:maxRows="2" + /> + + + android:background="?android:attr/editTextBackground"> + android:layout_height="wrap_content"> + android:paddingLeft="8dp" + android:paddingRight="8dp" + /> - + android:orientation="horizontal" + android:background="?selectableItemBackground" + android:minHeight="?listPreferredItemHeight"> - + + + android:orientation="vertical" + android:layout_marginStart="12dp" + android:layout_marginLeft="12dp" + > - @@ -43,18 +43,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:clipToPadding="false" - android:paddingBottom="72dp" - android:paddingLeft="16dp" - android:paddingRight="32dp" - android:paddingStart="16dp" /> - - + android:paddingBottom="72dp" /> @@ -149,6 +138,21 @@ + + diff --git a/OpenKeychain/src/main/res/layout/key_list_header_public.xml b/OpenKeychain/src/main/res/layout/key_list_header_public.xml index 97fb67984..6391a3bb7 100644 --- a/OpenKeychain/src/main/res/layout/key_list_header_public.xml +++ b/OpenKeychain/src/main/res/layout/key_list_header_public.xml @@ -1,16 +1,12 @@ + android:paddingRight="12dp" + android:paddingLeft="12dp"> - - - - - - - - diff --git a/OpenKeychain/src/main/res/layout/linked_id_view_fragment.xml b/OpenKeychain/src/main/res/layout/linked_id_view_fragment.xml index 14db368bf..c121cf295 100644 --- a/OpenKeychain/src/main/res/layout/linked_id_view_fragment.xml +++ b/OpenKeychain/src/main/res/layout/linked_id_view_fragment.xml @@ -132,11 +132,11 @@ android:layout_marginEnd="4dp" android:text="@string/add_keys_my_key" /> - - + diff --git a/OpenKeychain/src/main/res/layout/select_dummy_key_item.xml b/OpenKeychain/src/main/res/layout/select_dummy_key_item.xml index c217d018c..79333a10f 100644 --- a/OpenKeychain/src/main/res/layout/select_dummy_key_item.xml +++ b/OpenKeychain/src/main/res/layout/select_dummy_key_item.xml @@ -2,6 +2,8 @@ \ No newline at end of file + android:minHeight="?listPreferredItemHeight" + /> \ No newline at end of file diff --git a/OpenKeychain/src/main/res/layout/view_cert_activity.xml b/OpenKeychain/src/main/res/layout/view_cert_activity.xml deleted file mode 100644 index f412dbe92..000000000 --- a/OpenKeychain/src/main/res/layout/view_cert_activity.xml +++ /dev/null @@ -1,226 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/OpenKeychain/src/main/res/layout/view_key_adv_certs_fragment.xml b/OpenKeychain/src/main/res/layout/view_key_adv_certs_fragment.xml deleted file mode 100644 index 80bb21e16..000000000 --- a/OpenKeychain/src/main/res/layout/view_key_adv_certs_fragment.xml +++ /dev/null @@ -1,71 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/OpenKeychain/src/main/res/layout/view_key_adv_certs_header.xml b/OpenKeychain/src/main/res/layout/view_key_adv_certs_header.xml deleted file mode 100644 index 3ebb0f855..000000000 --- a/OpenKeychain/src/main/res/layout/view_key_adv_certs_header.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/OpenKeychain/src/main/res/layout/view_key_adv_certs_item.xml b/OpenKeychain/src/main/res/layout/view_key_adv_certs_item.xml deleted file mode 100644 index f1a59a4f9..000000000 --- a/OpenKeychain/src/main/res/layout/view_key_adv_certs_item.xml +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - - - - diff --git a/OpenKeychain/src/main/res/menu/key_list.xml b/OpenKeychain/src/main/res/menu/key_list.xml index d694060b5..776242c64 100644 --- a/OpenKeychain/src/main/res/menu/key_list.xml +++ b/OpenKeychain/src/main/res/menu/key_list.xml @@ -37,6 +37,12 @@ android:visible="false" app:showAsAction="never" /> + + "Saved!" "Not matching" + + Debug Actions + "Encrypt files" "Exchange keys" @@ -727,8 +730,6 @@ "This identity has been confirmed by you." "Not confirmed" "This identity has not been confirmed yet. You cannot be sure if the identity really corresponds to a specific person." - "Invalid" - "Something is wrong with this identity!" "No proof from the Internet on this key’s trustworthiness." @@ -2024,4 +2025,19 @@ Found key data in clipboard! View + + Keyserver update + Updating keys… + Finished updating %d keys + Key %d / %d + Started updating all keys… + Key update successful + An error occurred while updating all keys + + This key cannot be used for encryption! + This key cannot be used for signing! + This key cannot be used because it is insecure! + This key cannot be used because it is revoked! + This key cannot be used because it is expired! + diff --git a/OpenKeychain/src/main/res/values/styles.xml b/OpenKeychain/src/main/res/values/styles.xml index b70093b7a..4743e1eae 100644 --- a/OpenKeychain/src/main/res/values/styles.xml +++ b/OpenKeychain/src/main/res/values/styles.xml @@ -84,4 +84,13 @@ - + + diff --git a/OpenKeychain/src/main/res/xml/shortcuts.xml b/OpenKeychain/src/main/res/xml/shortcuts.xml new file mode 100644 index 000000000..f63add666 --- /dev/null +++ b/OpenKeychain/src/main/res/xml/shortcuts.xml @@ -0,0 +1,25 @@ + + + + + + + + + \ No newline at end of file diff --git a/OpenKeychain/src/main/res/xml/sync_preferences.xml b/OpenKeychain/src/main/res/xml/sync_preferences.xml index 600ccc9e8..6e57376e1 100644 --- a/OpenKeychain/src/main/res/xml/sync_preferences.xml +++ b/OpenKeychain/src/main/res/xml/sync_preferences.xml @@ -1,7 +1,8 @@ ?3 THEN 0 ELSE 1 END) AS key_is_expired_int, + (CASE WHEN gossip_key.expiry IS NULL THEN 0 WHEN gossip_key.expiry > ?3 THEN 0 ELSE 1 END) AS gossip_key_is_expired_int, + ac_key.is_revoked AS key_is_revoked_int, + gossip_key.is_revoked AS gossip_key_is_revoked_int, + EXISTS (SELECT * FROM certs WHERE certs.master_key_id = autocryptPeer.master_key_id AND verified = 1 ) AS key_is_verified_int, + EXISTS (SELECT * FROM certs WHERE certs.master_key_id = autocryptPeer.gossip_master_key_id AND verified = 1 ) AS gossip_key_is_verified_int + FROM autocrypt_peers AS autocryptPeer + LEFT JOIN keys AS ac_key ON (ac_key.master_key_id = autocryptPeer.master_key_id AND ac_key.rank = 0) + LEFT JOIN keys AS gossip_key ON (gossip_key.master_key_id = gossip_master_key_id AND gossip_key.rank = 0) + WHERE package_name = ?1 AND identifier IN ?2; \ No newline at end of file diff --git a/OpenKeychain/src/main/sqldelight/org/sufficientlysecure/keychain/Certs.sq b/OpenKeychain/src/main/sqldelight/org/sufficientlysecure/keychain/Certs.sq new file mode 100644 index 000000000..b35ac0405 --- /dev/null +++ b/OpenKeychain/src/main/sqldelight/org/sufficientlysecure/keychain/Certs.sq @@ -0,0 +1,22 @@ +import java.lang.Integer; +import org.sufficientlysecure.keychain.pgp.CanonicalizedKeyRing.VerificationStatus; + +CREATE TABLE IF NOT EXISTS certs( + master_key_id INTEGER NOT NULL, + rank INTEGER NOT NULL, + key_id_certifier INTEGER NOT NULL, + type INTEGER NOT NULL, + verified INTEGER AS VerificationStatus NOT NULL DEFAULT 0, + creation INTEGER NOT NULL, + data BLOB NOT NULL, + PRIMARY KEY(master_key_id, rank, key_id_certifier), + FOREIGN KEY(master_key_id) REFERENCES keyrings_public(master_key_id) ON DELETE CASCADE, + FOREIGN KEY(master_key_id, rank) REFERENCES user_packets(master_key_id, rank) ON DELETE CASCADE +); + +selectVerifyingCertDetails: +SELECT master_key_id AS masterKeyId, key_id_certifier AS signerMasterKeyId, creation * 1000 AS creation + FROM certs + WHERE verified = 1 AND master_key_id = ? AND rank = ? + ORDER BY creation DESC + LIMIT 1; \ No newline at end of file diff --git a/OpenKeychain/src/main/sqldelight/org/sufficientlysecure/keychain/KeyMetadata.sq b/OpenKeychain/src/main/sqldelight/org/sufficientlysecure/keychain/KeyMetadata.sq new file mode 100644 index 000000000..0f41a6c2a --- /dev/null +++ b/OpenKeychain/src/main/sqldelight/org/sufficientlysecure/keychain/KeyMetadata.sq @@ -0,0 +1,28 @@ +import java.lang.Boolean; +import java.util.Date; + +CREATE TABLE IF NOT EXISTS key_metadata ( + master_key_id INTEGER PRIMARY KEY, + last_updated INTEGER AS Date, + seen_on_keyservers INTEGER AS Boolean, + FOREIGN KEY(master_key_id) REFERENCES keyrings_public(master_key_id) ON DELETE CASCADE +); + +selectByMasterKeyId: +SELECT * + FROM key_metadata + WHERE master_key_id = ?; + +deleteAllLastUpdatedTimes: +UPDATE key_metadata + SET last_updated = null, seen_on_keyservers = null; + +replaceKeyMetadata: +REPLACE INTO key_metadata + (master_key_id, last_updated, seen_on_keyservers) VALUES (?, ?, ?); + +selectFingerprintsForKeysOlderThan: +SELECT fingerprint + FROM key_metadata + LEFT JOIN keys ON (key_metadata.master_key_id = keys.master_key_id AND keys.rank = 0) + WHERE last_updated < ?; \ No newline at end of file diff --git a/OpenKeychain/src/main/sqldelight/org/sufficientlysecure/keychain/KeyRingsPublic.sq b/OpenKeychain/src/main/sqldelight/org/sufficientlysecure/keychain/KeyRingsPublic.sq new file mode 100644 index 000000000..33e3b3f0d --- /dev/null +++ b/OpenKeychain/src/main/sqldelight/org/sufficientlysecure/keychain/KeyRingsPublic.sq @@ -0,0 +1,17 @@ +CREATE TABLE IF NOT EXISTS keyrings_public ( + master_key_id INTEGER NOT NULL PRIMARY KEY, + key_ring_data BLOB NULL +); + +selectAllMasterKeyIds: +SELECT master_key_id + FROM keyrings_public; + +selectByMasterKeyId: +SELECT * + FROM keyrings_public + WHERE master_key_id = ?; + +deleteByMasterKeyId: +DELETE FROM keyrings_public + WHERE master_key_id = ?; \ No newline at end of file diff --git a/OpenKeychain/src/main/sqldelight/org/sufficientlysecure/keychain/KeySignatures.sq b/OpenKeychain/src/main/sqldelight/org/sufficientlysecure/keychain/KeySignatures.sq new file mode 100644 index 000000000..58cc4b0c2 --- /dev/null +++ b/OpenKeychain/src/main/sqldelight/org/sufficientlysecure/keychain/KeySignatures.sq @@ -0,0 +1,10 @@ +CREATE TABLE IF NOT EXISTS key_signatures ( + master_key_id INTEGER NOT NULL, + signer_key_id INTEGER NOT NULL, + PRIMARY KEY(master_key_id, signer_key_id), + FOREIGN KEY(master_key_id) REFERENCES keyrings_public(master_key_id) ON DELETE CASCADE +); + +selectMasterKeyIdsBySigner: +SELECT master_key_id + FROM key_signatures WHERE signer_key_id IN ?; \ No newline at end of file diff --git a/OpenKeychain/src/main/sqldelight/org/sufficientlysecure/keychain/Keys.sq b/OpenKeychain/src/main/sqldelight/org/sufficientlysecure/keychain/Keys.sq new file mode 100644 index 000000000..dcf5cc586 --- /dev/null +++ b/OpenKeychain/src/main/sqldelight/org/sufficientlysecure/keychain/Keys.sq @@ -0,0 +1,96 @@ +import java.lang.Boolean; +import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey.SecretKeyType; + +CREATE TABLE IF NOT EXISTS keys ( + master_key_id INTEGER NOT NULL, + rank INTEGER NOT NULL, + key_id INTEGER NOT NULL, + key_size INTEGER AS Integer, + key_curve_oid TEXT, + algorithm INTEGER AS Integer NOT NULL, + fingerprint BLOB NOT NULL, + can_certify INTEGER AS Boolean NOT NULL, + can_sign INTEGER AS Boolean NOT NULL, + can_encrypt INTEGER AS Boolean NOT NULL, + can_authenticate INTEGER AS Boolean NOT NULL, + is_revoked INTEGER AS Boolean NOT NULL, + has_secret INTEGER AS SecretKeyType NOT NULL DEFAULT 0, + is_secure INTEGER AS Boolean NOT NULL, + creation INTEGER NOT NULL, + expiry INTEGER, + PRIMARY KEY(master_key_id, rank), + FOREIGN KEY(master_key_id) REFERENCES + keyrings_public(master_key_id) ON DELETE CASCADE +); + +unifiedKeyView: +CREATE VIEW unifiedKeyView AS + SELECT keys.master_key_id, keys.fingerprint, MIN(user_packets.rank), user_packets.user_id, user_packets.name, user_packets.email, user_packets.comment, keys.creation, keys.expiry, keys.is_revoked, keys.is_secure, keys.can_certify, certs.verified, + (EXISTS (SELECT * FROM user_packets AS dups WHERE dups.master_key_id != keys.master_key_id AND dups.rank = 0 AND dups.name = user_packets.name COLLATE NOCASE AND dups.email = user_packets.email COLLATE NOCASE )) AS has_duplicate_int, + (EXISTS (SELECT * FROM keys AS k WHERE k.master_key_id = keys.master_key_id AND k.has_secret != 0 )) AS has_any_secret_int, + (SELECT key_id FROM keys AS k WHERE k.master_key_id = keys.master_key_id AND k.can_encrypt != 0 LIMIT 1) AS has_encrypt_key_int, + (SELECT key_id FROM keys AS k WHERE k.master_key_id = keys.master_key_id AND k.can_sign != 0 LIMIT 1) AS has_sign_key_int, + (SELECT key_id FROM keys AS k WHERE k.master_key_id = keys.master_key_id AND k.can_authenticate != 0 LIMIT 1) AS has_auth_key_int, + GROUP_CONCAT(DISTINCT aTI.package_name) AS autocrypt_package_names_csv, + GROUP_CONCAT(user_packets.user_id, '|||') AS user_id_list + FROM keys + INNER JOIN user_packets ON ( keys.master_key_id = user_packets.master_key_id AND user_packets.type IS NULL AND (user_packets.rank = 0 OR user_packets.is_revoked = 0)) + LEFT JOIN certs ON ( keys.master_key_id = certs.master_key_id AND certs.verified = 1 ) + LEFT JOIN autocrypt_peers AS aTI ON ( aTI.master_key_id = keys.master_key_id ) + WHERE keys.rank = 0 + GROUP BY keys.master_key_id; + +selectAllUnifiedKeyInfo: +SELECT * FROM unifiedKeyView + ORDER BY has_any_secret_int DESC, IFNULL(name, email) COLLATE NOCASE ASC, creation DESC; + +selectUnifiedKeyInfoByMasterKeyId: +SELECT * FROM unifiedKeyView + WHERE master_key_id = ?; + +selectUnifiedKeyInfoByMasterKeyIds: +SELECT * FROM unifiedKeyView + WHERE master_key_id IN ?; + +selectUnifiedKeyInfoSearchMailAddress: +SELECT * FROM unifiedKeyView + WHERE email LIKE ? + ORDER BY creation DESC; + +selectAllUnifiedKeyInfoWithSecret: +SELECT * FROM unifiedKeyView + WHERE has_any_secret_int = 1 + ORDER BY creation DESC; + +selectMasterKeyIdBySubkey: +SELECT master_key_id + FROM keys + WHERE key_id = ?; + +selectSubkeysByMasterKeyId: +SELECT * + FROM keys + WHERE master_key_id = ? + ORDER BY rank ASC; + +selectSecretKeyType: +SELECT has_secret + FROM keys + WHERE key_id = ?; + +selectFingerprintByKeyId: +SELECT fingerprint + FROM keys + WHERE key_id = ?; + +selectEffectiveSignKeyIdByMasterKeyId: +SELECT key_id + FROM keys + WHERE is_revoked = 0 AND is_secure = 1 AND has_secret > 1 AND ( expiry IS NULL OR expiry >= date('now') ) + AND can_sign = 1 AND master_key_id = ?; + +selectEffectiveAuthKeyIdByMasterKeyId: +SELECT key_id + FROM keys + WHERE is_revoked = 0 AND is_secure = 1 AND has_secret > 1 AND ( expiry IS NULL OR expiry >= date('now') ) + AND can_authenticate = 1 AND master_key_id = ?; diff --git a/OpenKeychain/src/main/sqldelight/org/sufficientlysecure/keychain/OverriddenWarnings.sq b/OpenKeychain/src/main/sqldelight/org/sufficientlysecure/keychain/OverriddenWarnings.sq new file mode 100644 index 000000000..f5afb017d --- /dev/null +++ b/OpenKeychain/src/main/sqldelight/org/sufficientlysecure/keychain/OverriddenWarnings.sq @@ -0,0 +1,16 @@ +CREATE TABLE IF NOT EXISTS overridden_warnings ( + _id INTEGER PRIMARY KEY AUTOINCREMENT, + identifier TEXT NOT NULL UNIQUE +); + +selectCountByIdentifier: +SELECT COUNT(*) + FROM overridden_warnings + WHERE identifier = ?; + +insertIdentifier: +INSERT OR IGNORE INTO overridden_warnings (identifier) VALUES (?); + +deleteByIdentifier: +DELETE FROM overridden_warnings + WHERE identifier = ?; \ No newline at end of file diff --git a/OpenKeychain/src/main/sqldelight/org/sufficientlysecure/keychain/UserPackets.sq b/OpenKeychain/src/main/sqldelight/org/sufficientlysecure/keychain/UserPackets.sq new file mode 100644 index 000000000..85f8551e0 --- /dev/null +++ b/OpenKeychain/src/main/sqldelight/org/sufficientlysecure/keychain/UserPackets.sq @@ -0,0 +1,44 @@ +import java.lang.Integer; + +CREATE TABLE IF NOT EXISTS user_packets( + master_key_id INTEGER NOT NULL, + rank INTEGER AS Integer NOT NULL, + type INTEGER, + user_id TEXT, + name TEXT, + email TEXT, + comment TEXT, + attribute_data BLOB, + is_primary INTEGER AS Boolean NOT NULL, + is_revoked INTEGER AS Boolean NOT NULL, + PRIMARY KEY(master_key_id, rank), + FOREIGN KEY(master_key_id) REFERENCES + keyrings_public(master_key_id) ON DELETE CASCADE +); + +selectUserIdsByMasterKeyId: +SELECT user_packets.master_key_id, user_packets.rank, user_id, name, email, comment, is_primary, is_revoked, certs.verified + FROM user_packets + LEFT JOIN certs ON ( user_packets.master_key_id = certs.master_key_id AND user_packets.rank = certs.rank AND certs.verified > 0 ) + WHERE user_packets.type IS NULL AND user_packets.is_revoked = 0 AND user_packets.master_key_id IN ? + ORDER BY user_packets.master_key_id ASC,user_packets.rank ASC; + +selectUserIdsByMasterKeyIdAndVerification: +SELECT user_packets.master_key_id, user_packets.rank, user_id, name, email, comment, is_primary, is_revoked, certs.verified + FROM user_packets + LEFT JOIN certs ON ( user_packets.master_key_id = certs.master_key_id AND user_packets.rank = certs.rank AND certs.verified > 0 ) + WHERE user_packets.type IS NULL AND user_packets.is_revoked = 0 AND user_packets.master_key_id = ? AND certs.verified = ? + ORDER BY user_packets.rank ASC; + +selectUserAttributesByTypeAndMasterKeyId: +SELECT user_packets.master_key_id, user_packets.rank, attribute_data, is_primary, is_revoked, certs.verified + FROM user_packets + LEFT JOIN certs ON ( user_packets.master_key_id = certs.master_key_id AND user_packets.rank = certs.rank AND certs.verified > 0 ) + WHERE user_packets.type = ? AND user_packets.is_revoked = 0 AND user_packets.master_key_id = ? + ORDER BY user_packets.rank ASC; + +selectSpecificUserAttribute: +SELECT user_packets.master_key_id, user_packets.rank, attribute_data, is_primary, is_revoked, certs.verified + FROM user_packets + LEFT JOIN certs ON ( user_packets.master_key_id = certs.master_key_id AND user_packets.rank = certs.rank AND certs.verified > 0 ) + WHERE user_packets.type = ? AND user_packets.master_key_id = ? AND user_packets.rank = ?; diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/KeychainTestRunner.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/KeychainTestRunner.java index 37439d8b8..aad764af5 100644 --- a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/KeychainTestRunner.java +++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/KeychainTestRunner.java @@ -1,8 +1,11 @@ package org.sufficientlysecure.keychain; + import org.junit.runners.model.InitializationError; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; +import org.sufficientlysecure.keychain.shadows.ShadowWorkManager; + public class KeychainTestRunner extends RobolectricTestRunner { @@ -15,6 +18,7 @@ public class KeychainTestRunner extends RobolectricTestRunner { return new Config.Builder() .setSdk(27) .setConstants(WorkaroundBuildConfig.class) + .setShadows(new Class[] { ShadowWorkManager.class }) .build(); } } diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/TestHelpers.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/TestHelpers.java new file mode 100644 index 000000000..b6305bb87 --- /dev/null +++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/TestHelpers.java @@ -0,0 +1,43 @@ +package org.sufficientlysecure.keychain; + + +import android.content.ContentValues; + +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; +import org.mockito.Matchers; + + +public class TestHelpers { + public static ContentValues cvContains(ContentValues value) { + return Matchers.argThat(new BaseMatcher() { + @Override + public boolean matches(Object item) { + if (item instanceof ContentValues) { + ContentValues cv = (ContentValues) item; + for (String key : value.keySet()) { + if (!cv.containsKey(key)) { + return false; + } + + Object ours = value.get(key); + Object theirs = cv.get(key); + if (ours == null && theirs == null) { + continue; + } + if (ours == null || !ours.equals(theirs)) { + return false; + } + } + return true; + } + return false; + } + + @Override + public void describeTo(Description description) { + description.appendValue(value); + } + }); + } +} diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/operations/AuthenticationOperationTest.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/operations/AuthenticationOperationTest.java index 8f3726895..7ca5ee3fe 100644 --- a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/operations/AuthenticationOperationTest.java +++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/operations/AuthenticationOperationTest.java @@ -19,6 +19,13 @@ package org.sufficientlysecure.keychain.operations; +import java.io.PrintStream; +import java.security.MessageDigest; +import java.security.PublicKey; +import java.security.Security; +import java.security.Signature; +import java.util.ArrayList; + import org.bouncycastle.bcpg.HashAlgorithmTags; import org.bouncycastle.jcajce.provider.asymmetric.eddsa.EdDSAEngine; import org.bouncycastle.jcajce.provider.asymmetric.eddsa.spec.EdDSANamedCurveTable; @@ -34,8 +41,8 @@ import org.robolectric.shadows.ShadowLog; import org.sufficientlysecure.keychain.KeychainTestRunner; import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKey; import org.sufficientlysecure.keychain.pgp.UncachedKeyRing; -import org.sufficientlysecure.keychain.provider.KeyRepository; -import org.sufficientlysecure.keychain.provider.KeyWritableRepository; +import org.sufficientlysecure.keychain.daos.KeyRepository; +import org.sufficientlysecure.keychain.daos.KeyWritableRepository; import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import org.sufficientlysecure.keychain.ssh.AuthenticationData; import org.sufficientlysecure.keychain.ssh.AuthenticationOperation; @@ -44,13 +51,6 @@ import org.sufficientlysecure.keychain.ssh.AuthenticationResult; import org.sufficientlysecure.keychain.support.KeyringTestingHelper; import org.sufficientlysecure.keychain.util.Passphrase; -import java.io.PrintStream; -import java.security.MessageDigest; -import java.security.PublicKey; -import java.security.Security; -import java.security.Signature; -import java.util.ArrayList; - @RunWith(KeychainTestRunner.class) public class AuthenticationOperationTest { @@ -160,7 +160,7 @@ public class AuthenticationOperationTest { KeyRepository keyRepository = KeyRepository.create(RuntimeEnvironment.application); long masterKeyId = mStaticRingRsa.getMasterKeyId(); - Long authSubKeyId = keyRepository.getCachedPublicKeyRing(masterKeyId).getSecretAuthenticationId(); + Long authSubKeyId = keyRepository.getSecretAuthenticationId(masterKeyId); { // sign challenge AuthenticationOperation op = new AuthenticationOperation(RuntimeEnvironment.application, @@ -206,7 +206,7 @@ public class AuthenticationOperationTest { KeyRepository keyRepository = KeyRepository.create(RuntimeEnvironment.application); long masterKeyId = mStaticRingEcDsa.getMasterKeyId(); - Long authSubKeyId = keyRepository.getCachedPublicKeyRing(masterKeyId).getSecretAuthenticationId(); + Long authSubKeyId = keyRepository.getSecretAuthenticationId(masterKeyId); { // sign challenge AuthenticationOperation op = new AuthenticationOperation(RuntimeEnvironment.application, @@ -252,7 +252,7 @@ public class AuthenticationOperationTest { KeyRepository keyRepository = KeyRepository.create(RuntimeEnvironment.application); long masterKeyId = mStaticRingEdDsa.getMasterKeyId(); - Long authSubKeyId = keyRepository.getCachedPublicKeyRing(masterKeyId).getSecretAuthenticationId(); + Long authSubKeyId = keyRepository.getSecretAuthenticationId(masterKeyId); { // sign challenge AuthenticationOperation op = new AuthenticationOperation(RuntimeEnvironment.application, @@ -300,7 +300,7 @@ public class AuthenticationOperationTest { KeyRepository keyRepository = KeyRepository.create(RuntimeEnvironment.application); long masterKeyId = mStaticRingDsa.getMasterKeyId(); - Long authSubKeyId = keyRepository.getCachedPublicKeyRing(masterKeyId).getSecretAuthenticationId(); + Long authSubKeyId = keyRepository.getSecretAuthenticationId(masterKeyId); { // sign challenge AuthenticationOperation op = new AuthenticationOperation(RuntimeEnvironment.application, @@ -345,7 +345,7 @@ public class AuthenticationOperationTest { KeyRepository keyRepository = KeyRepository.create(RuntimeEnvironment.application); long masterKeyId = mStaticRingEcDsa.getMasterKeyId(); - Long authSubKeyId = keyRepository.getCachedPublicKeyRing(masterKeyId).getSecretAuthenticationId(); + Long authSubKeyId = keyRepository.getSecretAuthenticationId(masterKeyId); { // sign challenge - should succeed with selected key allowed AuthenticationOperation op = new AuthenticationOperation(RuntimeEnvironment.application, diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/operations/BackupOperationTest.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/operations/BackupOperationTest.java index 7417d4314..e6ec4a097 100644 --- a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/operations/BackupOperationTest.java +++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/operations/BackupOperationTest.java @@ -53,7 +53,7 @@ import org.sufficientlysecure.keychain.pgp.PgpKeyOperation; import org.sufficientlysecure.keychain.pgp.UncachedKeyRing; import org.sufficientlysecure.keychain.pgp.UncachedKeyRing.IteratorWithIOThrow; import org.sufficientlysecure.keychain.pgp.WrappedSignature; -import org.sufficientlysecure.keychain.provider.KeyWritableRepository; +import org.sufficientlysecure.keychain.daos.KeyWritableRepository; import org.sufficientlysecure.keychain.provider.TemporaryFileProvider; import org.sufficientlysecure.keychain.service.BackupKeyringParcel; import org.sufficientlysecure.keychain.service.ChangeUnlockParcel; @@ -162,15 +162,6 @@ public class BackupOperationTest { assertTrue("export must be a success", result); - long masterKeyId1, masterKeyId2; - if (mStaticRing1.getMasterKeyId() < mStaticRing2.getMasterKeyId()) { - masterKeyId1 = mStaticRing1.getMasterKeyId(); - masterKeyId2 = mStaticRing2.getMasterKeyId(); - } else { - masterKeyId2 = mStaticRing1.getMasterKeyId(); - masterKeyId1 = mStaticRing2.getMasterKeyId(); - } - IteratorWithIOThrow unc = UncachedKeyRing.fromStream(new ByteArrayInputStream(out.toByteArray())); @@ -178,7 +169,7 @@ public class BackupOperationTest { assertTrue("export must have two keys (1/2)", unc.hasNext()); UncachedKeyRing ring = unc.next(); Assert.assertEquals("first exported key has correct masterkeyid", - masterKeyId1, ring.getMasterKeyId()); + mStaticRing2.getMasterKeyId(), ring.getMasterKeyId()); assertFalse("first exported key must not be secret", ring.isSecret()); assertFalse("there must be no local signatures in an exported keyring", checkForLocal(ring)); @@ -188,7 +179,7 @@ public class BackupOperationTest { assertTrue("export must have two keys (2/2)", unc.hasNext()); UncachedKeyRing ring = unc.next(); Assert.assertEquals("second exported key has correct masterkeyid", - masterKeyId2, ring.getMasterKeyId()); + mStaticRing1.getMasterKeyId(), ring.getMasterKeyId()); assertFalse("second exported key must not be secret", ring.isSecret()); assertFalse("there must be no local signatures in an exported keyring", checkForLocal(ring)); @@ -205,7 +196,7 @@ public class BackupOperationTest { assertTrue("export must have four keys (1/4)", unc.hasNext()); UncachedKeyRing ring = unc.next(); Assert.assertEquals("1/4 exported key has correct masterkeyid", - masterKeyId1, ring.getMasterKeyId()); + mStaticRing2.getMasterKeyId(), ring.getMasterKeyId()); assertFalse("1/4 exported key must not be public", ring.isSecret()); assertFalse("there must be no local signatures in an exported keyring", checkForLocal(ring)); @@ -213,7 +204,7 @@ public class BackupOperationTest { assertTrue("export must have four keys (2/4)", unc.hasNext()); ring = unc.next(); Assert.assertEquals("2/4 exported key has correct masterkeyid", - masterKeyId1, ring.getMasterKeyId()); + mStaticRing2.getMasterKeyId(), ring.getMasterKeyId()); assertTrue("2/4 exported key must be public", ring.isSecret()); assertFalse("there must be no local signatures in an exported keyring", checkForLocal(ring)); @@ -223,7 +214,7 @@ public class BackupOperationTest { assertTrue("export must have four keys (3/4)", unc.hasNext()); UncachedKeyRing ring = unc.next(); Assert.assertEquals("3/4 exported key has correct masterkeyid", - masterKeyId2, ring.getMasterKeyId()); + mStaticRing1.getMasterKeyId(), ring.getMasterKeyId()); assertFalse("3/4 exported key must not be public", ring.isSecret()); assertFalse("there must be no local signatures in an exported keyring", checkForLocal(ring)); @@ -231,7 +222,7 @@ public class BackupOperationTest { assertTrue("export must have four keys (4/4)", unc.hasNext()); ring = unc.next(); Assert.assertEquals("4/4 exported key has correct masterkeyid", - masterKeyId2, ring.getMasterKeyId()); + mStaticRing1.getMasterKeyId(), ring.getMasterKeyId()); assertTrue("4/4 exported key must be public", ring.isSecret()); assertFalse("there must be no local signatures in an exported keyring", checkForLocal(ring)); diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/operations/BenchmarkOperationTest.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/operations/BenchmarkOperationTest.java index 682e689a4..4ec73b25a 100644 --- a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/operations/BenchmarkOperationTest.java +++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/operations/BenchmarkOperationTest.java @@ -25,7 +25,7 @@ import org.junit.runner.RunWith; import org.robolectric.RuntimeEnvironment; import org.robolectric.shadows.ShadowLog; import org.sufficientlysecure.keychain.KeychainTestRunner; -import org.sufficientlysecure.keychain.provider.KeyWritableRepository; +import org.sufficientlysecure.keychain.daos.KeyWritableRepository; import org.sufficientlysecure.keychain.service.BenchmarkInputParcel; import java.io.PrintStream; diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/operations/CertifyOperationTest.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/operations/CertifyOperationTest.java index 06f79699c..819329c01 100644 --- a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/operations/CertifyOperationTest.java +++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/operations/CertifyOperationTest.java @@ -37,12 +37,12 @@ import org.sufficientlysecure.keychain.KeychainTestRunner; import org.sufficientlysecure.keychain.operations.results.CertifyResult; import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType; import org.sufficientlysecure.keychain.operations.results.PgpEditKeyResult; +import org.sufficientlysecure.keychain.pgp.CanonicalizedKeyRing.VerificationStatus; import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKeyRing; import org.sufficientlysecure.keychain.pgp.PgpKeyOperation; import org.sufficientlysecure.keychain.pgp.UncachedKeyRing; import org.sufficientlysecure.keychain.pgp.WrappedUserAttribute; -import org.sufficientlysecure.keychain.provider.KeyWritableRepository; -import org.sufficientlysecure.keychain.provider.KeychainContract.Certs; +import org.sufficientlysecure.keychain.daos.KeyWritableRepository; import org.sufficientlysecure.keychain.service.CertifyActionsParcel; import org.sufficientlysecure.keychain.service.CertifyActionsParcel.CertifyAction; import org.sufficientlysecure.keychain.service.ChangeUnlockParcel; @@ -137,7 +137,7 @@ public class CertifyOperationTest { .getCanonicalizedPublicKeyRing(mStaticRing1.getMasterKeyId()); Assert.assertEquals("secret key must be marked self-certified in database", // TODO this should be more correctly be VERIFIED_SELF at some point! - Certs.VERIFIED_SECRET, ring.getVerified()); + VerificationStatus.VERIFIED_SECRET, ring.getVerified()); } @@ -149,8 +149,8 @@ public class CertifyOperationTest { { CanonicalizedPublicKeyRing ring = KeyWritableRepository.create(RuntimeEnvironment.application) .getCanonicalizedPublicKeyRing(mStaticRing2.getMasterKeyId()); - Assert.assertEquals("public key must not be marked verified prior to certification", - Certs.UNVERIFIED, ring.getVerified()); + Assert.assertNull("public key must not be marked verified prior to certification", + ring.getVerified()); } CertifyActionsParcel.Builder actions = CertifyActionsParcel.builder(mStaticRing1.getMasterKeyId()); @@ -164,21 +164,20 @@ public class CertifyOperationTest { CanonicalizedPublicKeyRing ring = KeyWritableRepository.create(RuntimeEnvironment.application) .getCanonicalizedPublicKeyRing(mStaticRing2.getMasterKeyId()); Assert.assertEquals("new key must be verified now", - Certs.VERIFIED_SECRET, ring.getVerified()); + VerificationStatus.VERIFIED_SECRET, ring.getVerified()); } } @Test public void testCertifyAttribute() throws Exception { - CertifyOperation op = new CertifyOperation(RuntimeEnvironment.application, - KeyWritableRepository.create(RuntimeEnvironment.application), null, null); + KeyWritableRepository keyWritableRepository = KeyWritableRepository.create(RuntimeEnvironment.application); + CertifyOperation op = new CertifyOperation(RuntimeEnvironment.application, keyWritableRepository, null, null); { - CanonicalizedPublicKeyRing ring = KeyWritableRepository.create(RuntimeEnvironment.application) - .getCanonicalizedPublicKeyRing(mStaticRing2.getMasterKeyId()); - Assert.assertEquals("public key must not be marked verified prior to certification", - Certs.UNVERIFIED, ring.getVerified()); + CanonicalizedPublicKeyRing ring = keyWritableRepository.getCanonicalizedPublicKeyRing(mStaticRing2.getMasterKeyId()); + Assert.assertNull("public key must not be marked verified prior to certification", + ring.getVerified()); } CertifyActionsParcel.Builder actions = CertifyActionsParcel.builder(mStaticRing1.getMasterKeyId()); @@ -189,10 +188,9 @@ public class CertifyOperationTest { Assert.assertTrue("certification must succeed", result.success()); { - CanonicalizedPublicKeyRing ring = KeyWritableRepository.create(RuntimeEnvironment.application) - .getCanonicalizedPublicKeyRing(mStaticRing2.getMasterKeyId()); + CanonicalizedPublicKeyRing ring = keyWritableRepository.getCanonicalizedPublicKeyRing(mStaticRing2.getMasterKeyId()); Assert.assertEquals("new key must be verified now", - Certs.VERIFIED_SECRET, ring.getVerified()); + VerificationStatus.VERIFIED_SECRET, ring.getVerified()); } } diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/operations/PromoteKeyOperationTest.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/operations/PromoteKeyOperationTest.java index 033908a44..18fe57a78 100644 --- a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/operations/PromoteKeyOperationTest.java +++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/operations/PromoteKeyOperationTest.java @@ -34,6 +34,7 @@ import org.junit.runner.RunWith; import org.robolectric.RuntimeEnvironment; import org.robolectric.shadows.ShadowLog; import org.sufficientlysecure.keychain.KeychainTestRunner; +import org.sufficientlysecure.keychain.model.SubKey.UnifiedKeyInfo; import org.sufficientlysecure.keychain.operations.results.PgpEditKeyResult; import org.sufficientlysecure.keychain.operations.results.PromoteKeyResult; import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey; @@ -42,8 +43,7 @@ import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKeyRing; import org.sufficientlysecure.keychain.pgp.PgpKeyOperation; import org.sufficientlysecure.keychain.pgp.UncachedKeyRing; import org.sufficientlysecure.keychain.pgp.UncachedPublicKey; -import org.sufficientlysecure.keychain.provider.CachedPublicKeyRing; -import org.sufficientlysecure.keychain.provider.KeyWritableRepository; +import org.sufficientlysecure.keychain.daos.KeyWritableRepository; import org.sufficientlysecure.keychain.service.ChangeUnlockParcel; import org.sufficientlysecure.keychain.service.PromoteKeyringParcel; import org.sufficientlysecure.keychain.service.SaveKeyringParcel; @@ -105,8 +105,9 @@ public class PromoteKeyOperationTest { @Test public void testPromote() throws Exception { + KeyWritableRepository keyRepository = KeyWritableRepository.create(RuntimeEnvironment.application); PromoteKeyOperation op = new PromoteKeyOperation(RuntimeEnvironment.application, - KeyWritableRepository.create(RuntimeEnvironment.application), null, null); + keyRepository, null, null); PromoteKeyResult result = op.execute( PromoteKeyringParcel.createPromoteKeyringParcel(mStaticRing.getMasterKeyId(), null, null), null); @@ -114,15 +115,14 @@ public class PromoteKeyOperationTest { Assert.assertTrue("promotion must succeed", result.success()); { - CachedPublicKeyRing ring = KeyWritableRepository.create(RuntimeEnvironment.application) - .getCachedPublicKeyRing(mStaticRing.getMasterKeyId()); - Assert.assertTrue("key must have a secret now", ring.hasAnySecret()); + UnifiedKeyInfo unifiedKeyInfo = keyRepository.getUnifiedKeyInfo(mStaticRing.getMasterKeyId()); + Assert.assertTrue("key must have a secret now", unifiedKeyInfo.has_any_secret()); Iterator it = mStaticRing.getPublicKeys(); while (it.hasNext()) { long keyId = it.next().getKeyId(); Assert.assertEquals("all subkeys must be gnu dummy", - SecretKeyType.GNU_DUMMY, ring.getSecretKeyType(keyId)); + SecretKeyType.GNU_DUMMY, keyRepository.getSecretKeyType(keyId)); } } diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/pgp/InputDataOperationTest.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/pgp/InputDataOperationTest.java index a40c31662..2ad3aa659 100644 --- a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/pgp/InputDataOperationTest.java +++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/pgp/InputDataOperationTest.java @@ -37,7 +37,7 @@ import org.sufficientlysecure.keychain.KeychainTestRunner; import org.sufficientlysecure.keychain.operations.InputDataOperation; import org.sufficientlysecure.keychain.operations.results.InputDataResult; import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType; -import org.sufficientlysecure.keychain.provider.KeyWritableRepository; +import org.sufficientlysecure.keychain.daos.KeyWritableRepository; import org.sufficientlysecure.keychain.provider.TemporaryFileProvider; import org.sufficientlysecure.keychain.service.InputDataParcel; import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; @@ -49,12 +49,16 @@ import java.io.PrintStream; import java.security.Security; import java.util.ArrayList; +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertTrue; import static org.mockito.Mockito.any; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.sufficientlysecure.keychain.TestHelpers.cvContains; + @RunWith(KeychainTestRunner.class) public class InputDataOperationTest { @@ -137,33 +141,33 @@ public class InputDataOperationTest { Assert.assertNull(result.mDecryptVerifyResult); ArrayList outUris = result.getOutputUris(); - Assert.assertEquals("must have two output URIs", 2, outUris.size()); - Assert.assertEquals("first uri must be the one we provided", fakeOutputUri1, outUris.get(0)); + assertEquals("must have two output URIs", 2, outUris.size()); + assertEquals("first uri must be the one we provided", fakeOutputUri1, outUris.get(0)); verify(mockResolver).openOutputStream(result.getOutputUris().get(0), "w"); - Assert.assertEquals("second uri must be the one we provided", fakeOutputUri2, outUris.get(1)); + assertEquals("second uri must be the one we provided", fakeOutputUri2, outUris.get(1)); verify(mockResolver).openOutputStream(result.getOutputUris().get(1), "w"); ContentValues contentValues = new ContentValues(); contentValues.put("name", "data.txt"); contentValues.put("mimetype", "text/plain"); - verify(mockResolver).insert(TemporaryFileProvider.CONTENT_URI, contentValues); + verify(mockResolver).insert(eq(TemporaryFileProvider.CONTENT_URI), cvContains(contentValues)); contentValues.put("name", (String) null); contentValues.put("mimetype", "text/testvalue"); - verify(mockResolver).insert(TemporaryFileProvider.CONTENT_URI, contentValues); + verify(mockResolver).insert(eq(TemporaryFileProvider.CONTENT_URI), cvContains(contentValues)); // quoted-printable returns windows style line endings for some reason? - Assert.assertEquals("first part must have expected content", + assertEquals("first part must have expected content", "message part 1\r\n", new String(outStream1.toByteArray())); - Assert.assertEquals("second part must have expected content", + assertEquals("second part must have expected content", "message part 2.1\nmessage part 2.2\n", new String(outStream2.toByteArray())); OpenPgpMetadata metadata = result.mMetadata.get(0); - Assert.assertEquals("text/plain", metadata.getMimeType()); - Assert.assertEquals("utf-8", metadata.getCharset()); + assertEquals("text/plain", metadata.getMimeType()); + assertEquals("utf-8", metadata.getCharset()); metadata = result.mMetadata.get(1); - Assert.assertEquals("text/testvalue", metadata.getMimeType()); - Assert.assertEquals("iso-8859-1", metadata.getCharset()); + assertEquals("text/testvalue", metadata.getMimeType()); + assertEquals("iso-8859-1", metadata.getCharset()); } @Test @@ -184,9 +188,9 @@ public class InputDataOperationTest { Assert.assertNull(result.mDecryptVerifyResult); OpenPgpMetadata metadata = result.mMetadata.get(0); - Assert.assertEquals("text/plain", metadata.getMimeType()); + assertEquals("text/plain", metadata.getMimeType()); - Assert.assertEquals("charset should be set since it was explicitly specified", + assertEquals("charset should be set since it was explicitly specified", "utf-8", metadata.getCharset()); Assert.assertTrue("faulty charset should have been detected", result.getLog().containsType(LogType.MSG_DATA_MIME_CHARSET_FAULTY)); @@ -210,7 +214,7 @@ public class InputDataOperationTest { Assert.assertNull(result.mDecryptVerifyResult); OpenPgpMetadata metadata = result.mMetadata.get(0); - Assert.assertEquals("text/plain", metadata.getMimeType()); + assertEquals("text/plain", metadata.getMimeType()); Assert.assertNull("charset was bad so it should not be set", metadata.getCharset()); Assert.assertTrue("faulty charset should have been detected", @@ -231,9 +235,9 @@ public class InputDataOperationTest { Assert.assertNull(result.mDecryptVerifyResult); OpenPgpMetadata metadata = result.mMetadata.get(0); - Assert.assertEquals("text/plain", metadata.getMimeType()); + assertEquals("text/plain", metadata.getMimeType()); - Assert.assertEquals("charset should be set since it was guessed and not faulty", + assertEquals("charset should be set since it was guessed and not faulty", "utf-8", metadata.getCharset()); Assert.assertTrue("charset should have been guessed", result.getLog().containsType(LogType.MSG_DATA_MIME_CHARSET_GUESS)); @@ -253,9 +257,9 @@ public class InputDataOperationTest { Assert.assertNull(result.mDecryptVerifyResult); OpenPgpMetadata metadata = result.mMetadata.get(0); - Assert.assertEquals("text/plain", metadata.getMimeType()); + assertEquals("text/plain", metadata.getMimeType()); - Assert.assertEquals("charset should be set since it was guessed and not faulty", + assertEquals("charset should be set since it was guessed and not faulty", "utf-8", metadata.getCharset()); Assert.assertTrue("charset should have been guessed", result.getLog().containsType(LogType.MSG_DATA_MIME_CHARSET_GUESS)); @@ -280,7 +284,7 @@ public class InputDataOperationTest { Assert.assertTrue("should not be mime parsed", result.getLog().containsType(LogType.MSG_DATA_MIME_NONE)); - Assert.assertEquals("output uri should simply be passed-through input uri", + assertEquals("output uri should simply be passed-through input uri", result.getOutputUris().get(0), FAKE_CONTENT_INPUT_URI_1); } diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/pgp/PgpEncryptDecryptTest.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/pgp/PgpEncryptDecryptTest.java index 943446728..88215fde2 100644 --- a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/pgp/PgpEncryptDecryptTest.java +++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/pgp/PgpEncryptDecryptTest.java @@ -53,8 +53,7 @@ import org.sufficientlysecure.keychain.operations.results.PgpSignEncryptResult; import org.sufficientlysecure.keychain.pgp.SecurityProblem.InsecureBitStrength; import org.sufficientlysecure.keychain.pgp.SecurityProblem.InsecureEncryptionAlgorithm; import org.sufficientlysecure.keychain.pgp.SecurityProblem.MissingMdc; -import org.sufficientlysecure.keychain.provider.KeyWritableRepository; -import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRingData; +import org.sufficientlysecure.keychain.daos.KeyWritableRepository; import org.sufficientlysecure.keychain.service.SaveKeyringParcel; import org.sufficientlysecure.keychain.service.SaveKeyringParcel.SubkeyChange; import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; @@ -831,9 +830,7 @@ public class PgpEncryptDecryptTest { { // decryption with passphrase cached should succeed for the other key if first is gone // delete first key from database - KeyWritableRepository.create(RuntimeEnvironment.application).getContentResolver().delete( - KeyRingData.buildPublicKeyRingUri(mStaticRing1.getMasterKeyId()), null, null - ); + KeyWritableRepository.create(RuntimeEnvironment.application).deleteKeyRing(mStaticRing1.getMasterKeyId()); ByteArrayOutputStream out = new ByteArrayOutputStream(); ByteArrayInputStream in = new ByteArrayInputStream(ciphertext); @@ -912,9 +909,7 @@ public class PgpEncryptDecryptTest { { // decryption with passphrase cached should succeed for the other key if first is gone // delete first key from database - KeyWritableRepository.create(RuntimeEnvironment.application).getContentResolver().delete( - KeyRingData.buildPublicKeyRingUri(mStaticRing1.getMasterKeyId()), null, null - ); + KeyWritableRepository.create(RuntimeEnvironment.application).deleteKeyRing(mStaticRing1.getMasterKeyId()); ByteArrayOutputStream out = new ByteArrayOutputStream(); ByteArrayInputStream in = new ByteArrayInputStream(ciphertext); diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/pgp/PgpKeyOperationTest.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/pgp/PgpKeyOperationTest.java index c96cc1ea2..37108971a 100644 --- a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/pgp/PgpKeyOperationTest.java +++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/pgp/PgpKeyOperationTest.java @@ -55,6 +55,7 @@ import org.sufficientlysecure.keychain.KeychainTestRunner; import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType; import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; import org.sufficientlysecure.keychain.operations.results.PgpEditKeyResult; +import org.sufficientlysecure.keychain.pgp.CanonicalizedKeyRing.VerificationStatus; import org.sufficientlysecure.keychain.service.ChangeUnlockParcel; import org.sufficientlysecure.keychain.service.SaveKeyringParcel; import org.sufficientlysecure.keychain.service.SaveKeyringParcel.Algorithm; @@ -672,7 +673,7 @@ public class PgpKeyOperationTest { resetBuilder(); builder.addRevokeSubkey(123L); - CanonicalizedSecretKeyRing secretRing = new CanonicalizedSecretKeyRing(ring.getEncoded(), 0); + CanonicalizedSecretKeyRing secretRing = new CanonicalizedSecretKeyRing(ring.getEncoded(), VerificationStatus.UNVERIFIED); UncachedKeyRing otherModified = op.modifySecretKeyRing(secretRing, cryptoInput, builder.build()).getRing(); Assert.assertNull("revoking a nonexistent subkey should fail", otherModified); @@ -869,7 +870,7 @@ public class PgpKeyOperationTest { securityTokenBuilder.addOrReplaceSubkeyChange(SubkeyChange.createMoveToSecurityTokenChange(keyId)); CanonicalizedSecretKeyRing secretRing = - new CanonicalizedSecretKeyRing(ringSecurityToken.getEncoded(), 0); + new CanonicalizedSecretKeyRing(ringSecurityToken.getEncoded(), VerificationStatus.UNVERIFIED); PgpKeyOperation op = new PgpKeyOperation(null); PgpEditKeyResult result = op.modifySecretKeyRing(secretRing, cryptoInput, securityTokenBuilder.build()); Assert.assertTrue("moveKeyToSecurityToken operation should be pending", result.isPending()); @@ -904,7 +905,7 @@ public class PgpKeyOperationTest { securityTokenBuilder.addOrReplaceSubkeyChange(SubkeyChange.createRecertifyChange(keyId, true)); CanonicalizedSecretKeyRing secretRing = - new CanonicalizedSecretKeyRing(modified.getEncoded(), 0); + new CanonicalizedSecretKeyRing(modified.getEncoded(), VerificationStatus.UNVERIFIED); PgpKeyOperation op = new PgpKeyOperation(null); PgpEditKeyResult result = op.modifySecretKeyRing(secretRing, cryptoInput, securityTokenBuilder.build()); Assert.assertTrue("moveKeyToSecurityToken operation should be pending", result.isPending()); @@ -1187,7 +1188,7 @@ public class PgpKeyOperationTest { // we should still be able to modify it (and change its passphrase) without errors PgpKeyOperation op = new PgpKeyOperation(null); - CanonicalizedSecretKeyRing secretRing = new CanonicalizedSecretKeyRing(modified.getEncoded(), 0); + CanonicalizedSecretKeyRing secretRing = new CanonicalizedSecretKeyRing(modified.getEncoded(), VerificationStatus.UNVERIFIED); PgpEditKeyResult result = op.modifySecretKeyRing(secretRing, otherCryptoInput, builder.build()); Assert.assertTrue("key modification must succeed", result.success()); Assert.assertFalse("log must not contain a warning", @@ -1201,7 +1202,7 @@ public class PgpKeyOperationTest { modified = KeyringTestingHelper.injectPacket(modified, sKeyWithPassphrase.buf, sKeyWithPassphrase.position); PgpKeyOperation op = new PgpKeyOperation(null); - CanonicalizedSecretKeyRing secretRing = new CanonicalizedSecretKeyRing(modified.getEncoded(), 0); + CanonicalizedSecretKeyRing secretRing = new CanonicalizedSecretKeyRing(modified.getEncoded(), VerificationStatus.UNVERIFIED); PgpEditKeyResult result = op.modifySecretKeyRing(secretRing, CryptoInputParcel.createCryptoInputParcel(otherPassphrase2), builder.build()); Assert.assertTrue("key modification must succeed", result.success()); @@ -1214,7 +1215,7 @@ public class PgpKeyOperationTest { @Test public void testRestricted() throws Exception { - CanonicalizedSecretKeyRing secretRing = new CanonicalizedSecretKeyRing(ring.getEncoded(), 0); + CanonicalizedSecretKeyRing secretRing = new CanonicalizedSecretKeyRing(ring.getEncoded(), VerificationStatus.UNVERIFIED); builder.addUserId("discord"); PgpKeyOperation op = new PgpKeyOperation(null); @@ -1250,7 +1251,7 @@ public class PgpKeyOperationTest { try { Assert.assertTrue("modified keyring must be secret", ring.isSecret()); - CanonicalizedSecretKeyRing secretRing = new CanonicalizedSecretKeyRing(ring.getEncoded(), 0); + CanonicalizedSecretKeyRing secretRing = new CanonicalizedSecretKeyRing(ring.getEncoded(), VerificationStatus.UNVERIFIED); PgpKeyOperation op = new PgpKeyOperation(null); PgpEditKeyResult result = op.modifySecretKeyRing(secretRing, cryptoInput, parcel); @@ -1323,7 +1324,7 @@ public class PgpKeyOperationTest { SaveKeyringParcel parcel, CryptoInputParcel cryptoInput, LogType expected) throws Exception { - CanonicalizedSecretKeyRing secretRing = new CanonicalizedSecretKeyRing(ring.getEncoded(), 0); + CanonicalizedSecretKeyRing secretRing = new CanonicalizedSecretKeyRing(ring.getEncoded(), VerificationStatus.UNVERIFIED); PgpEditKeyResult result = op.modifySecretKeyRing(secretRing, cryptoInput, parcel); Assert.assertFalse(reason, result.success()); @@ -1337,7 +1338,7 @@ public class PgpKeyOperationTest { LogType expected) throws Exception { - CanonicalizedSecretKeyRing secretRing = new CanonicalizedSecretKeyRing(ring.getEncoded(), 0); + CanonicalizedSecretKeyRing secretRing = new CanonicalizedSecretKeyRing(ring.getEncoded(), VerificationStatus.UNVERIFIED); PgpEditKeyResult result = op.modifySecretKeyRing(secretRing, cryptoInput, parcel); Assert.assertFalse(reason, result.success()); diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/pgp/UncachedKeyringMergeTest.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/pgp/UncachedKeyringMergeTest.java index abc71cd65..22beb189c 100644 --- a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/pgp/UncachedKeyringMergeTest.java +++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/pgp/UncachedKeyringMergeTest.java @@ -43,6 +43,7 @@ import org.sufficientlysecure.keychain.KeychainTestRunner; import org.sufficientlysecure.keychain.operations.results.OperationResult; import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; import org.sufficientlysecure.keychain.operations.results.PgpEditKeyResult; +import org.sufficientlysecure.keychain.pgp.CanonicalizedKeyRing.VerificationStatus; import org.sufficientlysecure.keychain.pgp.PgpCertifyOperation.PgpCertifyResult; import org.sufficientlysecure.keychain.service.CertifyActionsParcel.CertifyAction; import org.sufficientlysecure.keychain.service.ChangeUnlockParcel; @@ -188,7 +189,7 @@ public class UncachedKeyringMergeTest { UncachedKeyRing modifiedA, modifiedB; { CanonicalizedSecretKeyRing secretRing = - new CanonicalizedSecretKeyRing(ringA.getEncoded(), 0); + new CanonicalizedSecretKeyRing(ringA.getEncoded(), VerificationStatus.UNVERIFIED); resetBuilder(); builder.addUserId("flim"); @@ -230,7 +231,7 @@ public class UncachedKeyringMergeTest { UncachedKeyRing modifiedA, modifiedB; long subKeyIdA, subKeyIdB; { - CanonicalizedSecretKeyRing secretRing = new CanonicalizedSecretKeyRing(ringA.getEncoded(), 0); + CanonicalizedSecretKeyRing secretRing = new CanonicalizedSecretKeyRing(ringA.getEncoded(), VerificationStatus.UNVERIFIED); resetBuilder(); builder.addSubkeyAdd(SubkeyAdd.createSubkeyAdd( @@ -278,7 +279,7 @@ public class UncachedKeyringMergeTest { resetBuilder(); builder.addRevokeSubkey(KeyringTestingHelper.getSubkeyId(ringA, 1)); CanonicalizedSecretKeyRing secretRing = new CanonicalizedSecretKeyRing( - ringA.getEncoded(), 0); + ringA.getEncoded(), VerificationStatus.UNVERIFIED); modified = op.modifySecretKeyRing(secretRing, CryptoInputParcel.createCryptoInputParcel(new Date(), new Passphrase()), builder.build()).getRing(); } @@ -301,10 +302,10 @@ public class UncachedKeyringMergeTest { final UncachedKeyRing modified; { CanonicalizedPublicKeyRing publicRing = new CanonicalizedPublicKeyRing( - pubRing.getEncoded(), 0); + pubRing.getEncoded(), VerificationStatus.UNVERIFIED); CanonicalizedSecretKey secretKey = new CanonicalizedSecretKeyRing( - ringB.getEncoded(), 0).getSecretKey(); + ringB.getEncoded(), VerificationStatus.UNVERIFIED).getSecretKey(); secretKey.unlock(new Passphrase()); PgpCertifyOperation op = new PgpCertifyOperation(); CertifyAction action = CertifyAction.createForUserIds( @@ -379,7 +380,7 @@ public class UncachedKeyringMergeTest { builder.addUserAttribute(uat); CanonicalizedSecretKeyRing secretRing = new CanonicalizedSecretKeyRing( - ringA.getEncoded(), 0); + ringA.getEncoded(), VerificationStatus.UNVERIFIED); modified = op.modifySecretKeyRing(secretRing, CryptoInputParcel.createCryptoInputParcel(new Date(), new Passphrase()), builder.build()).getRing(); } diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/provider/Cv25519Test.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/provider/Cv25519Test.java index c87acdcb0..195c4ca5a 100644 --- a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/provider/Cv25519Test.java +++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/provider/Cv25519Test.java @@ -33,6 +33,7 @@ import org.robolectric.RuntimeEnvironment; import org.robolectric.shadows.ShadowLog; import org.robolectric.util.Util; import org.sufficientlysecure.keychain.KeychainTestRunner; +import org.sufficientlysecure.keychain.daos.KeyWritableRepository; import org.sufficientlysecure.keychain.operations.results.DecryptVerifyResult; import org.sufficientlysecure.keychain.operations.results.PgpSignEncryptResult; import org.sufficientlysecure.keychain.operations.results.SaveKeyringResult; diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/provider/EddsaTest.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/provider/EddsaTest.java index ddb0100a5..06d0429cd 100644 --- a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/provider/EddsaTest.java +++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/provider/EddsaTest.java @@ -34,6 +34,7 @@ import org.robolectric.RuntimeEnvironment; import org.robolectric.shadows.ShadowLog; import org.robolectric.util.Util; import org.sufficientlysecure.keychain.KeychainTestRunner; +import org.sufficientlysecure.keychain.daos.KeyWritableRepository; import org.sufficientlysecure.keychain.operations.results.DecryptVerifyResult; import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; import org.sufficientlysecure.keychain.operations.results.PgpEditKeyResult; diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/provider/InteropTest.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/provider/InteropTest.java index 6cdc1428d..5bbbd4833 100644 --- a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/provider/InteropTest.java +++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/provider/InteropTest.java @@ -16,7 +16,16 @@ package org.sufficientlysecure.keychain.provider; -import android.net.Uri; + +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.Closeable; +import java.io.File; +import java.io.FileInputStream; +import java.net.URL; +import java.security.Security; +import java.util.ArrayList; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.json.JSONArray; @@ -30,38 +39,24 @@ import org.openintents.openpgp.OpenPgpSignatureResult; import org.robolectric.RuntimeEnvironment; import org.robolectric.shadows.ShadowLog; import org.sufficientlysecure.keychain.KeychainTestRunner; +import org.sufficientlysecure.keychain.daos.KeyWritableRepository; import org.sufficientlysecure.keychain.operations.results.DecryptVerifyResult; import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; import org.sufficientlysecure.keychain.pgp.CanonicalizedKeyRing; import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKey; -import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKeyRing; -import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey.SecretKeyType; -import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKeyRing; import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyInputParcel; import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyOperation; import org.sufficientlysecure.keychain.pgp.UncachedKeyRing; -import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; -import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; import org.sufficientlysecure.keychain.util.InputData; import org.sufficientlysecure.keychain.util.Passphrase; -import java.io.BufferedInputStream; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.Closeable; -import java.io.File; -import java.io.FileInputStream; -import java.net.URL; -import java.security.Security; -import java.util.ArrayList; - @RunWith(KeychainTestRunner.class) public class InteropTest { @BeforeClass - public static void setUpOnce() throws Exception { + public static void setUpOnce() { Security.insertProviderAt(new BouncyCastleProvider(), 1); ShadowLog.stream = System.out; } @@ -103,11 +98,11 @@ public class InteropTest { } } - private static final String asString(File json) throws Exception { + private static String asString(File json) throws Exception { return new String(asBytes(json), "utf-8"); } - private static final byte[] asBytes(File f) throws Exception { + private static byte[] asBytes(File f) throws Exception { FileInputStream fin = null; try { fin = new FileInputStream(f); @@ -122,16 +117,14 @@ public class InteropTest { private void runDecryptTest(JSONObject config, File base) throws Exception { File root = base.getParentFile(); String baseName = getBaseName(base); - CanonicalizedPublicKeyRing verify; + UncachedKeyRing verify; if (config.has("verifyKey")) { - verify = (CanonicalizedPublicKeyRing) - readRingFromFile(new File(root, config.getString("verifyKey"))); + verify = readUncachedRingFromFile(new File(root, config.getString("verifyKey"))); } else { verify = null; } - CanonicalizedSecretKeyRing decrypt = (CanonicalizedSecretKeyRing) - readRingFromFile(new File(root, config.getString("decryptKey"))); + UncachedKeyRing decrypt = readUncachedRingFromFile(new File(root, config.getString("decryptKey"))); ByteArrayOutputStream out = new ByteArrayOutputStream(); ByteArrayInputStream in = @@ -153,9 +146,10 @@ public class InteropTest { if (verify != null) { // Certain keys are too short, so we check appropriately. int code = result.getSignatureResult().getResult(); - Assert.assertTrue(base + ": should have a signature", - (code == OpenPgpSignatureResult.RESULT_INVALID_KEY_INSECURE) || - (code == OpenPgpSignatureResult.RESULT_VALID_KEY_UNCONFIRMED)); + Assert.assertTrue(base + ": should have a signature (code: " + code + ")", + code == OpenPgpSignatureResult.RESULT_INVALID_KEY_INSECURE || + code == OpenPgpSignatureResult.RESULT_VALID_KEY_UNCONFIRMED + || code == OpenPgpSignatureResult.RESULT_VALID_KEY_CONFIRMED); } OpenPgpMetadata metadata = result.getDecryptionMetadata(); Assert.assertEquals(base + ": filesize must be correct", @@ -167,11 +161,10 @@ public class InteropTest { private void runImportTest(JSONObject config, File base) throws Exception { File root = base.getParentFile(); String baseName = getBaseName(base); - CanonicalizedKeyRing pkr = - readRingFromFile(new File(root, baseName + ".asc")); + CanonicalizedKeyRing pkr = readRingFromFile(new File(root, baseName + ".asc")); // Check we have the correct uids. - ArrayList expected = new ArrayList(); + ArrayList expected = new ArrayList<>(); JSONArray uids = config.getJSONArray("expected_uids"); for (int i = 0; i < uids.length(); i++) { expected.add(uids.getString(i)); @@ -187,7 +180,7 @@ public class InteropTest { expected.add(subkeys.getJSONObject(i).getString("expected_fingerprint")); } } - ArrayList actual = new ArrayList(); + ArrayList actual = new ArrayList<>(); for (CanonicalizedPublicKey pk: pkr.publicKeyIterator()) { if (pk.isValid()) { actual.add(KeyFormattingUtils.convertFingerprintToHex(pk.getFingerprint())); @@ -203,7 +196,7 @@ public class InteropTest { } } - UncachedKeyRing readUncachedRingFromFile(File path) throws Exception { + private UncachedKeyRing readUncachedRingFromFile(File path) throws Exception { BufferedInputStream bin = null; try { bin = new BufferedInputStream(new FileInputStream(path)); @@ -213,87 +206,40 @@ public class InteropTest { } } - CanonicalizedKeyRing readRingFromFile(File path) throws Exception { + private CanonicalizedKeyRing readRingFromFile(File path) throws Exception { UncachedKeyRing ukr = readUncachedRingFromFile(path); OperationLog log = new OperationLog(); return ukr.canonicalize(log, 0); } - private static final void close(Closeable v) { + private static void close(Closeable v) { if (v != null) { try { v.close(); - } catch (Throwable any) { + } catch (Throwable ignored) { } } } - private static final String getBaseName(File base) { + private static String getBaseName(File base) { String name = base.getName(); return name.substring(0, name.length() - ".json".length()); } private PgpDecryptVerifyOperation makeOperation(final String msg, final Passphrase passphrase, - final CanonicalizedSecretKeyRing decrypt, final CanonicalizedPublicKeyRing verify) - throws Exception { + UncachedKeyRing decrypt, UncachedKeyRing verify) { + KeyWritableRepository keyRepository = KeyWritableRepository.create(RuntimeEnvironment.application); - final long decryptId = decrypt.getEncryptId(); - final Uri decryptUri = KeyRings.buildUnifiedKeyRingsFindBySubkeyUri(decryptId); - final Uri verifyUri = verify != null ? - KeyRings.buildUnifiedKeyRingsFindBySubkeyUri(verify.getMasterKeyId()) : null; - - KeyWritableRepository helper = new KeyWritableRepository(RuntimeEnvironment.application, - LocalPublicKeyStorage.getInstance(RuntimeEnvironment.application), - LastUpdateInteractor.create(RuntimeEnvironment.application), - DatabaseNotifyManager.create(RuntimeEnvironment.application)) { + Assert.assertTrue(keyRepository.saveSecretKeyRing(decrypt).success()); + if (verify != null) { + Assert.assertTrue(keyRepository.savePublicKeyRing(verify).success()); + } + return new PgpDecryptVerifyOperation(RuntimeEnvironment.application, keyRepository, null) { @Override - public CachedPublicKeyRing getCachedPublicKeyRing(Uri queryUri) throws PgpKeyNotFoundException { - Assert.assertEquals(msg + ": query should be for the decryption key", queryUri, decryptUri); - return new CachedPublicKeyRing(this, queryUri) { - @Override - public long getMasterKeyId() throws PgpKeyNotFoundException { - return decrypt.getMasterKeyId(); - } - - @Override - public SecretKeyType getSecretKeyType(long keyId) throws NotFoundException { - return decrypt.getSecretKey(keyId).getSecretKeyTypeSuperExpensive(); - } - }; - } - - @Override - public CanonicalizedPublicKeyRing getCanonicalizedPublicKeyRing(Uri q) - throws NotFoundException { - Assert.assertEquals(msg + ": query should be for verification key", q, verifyUri); - return verify; - } - - @Override - public CanonicalizedSecretKeyRing getCanonicalizedSecretKeyRing(Uri q) - throws NotFoundException { - Assert.assertEquals(msg + ": query should be for the decryption key", q, decryptUri); - return decrypt; - } - - @Override - public CanonicalizedSecretKeyRing getCanonicalizedSecretKeyRing(long masterKeyId) - throws NotFoundException { - Assert.assertEquals(msg + ": query should be for the decryption key", - masterKeyId, decrypt.getMasterKeyId()); - return decrypt; - } - }; - - return new PgpDecryptVerifyOperation(RuntimeEnvironment.application, helper, null) { - @Override - public Passphrase getCachedPassphrase(long masterKeyId, long subKeyId) - throws NoSecretKeyException { + public Passphrase getCachedPassphrase(long masterKeyId, long subKeyId) { Assert.assertEquals(msg + ": passphrase should be for the secret key", masterKeyId, decrypt.getMasterKeyId()); - Assert.assertEquals(msg + ": passphrase should refer to the decryption subkey", - subKeyId, decryptId); return passphrase; } }; diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/provider/KeyRepositorySaveTest.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/provider/KeyRepositorySaveTest.java index 5f0e6b522..1a04345d4 100644 --- a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/provider/KeyRepositorySaveTest.java +++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/provider/KeyRepositorySaveTest.java @@ -18,6 +18,10 @@ package org.sufficientlysecure.keychain.provider; + +import java.util.Arrays; +import java.util.Iterator; + import org.bouncycastle.bcpg.sig.KeyFlags; import org.bouncycastle.util.encoders.Hex; import org.junit.Assert; @@ -26,7 +30,10 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RuntimeEnvironment; import org.robolectric.shadows.ShadowLog; +import org.sufficientlysecure.keychain.KeychainDatabase; import org.sufficientlysecure.keychain.KeychainTestRunner; +import org.sufficientlysecure.keychain.daos.KeyWritableRepository; +import org.sufficientlysecure.keychain.model.SubKey.UnifiedKeyInfo; import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; import org.sufficientlysecure.keychain.operations.results.SaveKeyringResult; import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKeyRing; @@ -36,9 +43,6 @@ import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKeyRing; import org.sufficientlysecure.keychain.pgp.UncachedKeyRing; import org.sufficientlysecure.keychain.util.IterableIterator; -import java.util.Arrays; -import java.util.Iterator; - @RunWith(KeychainTestRunner.class) public class KeyRepositorySaveTest { @@ -66,7 +70,7 @@ public class KeyRepositorySaveTest { result = KeyWritableRepository.create(RuntimeEnvironment.application).savePublicKeyRing(second); Assert.assertFalse("second keyring import should fail", result.success()); - new KeychainDatabase(RuntimeEnvironment.application).clearDatabase(); + KeychainDatabase.getInstance(RuntimeEnvironment.application).clearDatabase(); // and the other way around result = KeyWritableRepository.create(RuntimeEnvironment.application).savePublicKeyRing(second); @@ -112,11 +116,11 @@ public class KeyRepositorySaveTest { mDatabaseInteractor.savePublicKeyRing(pub); - CachedPublicKeyRing cachedRing = mDatabaseInteractor.getCachedPublicKeyRing(keyId); + UnifiedKeyInfo unifiedKeyInfo = mDatabaseInteractor.getUnifiedKeyInfo(keyId); CanonicalizedPublicKeyRing pubRing = mDatabaseInteractor.getCanonicalizedPublicKeyRing(keyId); Assert.assertEquals("master key should be encryption key", keyId, pubRing.getEncryptId()); - Assert.assertEquals("master key should be encryption key (cached)", keyId, cachedRing.getEncryptId()); + Assert.assertEquals("master key should be encryption key (cached)", keyId, unifiedKeyInfo.has_encrypt_key_int()); Assert.assertEquals("canonicalized key flags should be zero", 0, (long) pubRing.getPublicKey().getKeyUsage()); @@ -138,7 +142,6 @@ public class KeyRepositorySaveTest { // make sure both the CanonicalizedSecretKeyRing as well as the CachedPublicKeyRing correctly // indicate the secret key type - CachedPublicKeyRing cachedRing = mDatabaseInteractor.getCachedPublicKeyRing(keyId); CanonicalizedSecretKeyRing secRing = mDatabaseInteractor.getCanonicalizedSecretKeyRing(keyId); Iterator it = secRing.secretKeyIterator().iterator(); @@ -153,9 +156,8 @@ public class KeyRepositorySaveTest { Assert.assertTrue("canCertify() should be true", key.canCertify()); Assert.assertTrue("canSign() should be true", key.canSign()); - // cached Assert.assertEquals("all subkeys from CachedPublicKeyRing should be divert-to-key", - SecretKeyType.DIVERT_TO_CARD, cachedRing.getSecretKeyType(key.getKeyId())); + SecretKeyType.DIVERT_TO_CARD, mDatabaseInteractor.getSecretKeyType(key.getKeyId())); } { // second subkey @@ -169,7 +171,7 @@ public class KeyRepositorySaveTest { // cached Assert.assertEquals("all subkeys from CachedPublicKeyRing should be divert-to-key", - SecretKeyType.DIVERT_TO_CARD, cachedRing.getSecretKeyType(key.getKeyId())); + SecretKeyType.DIVERT_TO_CARD, mDatabaseInteractor.getSecretKeyType(key.getKeyId())); } { // third subkey @@ -183,7 +185,7 @@ public class KeyRepositorySaveTest { // cached Assert.assertEquals("all subkeys from CachedPublicKeyRing should be divert-to-key", - SecretKeyType.DIVERT_TO_CARD, cachedRing.getSecretKeyType(key.getKeyId())); + SecretKeyType.DIVERT_TO_CARD, mDatabaseInteractor.getSecretKeyType(key.getKeyId())); } Assert.assertFalse("keyring should have 3 subkeys (4)", it.hasNext()); @@ -233,14 +235,14 @@ public class KeyRepositorySaveTest { Assert.assertTrue("master key should have sign flag", ring.getPublicKey().canSign()); Assert.assertTrue("master key should have encrypt flag", ring.getPublicKey().canEncrypt()); - signId = mDatabaseInteractor.getCachedPublicKeyRing(masterKeyId).getSecretSignId(); + signId = mDatabaseInteractor.getSecretSignId(masterKeyId); Assert.assertNotEquals("encrypt id should not be 0", 0, signId); - Assert.assertNotEquals("encrypt key should be different from master key", masterKeyId, signId); + Assert.assertNotEquals("signing key should be different from master key", masterKeyId, signId); } { - CachedPublicKeyRing ring = mDatabaseInteractor.getCachedPublicKeyRing(masterKeyId); - Assert.assertEquals("signing key should be same id cached as uncached", signId, ring.getSecretSignId()); + Assert.assertEquals("signing key should be same id cached as uncached", + signId, mDatabaseInteractor.getSecretSignId(masterKeyId)); } } diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/remote/KeychainExternalProviderTest.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/remote/KeychainExternalProviderTest.java index e27a97ac9..27ffb3ffd 100644 --- a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/remote/KeychainExternalProviderTest.java +++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/remote/KeychainExternalProviderTest.java @@ -18,14 +18,16 @@ import org.robolectric.shadows.ShadowBinder; import org.robolectric.shadows.ShadowLog; import org.robolectric.shadows.ShadowPackageManager; import org.sufficientlysecure.keychain.KeychainTestRunner; +import org.sufficientlysecure.keychain.model.ApiApp; +import org.sufficientlysecure.keychain.model.AutocryptPeer.GossipOrigin; import org.sufficientlysecure.keychain.operations.CertifyOperation; import org.sufficientlysecure.keychain.operations.results.CertifyResult; import org.sufficientlysecure.keychain.operations.results.SaveKeyringResult; import org.sufficientlysecure.keychain.pgp.UncachedKeyRing; -import org.sufficientlysecure.keychain.provider.ApiDataAccessObject; -import org.sufficientlysecure.keychain.provider.AutocryptPeerDataAccessObject; +import org.sufficientlysecure.keychain.daos.ApiAppDao; +import org.sufficientlysecure.keychain.daos.AutocryptPeerDao; import org.sufficientlysecure.keychain.provider.KeyRepositorySaveTest; -import org.sufficientlysecure.keychain.provider.KeyWritableRepository; +import org.sufficientlysecure.keychain.daos.KeyWritableRepository; import org.sufficientlysecure.keychain.provider.KeychainExternalContract; import org.sufficientlysecure.keychain.provider.KeychainExternalContract.AutocryptStatus; import org.sufficientlysecure.keychain.provider.KeychainExternalContract.EmailStatus; @@ -61,8 +63,8 @@ public class KeychainExternalProviderTest { KeyWritableRepository.create(RuntimeEnvironment.application); ContentResolver contentResolver = RuntimeEnvironment.application.getContentResolver(); ApiPermissionHelper apiPermissionHelper; - ApiDataAccessObject apiDao; - AutocryptPeerDataAccessObject autocryptPeerDao; + ApiAppDao apiAppDao; + AutocryptPeerDao autocryptPeerDao; @Before @@ -78,16 +80,16 @@ public class KeychainExternalProviderTest { ShadowBinder.setCallingUid(PACKAGE_UID); - apiDao = new ApiDataAccessObject(RuntimeEnvironment.application); - apiPermissionHelper = new ApiPermissionHelper(RuntimeEnvironment.application, apiDao); - autocryptPeerDao = new AutocryptPeerDataAccessObject(RuntimeEnvironment.application, PACKAGE_NAME); + apiAppDao = ApiAppDao.getInstance(RuntimeEnvironment.application); + apiPermissionHelper = new ApiPermissionHelper(RuntimeEnvironment.application, apiAppDao); + autocryptPeerDao = AutocryptPeerDao.getInstance(RuntimeEnvironment.application); - apiDao.insertApiApp(new AppSettings(PACKAGE_NAME, PACKAGE_SIGNATURE)); + apiAppDao.insertApiApp(ApiApp.create(PACKAGE_NAME, PACKAGE_SIGNATURE)); } @Test(expected = AccessControlException.class) public void testPermission__withMissingPackage() throws Exception { - apiDao.deleteApiApp(PACKAGE_NAME); + apiAppDao.deleteApiApp(PACKAGE_NAME); contentResolver.query( EmailStatus.CONTENT_URI, @@ -98,8 +100,8 @@ public class KeychainExternalProviderTest { @Test(expected = AccessControlException.class) public void testPermission__withWrongPackageCert() throws Exception { - apiDao.deleteApiApp(PACKAGE_NAME); - apiDao.insertApiApp(new AppSettings(PACKAGE_NAME, new byte[] { 1, 2, 4 })); + apiAppDao.deleteApiApp(PACKAGE_NAME); + apiAppDao.insertApiApp(ApiApp.create(PACKAGE_NAME, new byte[] { 1, 2, 4 })); contentResolver.query( EmailStatus.CONTENT_URI, @@ -207,7 +209,8 @@ public class KeychainExternalProviderTest { insertSecretKeyringFrom("/test-keys/testring.sec"); insertPublicKeyringFrom("/test-keys/testring.pub"); - autocryptPeerDao.updateKey(AUTOCRYPT_PEER, new Date(), KEY_ID_PUBLIC, false); + autocryptPeerDao.insertOrUpdateLastSeen(PACKAGE_NAME, "tid", new Date()); + autocryptPeerDao.updateKey(PACKAGE_NAME, AUTOCRYPT_PEER, new Date(), KEY_ID_PUBLIC, false); Cursor cursor = contentResolver.query( AutocryptStatus.CONTENT_URI, new String[] { @@ -234,7 +237,8 @@ public class KeychainExternalProviderTest { insertSecretKeyringFrom("/test-keys/testring.sec"); insertPublicKeyringFrom("/test-keys/testring.pub"); - autocryptPeerDao.updateKey(AUTOCRYPT_PEER, new Date(), KEY_ID_PUBLIC, true); + autocryptPeerDao.insertOrUpdateLastSeen(PACKAGE_NAME, "tid", new Date()); + autocryptPeerDao.updateKey(PACKAGE_NAME, AUTOCRYPT_PEER, new Date(), KEY_ID_PUBLIC, true); Cursor cursor = contentResolver.query( AutocryptStatus.CONTENT_URI, new String[] { @@ -261,7 +265,8 @@ public class KeychainExternalProviderTest { insertSecretKeyringFrom("/test-keys/testring.sec"); insertPublicKeyringFrom("/test-keys/testring.pub"); - autocryptPeerDao.updateKey("tid", new Date(), KEY_ID_PUBLIC, false); + autocryptPeerDao.insertOrUpdateLastSeen(PACKAGE_NAME, "tid", new Date()); + autocryptPeerDao.updateKey(PACKAGE_NAME, AUTOCRYPT_PEER, new Date(), KEY_ID_PUBLIC, false); certifyKey(KEY_ID_SECRET, KEY_ID_PUBLIC, USER_ID_1); Cursor cursor = contentResolver.query( @@ -305,8 +310,9 @@ public class KeychainExternalProviderTest { insertSecretKeyringFrom("/test-keys/testring.sec"); insertPublicKeyringFrom("/test-keys/testring.pub"); - autocryptPeerDao.updateKeyGossipFromAutocrypt("tid", new Date(), KEY_ID_PUBLIC); - autocryptPeerDao.delete("tid"); + autocryptPeerDao.insertOrUpdateLastSeen(PACKAGE_NAME, "tid", new Date()); + autocryptPeerDao.updateKeyGossip(PACKAGE_NAME, "tid", new Date(), KEY_ID_PUBLIC, GossipOrigin.GOSSIP_HEADER); + autocryptPeerDao.deleteByIdentifier(PACKAGE_NAME, "tid"); Cursor cursor = contentResolver.query( AutocryptStatus.CONTENT_URI, new String[] { @@ -330,7 +336,8 @@ public class KeychainExternalProviderTest { insertSecretKeyringFrom("/test-keys/testring.sec"); insertPublicKeyringFrom("/test-keys/testring.pub"); - autocryptPeerDao.updateKeyGossipFromAutocrypt(AUTOCRYPT_PEER, new Date(), KEY_ID_PUBLIC); + autocryptPeerDao.insertOrUpdateLastSeen(PACKAGE_NAME, "tid", new Date()); + autocryptPeerDao.updateKeyGossip(PACKAGE_NAME, AUTOCRYPT_PEER, new Date(), KEY_ID_PUBLIC, GossipOrigin.GOSSIP_HEADER); certifyKey(KEY_ID_SECRET, KEY_ID_PUBLIC, USER_ID_1); Cursor cursor = contentResolver.query( diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/shadows/ShadowWorkManager.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/shadows/ShadowWorkManager.java new file mode 100644 index 000000000..f45ba8f6e --- /dev/null +++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/shadows/ShadowWorkManager.java @@ -0,0 +1,19 @@ +package org.sufficientlysecure.keychain.shadows; + + +import androidx.work.WorkManager; +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; + +import static org.mockito.Mockito.mock; + + +@Implements(WorkManager.class) +public class ShadowWorkManager { + + @Implementation + public static WorkManager getInstance() { + return mock(WorkManager.class); + } + +} diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/ssh/SshPublicKeyTest.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/ssh/SshPublicKeyTest.java index 088105ffc..296d4b1f2 100644 --- a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/ssh/SshPublicKeyTest.java +++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/ssh/SshPublicKeyTest.java @@ -31,8 +31,8 @@ import org.sufficientlysecure.keychain.KeychainTestRunner; import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKey; import org.sufficientlysecure.keychain.pgp.SshPublicKey; import org.sufficientlysecure.keychain.pgp.UncachedKeyRing; -import org.sufficientlysecure.keychain.provider.KeyRepository; -import org.sufficientlysecure.keychain.provider.KeyWritableRepository; +import org.sufficientlysecure.keychain.daos.KeyRepository; +import org.sufficientlysecure.keychain.daos.KeyWritableRepository; import org.sufficientlysecure.keychain.support.KeyringTestingHelper; import org.sufficientlysecure.keychain.util.Passphrase; @@ -76,7 +76,7 @@ public class SshPublicKeyTest { KeyRepository keyRepository = KeyRepository.create(RuntimeEnvironment.application); long masterKeyId = mStaticRingEcDsa.getMasterKeyId(); - long authSubKeyId = keyRepository.getCachedPublicKeyRing(masterKeyId).getSecretAuthenticationId(); + long authSubKeyId = keyRepository.getSecretAuthenticationId(masterKeyId); CanonicalizedPublicKey canonicalizedPublicKey = keyRepository.getCanonicalizedPublicKeyRing(masterKeyId) .getPublicKey(authSubKeyId); diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/support/KeyringTestingHelper.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/support/KeyringTestingHelper.java index 284895203..054b7617e 100644 --- a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/support/KeyringTestingHelper.java +++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/support/KeyringTestingHelper.java @@ -37,8 +37,8 @@ import org.bouncycastle.util.Arrays; import org.sufficientlysecure.keychain.operations.results.SaveKeyringResult; import org.sufficientlysecure.keychain.pgp.UncachedKeyRing; import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; -import org.sufficientlysecure.keychain.provider.KeyRepository; -import org.sufficientlysecure.keychain.provider.KeyWritableRepository; +import org.sufficientlysecure.keychain.daos.KeyRepository; +import org.sufficientlysecure.keychain.daos.KeyWritableRepository; /** Helper methods for keyring tests. */ public class KeyringTestingHelper { diff --git a/build.gradle b/build.gradle index 7ca9e2344..84f10a228 100644 --- a/build.gradle +++ b/build.gradle @@ -6,10 +6,11 @@ buildscript { dependencies { // NOTE: Always use fixed version codes not dynamic ones, e.g. 0.7.3 instead of 0.7.+, see README for more information - classpath 'com.android.tools.build:gradle:3.0.1' + classpath 'com.android.tools.build:gradle:3.1.3' classpath files('gradle-witness.jar') // bintray dependency to satisfy dependency of openpgp-api lib classpath 'com.novoda:bintray-release:0.8.0' + classpath 'com.squareup.sqldelight:gradle-plugin:0.7.0' } } @@ -17,6 +18,7 @@ allprojects { repositories { jcenter() google() + maven { url "https://jitpack.io" } } } diff --git a/extern/MaterialChipsInput/.gitignore b/extern/MaterialChipsInput/.gitignore new file mode 100644 index 000000000..09b993d06 --- /dev/null +++ b/extern/MaterialChipsInput/.gitignore @@ -0,0 +1,8 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild diff --git a/extern/MaterialChipsInput/build.gradle b/extern/MaterialChipsInput/build.gradle new file mode 100644 index 000000000..759457a57 --- /dev/null +++ b/extern/MaterialChipsInput/build.gradle @@ -0,0 +1,36 @@ +apply plugin: 'com.android.library' + +android { + compileSdkVersion 27 + buildToolsVersion "27.0.3" + + defaultConfig { + minSdkVersion 15 + targetSdkVersion 27 + versionCode 114 + versionName "1.1.4" + + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) + androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { + exclude group: 'com.android.support', module: 'support-annotations' + }) + compile 'com.android.support:appcompat-v7:27.1.1' + testCompile 'junit:junit:4.12' + + // recycler + compile 'com.android.support:recyclerview-v7:27.1.1' + compile 'com.beloo.widget:ChipsLayoutManager:0.3.7@aar' +} + diff --git a/extern/MaterialChipsInput/proguard-rules.pro b/extern/MaterialChipsInput/proguard-rules.pro new file mode 100644 index 000000000..0ae68584d --- /dev/null +++ b/extern/MaterialChipsInput/proguard-rules.pro @@ -0,0 +1,25 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /Users/couleurwhatever/Library/Android/sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/extern/MaterialChipsInput/src/androidTest/java/org/sufficientlysecure/materialchips/ExampleInstrumentedTest.java b/extern/MaterialChipsInput/src/androidTest/java/org/sufficientlysecure/materialchips/ExampleInstrumentedTest.java new file mode 100644 index 000000000..005243292 --- /dev/null +++ b/extern/MaterialChipsInput/src/androidTest/java/org/sufficientlysecure/materialchips/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package org.sufficientlysecure.materialchips; + +import android.content.Context; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumentation test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() throws Exception { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getTargetContext(); + + assertEquals("org.sufficientlysecure.library.test", appContext.getPackageName()); + } +} diff --git a/extern/MaterialChipsInput/src/main/AndroidManifest.xml b/extern/MaterialChipsInput/src/main/AndroidManifest.xml new file mode 100644 index 000000000..1bcab1dfc --- /dev/null +++ b/extern/MaterialChipsInput/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + diff --git a/extern/MaterialChipsInput/src/main/java/org/sufficientlysecure/materialchips/ChipView.java b/extern/MaterialChipsInput/src/main/java/org/sufficientlysecure/materialchips/ChipView.java new file mode 100644 index 000000000..f20c523c9 --- /dev/null +++ b/extern/MaterialChipsInput/src/main/java/org/sufficientlysecure/materialchips/ChipView.java @@ -0,0 +1,399 @@ +package org.sufficientlysecure.materialchips; + + +import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.TypedArray; +import android.graphics.PorterDuff; +import android.graphics.drawable.Drawable; +import android.support.annotation.ColorInt; +import android.support.v4.content.ContextCompat; +import android.util.AttributeSet; +import android.view.View; +import android.widget.ImageButton; +import android.widget.LinearLayout; +import android.widget.RelativeLayout; +import android.widget.TextView; + +import org.sufficientlysecure.materialchips.model.ChipInterface; +import org.sufficientlysecure.materialchips.util.LetterTileProvider; +import org.sufficientlysecure.materialchips.util.ViewUtil; + +public class ChipView extends RelativeLayout { + + private static final String TAG = ChipView.class.toString(); + // context + private Context mContext; + // xml elements + private LinearLayout mContentLayout; + private TextView mLabelTextView; + private ImageButton mDeleteButton; + // attributes + private static final int NONE = -1; + private String mLabel; + private ColorStateList mLabelColor; + private boolean mDeletable = false; + private Drawable mDeleteIcon; + private ColorStateList mDeleteIconColor; + private ColorStateList mBackgroundColor; + // letter tile provider + private LetterTileProvider mLetterTileProvider; + // chip + private ChipInterface mChip; + + public ChipView(Context context) { + super(context); + mContext = context; + init(null); + } + + public ChipView(Context context, AttributeSet attrs) { + super(context, attrs); + mContext = context; + init(attrs); + } + + /** + * Inflate the view according to attributes + * + * @param attrs the attributes + */ + private void init(AttributeSet attrs) { + // inflate layout + View rootView = inflate(getContext(), R.layout.chip_view, this); + + mContentLayout = (LinearLayout) rootView.findViewById(R.id.content); + mLabelTextView = (TextView) rootView.findViewById(R.id.label); + mDeleteButton = (ImageButton) rootView.findViewById(R.id.delete_button); + + // letter tile provider + mLetterTileProvider = new LetterTileProvider(mContext); + + // attributes + if (attrs != null) { + TypedArray a = mContext.getTheme().obtainStyledAttributes( + attrs, + R.styleable.ChipView, + 0, 0); + + try { + // label + mLabel = a.getString(R.styleable.ChipView_label); + mLabelColor = a.getColorStateList(R.styleable.ChipView_labelColor); + mDeletable = a.getBoolean(R.styleable.ChipView_deletable, false); + mDeleteIconColor = a.getColorStateList(R.styleable.ChipView_deleteIconColor); + int deleteIconId = a.getResourceId(R.styleable.ChipView_deleteIcon, NONE); + if (deleteIconId != NONE) + mDeleteIcon = ContextCompat.getDrawable(mContext, deleteIconId); + // background color + mBackgroundColor = a.getColorStateList(R.styleable.ChipView_backgroundColor); + } finally { + a.recycle(); + } + } + + // inflate + inflateWithAttributes(); + } + + /** + * Inflate the view + */ + private void inflateWithAttributes() { + // label + setLabel(mLabel); + if (mLabelColor != null) + setLabelColor(mLabelColor); + + // delete button + setDeletable(mDeletable); + + // background color + if (mBackgroundColor != null) + setChipBackgroundColor(mBackgroundColor); + } + + public void inflate(ChipInterface chip) { + mChip = chip; + // label + mLabel = mChip.getLabel(); + // inflate + inflateWithAttributes(); + } + + /** + * Get label + * + * @return the label + */ + public String getLabel() { + return mLabel; + } + + /** + * Set label + * + * @param label the label to set + */ + public void setLabel(String label) { + mLabel = label; + mLabelTextView.setText(label); + } + + /** + * Set label color + * + * @param color the color to set + */ + public void setLabelColor(ColorStateList color) { + mLabelColor = color; + mLabelTextView.setTextColor(color); + } + + /** + * Set label color + * + * @param color the color to set + */ + public void setLabelColor(@ColorInt int color) { + mLabelColor = ColorStateList.valueOf(color); + mLabelTextView.setTextColor(color); + } + +// /** +// * Show or hide avatar icon +// * +// * @param hasAvatarIcon true to show, false to hide +// */ +// public void setHasAvatarIcon(boolean hasAvatarIcon) { +// mHasAvatarIcon = hasAvatarIcon; +// +// if(!mHasAvatarIcon) { +// // hide icon +// mAvatarIconImageView.setVisibility(GONE); +// // adjust padding +// if(mDeleteButton.getVisibility() == VISIBLE) +// mLabelTextView.setPadding(ViewUtil.dpToPx(12), 0, 0, 0); +// else +// mLabelTextView.setPadding(ViewUtil.dpToPx(12), 0, ViewUtil.dpToPx(12), 0); +// +// } +// else { +// // show icon +// mAvatarIconImageView.setVisibility(VISIBLE); +// // adjust padding +// if(mDeleteButton.getVisibility() == VISIBLE) +// mLabelTextView.setPadding(ViewUtil.dpToPx(8), 0, 0, 0); +// else +// mLabelTextView.setPadding(ViewUtil.dpToPx(8), 0, ViewUtil.dpToPx(12), 0); +// +// // set icon +// if(mAvatarIconUri != null) +// mAvatarIconImageView.setImageURI(mAvatarIconUri); +// else if(mAvatarIconDrawable != null) +// mAvatarIconImageView.setImageDrawable(mAvatarIconDrawable); +// else +// mAvatarIconImageView.setImageBitmap(mLetterTileProvider.getLetterTile(getLabel())); +// } +// } + +// /** +// * Set avatar icon +// * +// * @param avatarIcon the icon to set +// */ +// public void setAvatarIcon(Drawable avatarIcon) { +// mAvatarIconDrawable = avatarIcon; +// mHasAvatarIcon = true; +// inflateWithAttributes(); +// } + +// /** +// * Set avatar icon +// * +// * @param avatarUri the uri of the icon to set +// */ +// public void setAvatarIcon(Uri avatarUri) { +// mAvatarIconUri = avatarUri; +// mHasAvatarIcon = true; +// inflateWithAttributes(); +// } + + /** + * Show or hide delte button + * + * @param deletable true to show, false to hide + */ + public void setDeletable(boolean deletable) { + mDeletable = deletable; + if (!mDeletable) { + // hide delete icon + mDeleteButton.setVisibility(GONE); + // adjust padding + mLabelTextView.setPadding(ViewUtil.dpToPx(12), 0, ViewUtil.dpToPx(12), 0); + } else { + // show icon + mDeleteButton.setVisibility(VISIBLE); + // adjust padding + mLabelTextView.setPadding(ViewUtil.dpToPx(12), 0, 0, 0); + + // set icon + if (mDeleteIcon != null) + mDeleteButton.setImageDrawable(mDeleteIcon); + if (mDeleteIconColor != null) + mDeleteButton.getDrawable().mutate().setColorFilter(mDeleteIconColor.getDefaultColor(), PorterDuff.Mode.SRC_ATOP); + } + } + + /** + * Set delete icon color + * + * @param color the color to set + */ + public void setDeleteIconColor(ColorStateList color) { + mDeleteIconColor = color; + mDeletable = true; + inflateWithAttributes(); + } + + /** + * Set delete icon color + * + * @param color the color to set + */ + public void setDeleteIconColor(@ColorInt int color) { + mDeleteIconColor = ColorStateList.valueOf(color); + mDeletable = true; + inflateWithAttributes(); + } + + /** + * Set delete icon + * + * @param deleteIcon the icon to set + */ + public void setDeleteIcon(Drawable deleteIcon) { + mDeleteIcon = deleteIcon; + mDeletable = true; + inflateWithAttributes(); + } + + /** + * Set background color + * + * @param color the color to set + */ + public void setChipBackgroundColor(ColorStateList color) { + mBackgroundColor = color; + setChipBackgroundColor(color.getDefaultColor()); + } + + /** + * Set background color + * + * @param color the color to set + */ + public void setChipBackgroundColor(@ColorInt int color) { + mBackgroundColor = ColorStateList.valueOf(color); + mContentLayout.getBackground().setColorFilter(color, PorterDuff.Mode.SRC_ATOP); + } + + /** + * Set the chip object + * + * @param chip the chip + */ + public void setChip(ChipInterface chip) { + mChip = chip; + } + + /** + * Set OnClickListener on the delete button + * + * @param onClickListener the OnClickListener + */ + public void setOnDeleteClicked(OnClickListener onClickListener) { + mDeleteButton.setOnClickListener(onClickListener); + } + + /** + * Set OnclickListener on the entire chip + * + * @param onClickListener the OnClickListener + */ + public void setOnChipClicked(OnClickListener onClickListener) { + mContentLayout.setOnClickListener(onClickListener); + } + + /** + * Builder class + */ + public static class Builder { + private Context context; + private String label; + private ColorStateList labelColor; + private boolean deletable = false; + private Drawable deleteIcon; + private ColorStateList deleteIconColor; + private ColorStateList backgroundColor; + private ChipInterface chip; + + public Builder(Context context) { + this.context = context; + } + + public Builder label(String label) { + this.label = label; + return this; + } + + public Builder labelColor(ColorStateList labelColor) { + this.labelColor = labelColor; + return this; + } + + public Builder deletable(boolean deletable) { + this.deletable = deletable; + return this; + } + + public Builder deleteIcon(Drawable deleteIcon) { + this.deleteIcon = deleteIcon; + return this; + } + + public Builder deleteIconColor(ColorStateList deleteIconColor) { + this.deleteIconColor = deleteIconColor; + return this; + } + + public Builder backgroundColor(ColorStateList backgroundColor) { + this.backgroundColor = backgroundColor; + return this; + } + + public Builder chip(ChipInterface chip) { + this.chip = chip; + this.label = chip.getLabel(); + return this; + } + + public ChipView build() { + return newInstance(this); + } + } + + private static ChipView newInstance(Builder builder) { + ChipView chipView = new ChipView(builder.context); + chipView.mLabel = builder.label; + chipView.mLabelColor = builder.labelColor; + chipView.mDeletable = builder.deletable; + chipView.mDeleteIcon = builder.deleteIcon; + chipView.mDeleteIconColor = builder.deleteIconColor; + chipView.mBackgroundColor = builder.backgroundColor; + chipView.mChip = builder.chip; + chipView.inflateWithAttributes(); + + return chipView; + } +} diff --git a/extern/MaterialChipsInput/src/main/java/org/sufficientlysecure/materialchips/ChipsInput.java b/extern/MaterialChipsInput/src/main/java/org/sufficientlysecure/materialchips/ChipsInput.java new file mode 100644 index 000000000..bc0c83e13 --- /dev/null +++ b/extern/MaterialChipsInput/src/main/java/org/sufficientlysecure/materialchips/ChipsInput.java @@ -0,0 +1,332 @@ +package org.sufficientlysecure.materialchips; + + +import java.util.ArrayList; +import java.util.List; + +import android.app.Activity; +import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.TypedArray; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.RecyclerView.ViewHolder; +import android.text.Editable; +import android.text.InputType; +import android.text.TextWatcher; +import android.util.AttributeSet; +import android.view.KeyEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.EditorInfo; +import android.widget.Filter.FilterListener; +import android.widget.RelativeLayout; +import android.widget.TextView; + +import com.beloo.widget.chipslayoutmanager.ChipsLayoutManager; +import org.sufficientlysecure.materialchips.RecyclerItemClickListener.OnItemClickListener; +import org.sufficientlysecure.materialchips.adapter.ChipsAdapter; +import org.sufficientlysecure.materialchips.adapter.FilterableAdapter; +import org.sufficientlysecure.materialchips.adapter.FilterableAdapter.FilterableItem; +import org.sufficientlysecure.materialchips.util.ActivityUtil; +import org.sufficientlysecure.materialchips.util.ClickOutsideCallback; +import org.sufficientlysecure.materialchips.util.ViewUtil; +import org.sufficientlysecure.materialchips.views.ChipsInputEditText; +import org.sufficientlysecure.materialchips.views.DropdownListView; +import org.sufficientlysecure.materialchips.views.ScrollViewMaxHeight; + +public abstract class ChipsInput extends ScrollViewMaxHeight { + private Context mContext; + + // attributes + private String mHint; + private ColorStateList mHintColor; + private ColorStateList mTextColor; + private int mMaxRows = 2; + private boolean mShowChipDetailed = true; + + private List> mChipsListenerList = new ArrayList<>(); + + private ChipsAdapter chipsAdapter; + private RecyclerView chipsRecyclerView; + private ChipsInputEditText chipsInputEditText; + + private ChipDropdownAdapter filterableAdapter; + private ViewGroup filterableListLayout; + private DropdownListView mDropdownListView; + + public ChipsInput(Context context) { + super(context); + mContext = context; + init(null); + } + + public ChipsInput(Context context, AttributeSet attrs) { + super(context, attrs); + mContext = context; + init(attrs); + } + + /** + * Inflate the view according to attributes + * + * @param attrs the attributes + */ + private void init(AttributeSet attrs) { + // inflate filterableListLayout + View rootView = inflate(getContext(), R.layout.chips_input, this); + + chipsRecyclerView = rootView.findViewById(R.id.chips_recycler); + + initEditText(); + + // attributes + if (attrs != null) { + TypedArray a = mContext.getTheme().obtainStyledAttributes( + attrs, + R.styleable.ChipsInput, + 0, 0); + + try { + // hint + mHint = a.getString(R.styleable.ChipsInput_hint); + mHintColor = a.getColorStateList(R.styleable.ChipsInput_hintColor); + mTextColor = a.getColorStateList(R.styleable.ChipsInput_textColor); + mMaxRows = a.getInteger(R.styleable.ChipsInput_maxRows, 2); + setMaxHeight(ViewUtil.dpToPx((40 * mMaxRows) + 8)); + //setVerticalScrollBarEnabled(true); + mShowChipDetailed = a.getBoolean(R.styleable.ChipsInput_showChipDetailed, true); + // chip detailed text color + } finally { + a.recycle(); + } + } + + ChipsLayoutManager chipsLayoutManager = ChipsLayoutManager.newBuilder(mContext) + .setOrientation(ChipsLayoutManager.HORIZONTAL) + .build(); + chipsRecyclerView.setLayoutManager(chipsLayoutManager); + chipsRecyclerView.setNestedScrollingEnabled(false); + + setupClickOutsideCallback(); + } + + public void setChipsAdapter(ChipsAdapter chipsAdapter) { + this.chipsAdapter = chipsAdapter; + chipsRecyclerView.setAdapter(chipsAdapter); + } + + private void setupClickOutsideCallback() { + Activity activity = ActivityUtil.scanForActivity(mContext); + if (activity == null) { + throw new ClassCastException("android.view.Context cannot be cast to android.app.Activity"); + } + + android.view.Window.Callback originalWindowCallback = (activity).getWindow().getCallback(); + activity.getWindow().setCallback(new ClickOutsideCallback(originalWindowCallback, activity)); + } + + private void initEditText() { + chipsInputEditText = new ChipsInputEditText(mContext); + if (mHintColor != null) + chipsInputEditText.setHintTextColor(mHintColor); + if (mTextColor != null) + chipsInputEditText.setTextColor(mTextColor); + + chipsInputEditText.setLayoutParams(new RelativeLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + chipsInputEditText.setHint(mHint); + chipsInputEditText.setBackgroundResource(android.R.color.transparent); + // prevent fullscreen on landscape + chipsInputEditText.setImeOptions(EditorInfo.IME_FLAG_NO_EXTRACT_UI | EditorInfo.IME_ACTION_DONE); + chipsInputEditText.setPrivateImeOptions("nm"); + // no suggestion + chipsInputEditText.setInputType(InputType.TYPE_TEXT_VARIATION_FILTER | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); + + // handle back space + chipsInputEditText.setOnKeyListener(new View.OnKeyListener() { + @Override + public boolean onKey(View v, int keyCode, KeyEvent event) { + // backspace + if (event.getAction() == KeyEvent.ACTION_DOWN + && event.getKeyCode() == KeyEvent.KEYCODE_DEL) { + // remove last chip + if (chipsInputEditText.getText().toString().length() == 0) + chipsAdapter.removeLastChip(); + } + return false; + } + }); + + chipsInputEditText.setOnEditorActionListener(new TextView.OnEditorActionListener() { + @Override + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + if ((actionId == EditorInfo.IME_ACTION_DONE) || (event != null && event.getKeyCode() == KeyEvent.KEYCODE_ENTER)) { + ChipsInput.this.onActionDone(chipsInputEditText.getText().toString()); + } + return false; + } + }); + + // text changed + chipsInputEditText.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + ChipsInput.this.onTextChanged(s); + } + + @Override + public void afterTextChanged(Editable s) { + } + }); + } + + public void addChips(List chips) { + chipsAdapter.addChipsProgrammatically(chips); + } + + public ChipsInputEditText getEditText() { + return chipsInputEditText; + } + + public void addChipsListener(ChipsListener chipsListener) { + mChipsListenerList.add(chipsListener); + } + + public void onChipAdded(T chip, int size) { + filterableAdapter.hideItem(chip); + for (ChipsListener chipsListener : mChipsListenerList) { + chipsListener.onChipAdded(chip, size); + } + } + + public void onChipRemoved(T chip, int size) { + filterableAdapter.unhideItem(chip); + for (ChipsListener chipsListener : mChipsListenerList) { + chipsListener.onChipRemoved(chip, size); + } + } + + public void onTextChanged(CharSequence text) { + for (ChipsListener chipsListener : mChipsListenerList) { + chipsListener.onTextChanged(text); + } + + mDropdownListView.getRecyclerView().scrollToPosition(0); + + // show filterable list + if (mDropdownListView != null) { + if (text.length() > 0) { + filterDropdownList(text); + } else { + mDropdownListView.fadeOut(); + } + } + } + + public void filterDropdownList(CharSequence text) { + filterableAdapter.getFilter().filter(text, new FilterListener() { + @Override + public void onFilterComplete(int count) { + // show if there are results + if (filterableAdapter.getItemCount() > 0) + mDropdownListView.fadeIn(); + else + mDropdownListView.fadeOut(); + } + }); + } + + public void onActionDone(CharSequence text) { + for (ChipsListener chipsListener : mChipsListenerList) { + chipsListener.onActionDone(text); + } + mDropdownListView.getRecyclerView().scrollToPosition(0); + } + + public List getSelectedChipList() { + return chipsAdapter.getChipList(); + } + + public String getHint() { + return mHint; + } + + public void setHint(String mHint) { + this.mHint = mHint; + } + + public void setHintColor(ColorStateList mHintColor) { + this.mHintColor = mHintColor; + } + + public void setTextColor(ColorStateList mTextColor) { + this.mTextColor = mTextColor; + } + + public ChipsInput setMaxRows(int mMaxRows) { + this.mMaxRows = mMaxRows; + return this; + } + + public ChipsInput setShowChipDetailed(boolean mShowChipDetailed) { + this.mShowChipDetailed = mShowChipDetailed; + return this; + } + + public boolean isShowChipDetailed() { + return mShowChipDetailed; + } + + public void setFilterableListLayout(ViewGroup layout) { + this.filterableListLayout = layout; + } + + public RecyclerView getChipsRecyclerView() { + return chipsRecyclerView; + } + + public abstract static class ChipDropdownAdapter + extends FilterableAdapter { + public ChipDropdownAdapter(List itemList) { + super(itemList); + } + } + + public void setChipDropdownAdapter(final ChipDropdownAdapter filterableAdapter) { + this.filterableAdapter = filterableAdapter; + if (filterableListLayout != null) { + mDropdownListView = new DropdownListView(mContext, filterableListLayout); + } else { + mDropdownListView = new DropdownListView(mContext, this); + } + mDropdownListView.build(filterableAdapter); + chipsInputEditText.setFilterableListView(mDropdownListView); + mDropdownListView.getRecyclerView().addOnItemTouchListener(new RecyclerItemClickListener(getContext(), new OnItemClickListener() { + @Override + public void onItemClick(View view, int position) { + T item = filterableAdapter.getItem(position); + chipsAdapter.addChip(item); + } + })); + } + + public interface ChipsListener { + void onChipAdded(T chip, int newSize); + void onChipRemoved(T chip, int newSize); + void onTextChanged(CharSequence text); + void onActionDone(CharSequence text); + } + + public static abstract class SimpleChipsListener implements ChipsListener { + public void onChipAdded(T chip, int newSize) { } + public void onChipRemoved(T chip, int newSize) { } + public void onTextChanged(CharSequence text) { } + public void onActionDone(CharSequence text) { } + } +} diff --git a/extern/MaterialChipsInput/src/main/java/org/sufficientlysecure/materialchips/RecyclerItemClickListener.java b/extern/MaterialChipsInput/src/main/java/org/sufficientlysecure/materialchips/RecyclerItemClickListener.java new file mode 100644 index 000000000..0e5f13b0a --- /dev/null +++ b/extern/MaterialChipsInput/src/main/java/org/sufficientlysecure/materialchips/RecyclerItemClickListener.java @@ -0,0 +1,51 @@ +package org.sufficientlysecure.materialchips; + +import android.content.Context; +import android.support.v7.widget.RecyclerView; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.View; + +public class RecyclerItemClickListener implements RecyclerView.OnItemTouchListener { + private OnItemClickListener mListener; + private boolean mIgnoreTouch = false; + + public interface OnItemClickListener { + void onItemClick(View view, int position); + } + + private GestureDetector mGestureDetector; + + public RecyclerItemClickListener(Context context, OnItemClickListener listener) { + mListener = listener; + mGestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() { + @Override + public boolean onSingleTapUp(MotionEvent e) { + return true; + } + }); + } + + @Override + public boolean onInterceptTouchEvent(RecyclerView view, MotionEvent e) { + if (mIgnoreTouch) { + return false; + } + View childView = view.findChildViewUnder(e.getX(), e.getY()); + if (childView != null && mListener != null && mGestureDetector.onTouchEvent(e)) { + mListener.onItemClick(childView, view.getChildAdapterPosition(childView)); + return true; + } + return false; + } + + @Override + public void onTouchEvent(RecyclerView view, MotionEvent motionEvent) { + // TODO: should we move mListener.onItemClick here + } + + @Override + public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { + mIgnoreTouch = disallowIntercept; + } +} \ No newline at end of file diff --git a/extern/MaterialChipsInput/src/main/java/org/sufficientlysecure/materialchips/adapter/ChipsAdapter.java b/extern/MaterialChipsInput/src/main/java/org/sufficientlysecure/materialchips/adapter/ChipsAdapter.java new file mode 100644 index 000000000..095fdcb85 --- /dev/null +++ b/extern/MaterialChipsInput/src/main/java/org/sufficientlysecure/materialchips/adapter/ChipsAdapter.java @@ -0,0 +1,294 @@ +package org.sufficientlysecure.materialchips.adapter; + + +import java.util.ArrayList; +import java.util.List; + +import android.content.Context; +import android.os.Build; +import android.support.annotation.NonNull; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.RecyclerView.ViewHolder; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.widget.EditText; +import android.widget.RelativeLayout; + +import org.sufficientlysecure.materialchips.ChipView; +import org.sufficientlysecure.materialchips.ChipsInput; +import org.sufficientlysecure.materialchips.adapter.FilterableAdapter.FilterableItem; +import org.sufficientlysecure.materialchips.util.ViewUtil; +import org.sufficientlysecure.materialchips.views.ChipsInputEditText; +import org.sufficientlysecure.materialchips.views.DetailedChipView; + + +public abstract class ChipsAdapter + extends RecyclerView.Adapter { + private static final int TYPE_EDIT_TEXT = 0; + private static final int TYPE_ITEM = 1; + + protected Context context; + + private ChipsInput chipsInput; + private List chipList = new ArrayList<>(); + + private ChipsInputEditText editText; + private String hintLabel; + + private RecyclerView chipsRecycler; + + public ChipsAdapter(Context context, ChipsInput chipsInput) { + this.chipsInput = chipsInput; + this.chipsRecycler = chipsInput.getChipsRecyclerView(); + this.editText = chipsInput.getEditText(); + this.context = context; + + this.hintLabel = chipsInput.getHint(); + setHasStableIds(true); + } + + @Override + public int getItemCount() { + return chipList.size() + 1; + } + + protected T getItem(int position) { + return chipList.get(position); + } + + @Override + public int getItemViewType(int position) { + if (position == chipList.size()) { + return TYPE_EDIT_TEXT; + } + + return TYPE_ITEM; + } + + @Override + public long getItemId(int position) { + if (position == chipList.size()) { + return 0; + } + + FilterableItem item = getItem(position); + return item != null ? item.getId() : RecyclerView.NO_ID; + } + + private void autofitEditText() { + // min width of edit text = 50 dp + ViewGroup.LayoutParams params = editText.getLayoutParams(); + params.width = ViewUtil.dpToPx(50); + editText.setLayoutParams(params); + + // listen to change in the tree + editText.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { + + @Override + public void onGlobalLayout() { + // get right of recycler and left of edit text + int right = chipsRecycler.getRight(); + int left = editText.getLeft(); + + // edit text will fill the space + ViewGroup.LayoutParams params = editText.getLayoutParams(); + params.width = right - left - ViewUtil.dpToPx(8); + editText.setLayoutParams(params); + + // request focus + editText.requestFocus(); + + // remove the listener: + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { + editText.getViewTreeObserver().removeGlobalOnLayoutListener(this); + } else { + editText.getViewTreeObserver().removeOnGlobalLayoutListener(this); + } + } + + }); + } + + public void handleClickOnEditText(ChipView chipView, final int position) { + // delete chip + chipView.setOnDeleteClicked(new View.OnClickListener() { + @Override + public void onClick(View v) { + removeChip(position); + } + }); + + // show detailed chip + if (chipsInput.isShowChipDetailed()) { + chipView.setOnChipClicked(new View.OnClickListener() { + @Override + public void onClick(View v) { + // get chip position + int[] coord = new int[2]; + v.getLocationInWindow(coord); + + final DetailedChipView detailedChipView = getDetailedChipView(getItem(position)); + setDetailedChipViewPosition(detailedChipView, coord); + + // delete button + detailedChipView.setOnDeleteClicked(new View.OnClickListener() { + @Override + public void onClick(View v) { + removeChip(position); + detailedChipView.fadeOut(); + } + }); + } + }); + } + } + + public abstract DetailedChipView getDetailedChipView(T chip); + + private void setDetailedChipViewPosition(DetailedChipView detailedChipView, int[] coord) { + // window width + ViewGroup rootView = (ViewGroup) chipsRecycler.getRootView(); + int windowWidth = ViewUtil.getWindowWidth(context); + + // chip size + RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams( + ViewUtil.dpToPx(300), + ViewUtil.dpToPx(100)); + + layoutParams.addRule(RelativeLayout.ALIGN_PARENT_TOP); + layoutParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT); + + // align left window + if (coord[0] <= 0) { + layoutParams.leftMargin = 0; + layoutParams.topMargin = coord[1] - ViewUtil.dpToPx(13); + detailedChipView.alignLeft(); + } + // align right + else if (coord[0] + ViewUtil.dpToPx(300) > windowWidth + ViewUtil.dpToPx(13)) { + layoutParams.leftMargin = windowWidth - ViewUtil.dpToPx(300); + layoutParams.topMargin = coord[1] - ViewUtil.dpToPx(13); + detailedChipView.alignRight(); + } + // same position as chip + else { + layoutParams.leftMargin = coord[0] - ViewUtil.dpToPx(13); + layoutParams.topMargin = coord[1] - ViewUtil.dpToPx(13); + } + + // show view + rootView.addView(detailedChipView, layoutParams); + detailedChipView.fadeIn(); + } + + public void addChipsProgrammatically(List chipList) { + if (chipList != null) { + if (chipList.size() > 0) { + int chipsBeforeAdding = getItemCount(); + for (T chip : chipList) { + this.chipList.add(chip); + chipsInput.onChipAdded(chip, getItemCount()); + } + + // hide hint + editText.setHint(null); + // reset text + editText.setText(null); + + notifyItemRangeChanged(chipsBeforeAdding, chipList.size()); + } + } + } + + public void addChip(T chip) { + if (chipList.contains(chip)) { + return; + } + + chipList.add(chip); + // notify listener + chipsInput.onChipAdded(chip, chipList.size()); + // hide hint + editText.setHint(null); + // reset text + editText.setText(null); + // refresh data + notifyItemInserted(chipList.size() -1); + } + + public void removeChip(T chip) { + int position = chipList.indexOf(chip); + chipList.remove(position); + // notify listener + notifyItemRangeChanged(position, getItemCount()); + chipsInput.onChipRemoved(chip, chipList.size()); + // if 0 chip + if (chipList.size() == 0) { + editText.setHint(hintLabel); + } + // refresh data + notifyDataSetChanged(); + } + + public void removeChip(int position) { + T chip = chipList.get(position); + // remove contact + chipList.remove(position); + // notify listener + chipsInput.onChipRemoved(chip, chipList.size()); + // if 0 chip + if (chipList.size() == 0) { + editText.setHint(hintLabel); + } + // refresh data + notifyDataSetChanged(); + } + + public void removeLastChip() { + if (chipList.size() > 0) { + removeChip(chipList.get(chipList.size() - 1)); + } + } + + public List getChipList() { + return chipList; + } + + + @NonNull + @Override + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + if (viewType == TYPE_EDIT_TEXT) { + return new EditTextViewHolder(editText); + } else { + return onCreateChipViewHolder(parent, viewType); + } + } + + public abstract ViewHolder onCreateChipViewHolder(ViewGroup parent, int viewType); + + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { + if (position == chipList.size()) { + if (chipList.size() == 0) { + editText.setHint(hintLabel); + } + + autofitEditText(); + } else if (getItemCount() > 1) { + onBindChipViewHolder((VH) holder, position); + } + } + + public abstract void onBindChipViewHolder(VH holder, int position); + + protected class EditTextViewHolder extends RecyclerView.ViewHolder { + private final EditText editText; + + EditTextViewHolder(View view) { + super(view); + editText = (EditText) view; + } + } +} diff --git a/extern/MaterialChipsInput/src/main/java/org/sufficientlysecure/materialchips/adapter/FilterableAdapter.java b/extern/MaterialChipsInput/src/main/java/org/sufficientlysecure/materialchips/adapter/FilterableAdapter.java new file mode 100644 index 000000000..2731768ad --- /dev/null +++ b/extern/MaterialChipsInput/src/main/java/org/sufficientlysecure/materialchips/adapter/FilterableAdapter.java @@ -0,0 +1,100 @@ +package org.sufficientlysecure.materialchips.adapter; + + +import java.util.ArrayList; +import java.util.List; + +import android.support.v7.widget.RecyclerView; +import android.widget.Filter; +import android.widget.Filterable; + +import org.sufficientlysecure.materialchips.adapter.FilterableAdapter.FilterableItem; + +public abstract class FilterableAdapter + extends RecyclerView.Adapter implements Filterable { + private List displayedList = new ArrayList<>(); + private List hiddenItemsList = new ArrayList<>(); + private ItemFilter itemFilter; + + public FilterableAdapter(List itemList) { + itemFilter = new ItemFilter(itemList); + displayedList.addAll(itemList); + + setHasStableIds(true); + } + + @Override + public int getItemCount() { + return displayedList.size(); + } + + public T getItem(int position) { + return displayedList.get(position); + } + + @Override + public Filter getFilter() { + return itemFilter; + } + + @Override + public long getItemId(int position) { + FilterableItem item = getItem(position); + return item != null ? item.getId() : RecyclerView.NO_ID; + } + + private class ItemFilter extends Filter { + private List originalList; + private List filteredList; + + ItemFilter(List chipList) { + super(); + this.originalList = new ArrayList<>(chipList); + this.filteredList = new ArrayList<>(); + } + + @Override + protected FilterResults performFiltering(CharSequence constraint) { + filteredList.clear(); + FilterResults results = new FilterResults(); + if (constraint == null || constraint.length() == 0) { + filteredList.addAll(originalList); + } else { + String filterPattern = constraint.toString().toLowerCase().trim(); + for (T item : originalList) { + if (item.isKeptForConstraint(filterPattern)) { + filteredList.add(item); + } + } + } + + results.values = filteredList; + results.count = filteredList.size(); + return results; + } + + @Override + protected void publishResults(CharSequence constraint, FilterResults results) { + FilterableAdapter.this.displayedList.clear(); + FilterableAdapter.this.displayedList.addAll((ArrayList) results.values); + notifyDataSetChanged(); + } + } + + public void hideItem(T item) { + if (!hiddenItemsList.contains(item)) { + hiddenItemsList.add(item); + } + notifyDataSetChanged(); + } + + public void unhideItem(T item) { + hiddenItemsList.remove(item); + notifyDataSetChanged(); + } + + public interface FilterableItem { + long getId(); + boolean isKeptForConstraint(CharSequence constraint); + } +} diff --git a/extern/MaterialChipsInput/src/main/java/org/sufficientlysecure/materialchips/model/ChipInterface.java b/extern/MaterialChipsInput/src/main/java/org/sufficientlysecure/materialchips/model/ChipInterface.java new file mode 100644 index 000000000..7c0ac7d91 --- /dev/null +++ b/extern/MaterialChipsInput/src/main/java/org/sufficientlysecure/materialchips/model/ChipInterface.java @@ -0,0 +1,10 @@ +package org.sufficientlysecure.materialchips.model; + + +import org.sufficientlysecure.materialchips.adapter.FilterableAdapter.FilterableItem; + + +public interface ChipInterface extends FilterableItem { + String getLabel(); + String getInfo(); +} diff --git a/extern/MaterialChipsInput/src/main/java/org/sufficientlysecure/materialchips/simple/SimpleChip.java b/extern/MaterialChipsInput/src/main/java/org/sufficientlysecure/materialchips/simple/SimpleChip.java new file mode 100644 index 000000000..eb4d3e5c9 --- /dev/null +++ b/extern/MaterialChipsInput/src/main/java/org/sufficientlysecure/materialchips/simple/SimpleChip.java @@ -0,0 +1,48 @@ +package org.sufficientlysecure.materialchips.simple; + + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import org.sufficientlysecure.materialchips.model.ChipInterface; + + +public class SimpleChip implements ChipInterface { + private long id; + private String label; + private String info; + private String filterString; + + public SimpleChip(@NonNull long id, @NonNull String label, @Nullable String info, @Nullable String filterString) { + this.id = id; + this.label = label; + this.info = info; + this.filterString = filterString != null ? filterString.toLowerCase() : label.toLowerCase(); + } + + public SimpleChip(@NonNull String label, @Nullable String info) { + this.label = label; + this.info = info; + this.id = (label + info).hashCode(); + } + + @Override + public long getId() { + return id; + } + + @Override + public String getLabel() { + return label; + } + + @Override + public String getInfo() { + return info; + } + + @Override + public boolean isKeptForConstraint(CharSequence constraint) { + return filterString.contains(constraint); + } +} diff --git a/extern/MaterialChipsInput/src/main/java/org/sufficientlysecure/materialchips/simple/SimpleChipDropdownAdapter.java b/extern/MaterialChipsInput/src/main/java/org/sufficientlysecure/materialchips/simple/SimpleChipDropdownAdapter.java new file mode 100644 index 000000000..4595a1b97 --- /dev/null +++ b/extern/MaterialChipsInput/src/main/java/org/sufficientlysecure/materialchips/simple/SimpleChipDropdownAdapter.java @@ -0,0 +1,59 @@ +package org.sufficientlysecure.materialchips.simple; + + +import java.util.List; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import org.sufficientlysecure.materialchips.ChipsInput.ChipDropdownAdapter; +import org.sufficientlysecure.materialchips.R; +import org.sufficientlysecure.materialchips.simple.SimpleChipDropdownAdapter.ItemViewHolder; + + +public class SimpleChipDropdownAdapter extends ChipDropdownAdapter { + private final LayoutInflater layoutInflater; + + public SimpleChipDropdownAdapter(Context context, List keyInfoChips) { + super(keyInfoChips); + + layoutInflater = LayoutInflater.from(context); + } + + static class ItemViewHolder extends RecyclerView.ViewHolder { + private TextView mLabel; + private TextView mInfo; + + ItemViewHolder(View view) { + super(view); + mLabel = view.findViewById(org.sufficientlysecure.materialchips.R.id.label); + mInfo = view.findViewById(org.sufficientlysecure.materialchips.R.id.info); + } + } + + @NonNull + @Override + public ItemViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = layoutInflater.inflate(R.layout.item_list_filterable, parent, false); + return new ItemViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull ItemViewHolder holder, int position) { + SimpleChip chip = getItem(position); + + holder.mLabel.setText(chip.getLabel()); + if (chip.getInfo() != null) { + holder.mInfo.setVisibility(View.VISIBLE); + holder.mInfo.setText(chip.getInfo()); + } else { + holder.mInfo.setVisibility(View.GONE); + } + } + +} diff --git a/extern/MaterialChipsInput/src/main/java/org/sufficientlysecure/materialchips/simple/SimpleChipsAdapter.java b/extern/MaterialChipsInput/src/main/java/org/sufficientlysecure/materialchips/simple/SimpleChipsAdapter.java new file mode 100644 index 000000000..7f4d9d3b6 --- /dev/null +++ b/extern/MaterialChipsInput/src/main/java/org/sufficientlysecure/materialchips/simple/SimpleChipsAdapter.java @@ -0,0 +1,58 @@ +package org.sufficientlysecure.materialchips.simple; + + +import android.content.Context; +import android.support.v7.widget.RecyclerView; +import android.view.View; +import android.view.ViewGroup; + +import org.sufficientlysecure.materialchips.ChipView; +import org.sufficientlysecure.materialchips.ChipsInput; +import org.sufficientlysecure.materialchips.adapter.ChipsAdapter; +import org.sufficientlysecure.materialchips.simple.SimpleChipsAdapter.ItemViewHolder; +import org.sufficientlysecure.materialchips.util.ViewUtil; +import org.sufficientlysecure.materialchips.views.DetailedChipView; + + +public class SimpleChipsAdapter extends ChipsAdapter { + public SimpleChipsAdapter(Context context, ChipsInput chipsInput) { + super(context, chipsInput); + } + + class ItemViewHolder extends RecyclerView.ViewHolder { + private final ChipView chipView; + + ItemViewHolder(View view) { + super(view); + chipView = (ChipView) view; + } + } + + @Override + public ItemViewHolder onCreateChipViewHolder(ViewGroup parent, int viewType) { + int padding = ViewUtil.dpToPx(4); + ChipView chipView = new ChipView.Builder(context) + // .labelColor(mChipLabelColor) + // .deletable(mChipDeletable) + // .deleteIcon(mChipDeleteIcon) + // .deleteIconColor(mChipDeleteIconColor) + .build(); + chipView.setPadding(padding, padding, padding, padding); + + return new ItemViewHolder(chipView); + } + + @Override + public void onBindChipViewHolder(ItemViewHolder holder, int position) { + holder.chipView.inflate(getItem(position)); + handleClickOnEditText(holder.chipView, position); + } + + @Override + public DetailedChipView getDetailedChipView(SimpleChip chip) { + return new DetailedChipView.Builder(context) + .chip(chip) + .build(); + } + +} diff --git a/extern/MaterialChipsInput/src/main/java/org/sufficientlysecure/materialchips/simple/SimpleChipsInput.java b/extern/MaterialChipsInput/src/main/java/org/sufficientlysecure/materialchips/simple/SimpleChipsInput.java new file mode 100644 index 000000000..46f997b2b --- /dev/null +++ b/extern/MaterialChipsInput/src/main/java/org/sufficientlysecure/materialchips/simple/SimpleChipsInput.java @@ -0,0 +1,32 @@ +package org.sufficientlysecure.materialchips.simple; + + +import java.util.List; + +import android.content.Context; +import android.util.AttributeSet; + +import org.sufficientlysecure.materialchips.ChipsInput; + + +public class SimpleChipsInput extends ChipsInput { + public SimpleChipsInput(Context context) { + super(context); + init(); + } + + public SimpleChipsInput(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + private void init() { + SimpleChipsAdapter chipsAdapter = new SimpleChipsAdapter(getContext(), this); + setChipsAdapter(chipsAdapter); + } + + public void setData(List simpleChips) { + SimpleChipDropdownAdapter chipDropdownAdapter = new SimpleChipDropdownAdapter(getContext(), simpleChips); + setChipDropdownAdapter(chipDropdownAdapter); + } +} diff --git a/extern/MaterialChipsInput/src/main/java/org/sufficientlysecure/materialchips/util/ActivityUtil.java b/extern/MaterialChipsInput/src/main/java/org/sufficientlysecure/materialchips/util/ActivityUtil.java new file mode 100644 index 000000000..6eacc3243 --- /dev/null +++ b/extern/MaterialChipsInput/src/main/java/org/sufficientlysecure/materialchips/util/ActivityUtil.java @@ -0,0 +1,20 @@ +package org.sufficientlysecure.materialchips.util; + + +import android.app.Activity; +import android.content.Context; +import android.content.ContextWrapper; + +public class ActivityUtil { + + public static Activity scanForActivity(Context context) { + if (context == null) + return null; + else if (context instanceof Activity) + return (Activity)context; + else if (context instanceof ContextWrapper) + return scanForActivity(((ContextWrapper)context).getBaseContext()); + + return null; + } +} diff --git a/extern/MaterialChipsInput/src/main/java/org/sufficientlysecure/materialchips/util/ClickOutsideCallback.java b/extern/MaterialChipsInput/src/main/java/org/sufficientlysecure/materialchips/util/ClickOutsideCallback.java new file mode 100644 index 000000000..e66f60433 --- /dev/null +++ b/extern/MaterialChipsInput/src/main/java/org/sufficientlysecure/materialchips/util/ClickOutsideCallback.java @@ -0,0 +1,51 @@ +package org.sufficientlysecure.materialchips.util; + +import android.app.Activity; +import android.content.Context; +import android.graphics.Rect; +import android.view.MotionEvent; +import android.view.View; +import android.view.Window; +import android.view.inputmethod.InputMethodManager; + +import org.sufficientlysecure.materialchips.views.ChipsInputEditText; +import org.sufficientlysecure.materialchips.views.DetailedChipView; + +public class ClickOutsideCallback extends DelegateWindowCallback { + private Activity activity; + + public ClickOutsideCallback(Window.Callback delegateCallback, Activity activity) { + super(delegateCallback); + this.activity = activity; + } + + @Override + public boolean dispatchTouchEvent(MotionEvent motionEvent) { + if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) { + View v = activity.getCurrentFocus(); + if(v instanceof DetailedChipView) { + Rect outRect = new Rect(); + v.getGlobalVisibleRect(outRect); + if (!outRect.contains((int) motionEvent.getRawX(), (int) motionEvent.getRawY())) { + ((DetailedChipView) v).fadeOut(); + } + } + if (v instanceof ChipsInputEditText) { + Rect outRect = new Rect(); + v.getGlobalVisibleRect(outRect); + if (!outRect.contains((int) motionEvent.getRawX(), (int) motionEvent.getRawY()) + && !((ChipsInputEditText) v).isFilterableListVisible()) { + hideKeyboard(v); + } + } + } + return super.dispatchTouchEvent(motionEvent); + } + + private void hideKeyboard(View v) { + InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm != null) { + imm.hideSoftInputFromWindow(v.getWindowToken(), 0); + } + } +} diff --git a/extern/MaterialChipsInput/src/main/java/org/sufficientlysecure/materialchips/util/ColorUtil.java b/extern/MaterialChipsInput/src/main/java/org/sufficientlysecure/materialchips/util/ColorUtil.java new file mode 100644 index 000000000..4ced65b84 --- /dev/null +++ b/extern/MaterialChipsInput/src/main/java/org/sufficientlysecure/materialchips/util/ColorUtil.java @@ -0,0 +1,38 @@ +package org.sufficientlysecure.materialchips.util; + + +import android.content.Context; +import android.content.res.ColorStateList; +import android.graphics.Color; +import android.util.TypedValue; + +import org.sufficientlysecure.materialchips.R; + +public class ColorUtil { + + public static int lighter(int color, float factor) { + int red = (int) ((Color.red(color) * (1 - factor) / 255 + factor) * 255); + int green = (int) ((Color.green(color) * (1 - factor) / 255 + factor) * 255); + int blue = (int) ((Color.blue(color) * (1 - factor) / 255 + factor) * 255); + return Color.argb(Color.alpha(color), red, green, blue); + } + + public static int lighter(ColorStateList color, float factor) { + return lighter(color.getDefaultColor(), factor); + } + + public static int alpha(int color, int alpha) { + return Color.argb(alpha, Color.red(color), Color.green(color), Color.blue(color)); + } + + public static boolean isColorDark(int color){ + double darkness = 1 - (0.2126*Color.red(color) + 0.7152*Color.green(color) + 0.0722*Color.blue(color))/255; + return darkness >= 0.5; + } + + public static int getThemeAccentColor (final Context context) { + final TypedValue value = new TypedValue (); + context.getTheme ().resolveAttribute (R.attr.colorAccent, value, true); + return value.data; + } +} diff --git a/extern/MaterialChipsInput/src/main/java/org/sufficientlysecure/materialchips/util/DelegateWindowCallback.java b/extern/MaterialChipsInput/src/main/java/org/sufficientlysecure/materialchips/util/DelegateWindowCallback.java new file mode 100644 index 000000000..1f553143c --- /dev/null +++ b/extern/MaterialChipsInput/src/main/java/org/sufficientlysecure/materialchips/util/DelegateWindowCallback.java @@ -0,0 +1,147 @@ +package org.sufficientlysecure.materialchips.util; + + +import android.os.Build; +import android.support.annotation.Nullable; +import android.support.annotation.RequiresApi; +import android.view.ActionMode; +import android.view.KeyEvent; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.SearchEvent; +import android.view.View; +import android.view.Window; +import android.view.Window.Callback; +import android.view.WindowManager; +import android.view.accessibility.AccessibilityEvent; + + +public abstract class DelegateWindowCallback implements Window.Callback { + private Window.Callback delegateCallback; + + public DelegateWindowCallback(Callback delegateCallback) { + this.delegateCallback = delegateCallback; + } + + @Override + public boolean dispatchKeyEvent(KeyEvent keyEvent) { + return delegateCallback.dispatchKeyEvent(keyEvent); + } + + @Override + public boolean dispatchKeyShortcutEvent(KeyEvent keyEvent) { + return delegateCallback.dispatchKeyShortcutEvent(keyEvent); + } + + @Override + public boolean dispatchTouchEvent(MotionEvent motionEvent) { + return delegateCallback.dispatchTouchEvent(motionEvent); + } + + @Override + public boolean dispatchTrackballEvent(MotionEvent motionEvent) { + return delegateCallback.dispatchTrackballEvent(motionEvent); + } + + @Override + public boolean dispatchGenericMotionEvent(MotionEvent motionEvent) { + return delegateCallback.dispatchGenericMotionEvent(motionEvent); + } + + @Override + public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent accessibilityEvent) { + return delegateCallback.dispatchPopulateAccessibilityEvent(accessibilityEvent); + } + + @Nullable + @Override + public View onCreatePanelView(int i) { + return delegateCallback.onCreatePanelView(i); + } + + @Override + public boolean onCreatePanelMenu(int i, Menu menu) { + return delegateCallback.onCreatePanelMenu(i, menu); + } + + @Override + public boolean onPreparePanel(int i, View view, Menu menu) { + return delegateCallback.onPreparePanel(i, view, menu); + } + + @Override + public boolean onMenuOpened(int i, Menu menu) { + return delegateCallback.onMenuOpened(i, menu); + } + + @Override + public boolean onMenuItemSelected(int i, MenuItem menuItem) { + return delegateCallback.onMenuItemSelected(i, menuItem); + } + + @Override + public void onWindowAttributesChanged(WindowManager.LayoutParams layoutParams) { + delegateCallback.onWindowAttributesChanged(layoutParams); + } + + @Override + public void onContentChanged() { + delegateCallback.onContentChanged(); + } + + @Override + public void onWindowFocusChanged(boolean b) { + delegateCallback.onWindowFocusChanged(b); + } + + @Override + public void onAttachedToWindow() { + delegateCallback.onAttachedToWindow(); + } + + @Override + public void onDetachedFromWindow() { + delegateCallback.onDetachedFromWindow(); + } + + @Override + public void onPanelClosed(int i, Menu menu) { + delegateCallback.onPanelClosed(i, menu); + } + + @Override + public boolean onSearchRequested() { + return delegateCallback.onSearchRequested(); + } + + @RequiresApi(api = Build.VERSION_CODES.M) + @Override + public boolean onSearchRequested(SearchEvent searchEvent) { + return delegateCallback.onSearchRequested(searchEvent); + } + + @Nullable + @Override + public ActionMode onWindowStartingActionMode(ActionMode.Callback callback) { + return delegateCallback.onWindowStartingActionMode(callback); + } + + @RequiresApi(api = Build.VERSION_CODES.M) + @Nullable + @Override + public ActionMode onWindowStartingActionMode(ActionMode.Callback callback, int i) { + return delegateCallback.onWindowStartingActionMode(callback, i); + } + + @Override + public void onActionModeStarted(ActionMode actionMode) { + delegateCallback.onActionModeStarted(actionMode); + } + + @Override + public void onActionModeFinished(ActionMode actionMode) { + delegateCallback.onActionModeFinished(actionMode); + } + +} diff --git a/extern/MaterialChipsInput/src/main/java/org/sufficientlysecure/materialchips/util/LetterTileProvider.java b/extern/MaterialChipsInput/src/main/java/org/sufficientlysecure/materialchips/util/LetterTileProvider.java new file mode 100644 index 000000000..48e64c848 --- /dev/null +++ b/extern/MaterialChipsInput/src/main/java/org/sufficientlysecure/materialchips/util/LetterTileProvider.java @@ -0,0 +1,221 @@ +package org.sufficientlysecure.materialchips.util; + +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; +import android.graphics.Rect; +import android.graphics.Typeface; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.support.v4.content.ContextCompat; +import android.text.TextPaint; +import android.util.Log; + +import org.sufficientlysecure.materialchips.R; + +/** + * Used to create a {@link Bitmap} that contains a letter used in the English + * alphabet or digit, if there is no letter or digit available, a default image + * is shown instead + */ +public class LetterTileProvider { + + /** The number of available tile colors (see R.array.letter_tile_colors) */ + private static final int NUM_OF_TILE_COLORS = 8; + + /** The {@link TextPaint} used to draw the letter onto the tile */ + private final TextPaint mPaint = new TextPaint(); + /** The bounds that enclose the letter */ + private final Rect mBounds = new Rect(); + /** The {@link Canvas} to draw on */ + private final Canvas mCanvas = new Canvas(); + /** The first char of the name being displayed */ + private final char[] mFirstChar = new char[1]; + + /** The background colors of the tile */ + private final TypedArray mColors; + /** The font size used to display the letter */ + private final int mTileLetterFontSize; + /** The default image to display */ + private final Bitmap mDefaultBitmap; + + /** Width */ + private final int mWidth; + /** Height */ + private final int mHeight; + + /** + * Constructor for LetterTileProvider + * + * @param context The {@link Context} to use + */ + public LetterTileProvider(Context context) { + final Resources res = context.getResources(); + + mPaint.setTypeface(Typeface.create("sans-serif-light", Typeface.NORMAL)); + mPaint.setColor(Color.WHITE); + mPaint.setTextAlign(Paint.Align.CENTER); + mPaint.setAntiAlias(true); + + mColors = res.obtainTypedArray(R.array.letter_tile_colors); + mTileLetterFontSize = res.getDimensionPixelSize(R.dimen.tile_letter_font_size); + + //mDefaultBitmap = BitmapFactory.decodeResource(res, android.R.drawable.); + mDefaultBitmap = drawableToBitmap(ContextCompat.getDrawable(context, R.drawable.ic_person_white_24dp)); + mWidth = res.getDimensionPixelSize(R.dimen.letter_tile_size); + mHeight = res.getDimensionPixelSize(R.dimen.letter_tile_size); + } + + /** + * @param displayName The name used to create the letter for the tile + * @return A {@link Bitmap} that contains a letter used in the English + * alphabet or digit, if there is no letter or digit available, a + * default image is shown instead + */ + public Bitmap getLetterTile(String displayName) { + // workaround + if(displayName == null || displayName.length() == 0) + return null; + + final Bitmap bitmap = Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.ARGB_8888); + + final char firstChar = displayName.charAt(0); + + final Canvas c = mCanvas; + c.setBitmap(bitmap); + c.drawColor(pickColor(displayName)); + + if (isLetterOrDigit(firstChar)) { + mFirstChar[0] = Character.toUpperCase(firstChar); + mPaint.setTextSize(mTileLetterFontSize); + mPaint.getTextBounds(mFirstChar, 0, 1, mBounds); + c.drawText(mFirstChar, 0, 1, mWidth / 2, mHeight / 2 + + (mBounds.bottom - mBounds.top) / 2, mPaint); + } + else { + // (32 - 24) / 2 = 4 + c.drawBitmap(mDefaultBitmap, ViewUtil.dpToPx(4), ViewUtil.dpToPx(4), null); + } + return bitmap; + } + + /** + * @param displayName The name used to create the letter for the tile + * @return A circular {@link Bitmap} that contains a letter used in the English + * alphabet or digit, if there is no letter or digit available, a + * default image is shown instead + */ + public Bitmap getCircularLetterTile(String displayName) { + // workaround + if(displayName == null || displayName.length() == 0) + displayName = "."; + + final Bitmap bitmap = Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.ARGB_8888); + final char firstChar = displayName.charAt(0); + + final Canvas c = mCanvas; + c.setBitmap(bitmap); + c.drawColor(pickColor(displayName)); + + if (isLetterOrDigit(firstChar)) { + mFirstChar[0] = Character.toUpperCase(firstChar); + mPaint.setTextSize(mTileLetterFontSize); + mPaint.getTextBounds(mFirstChar, 0, 1, mBounds); + c.drawText(mFirstChar, 0, 1, mWidth / 2, mHeight / 2 + + (mBounds.bottom - mBounds.top) / 2, mPaint); + } else { + // (32 - 24) / 2 = 4 + c.drawBitmap(mDefaultBitmap, ViewUtil.dpToPx(4), ViewUtil.dpToPx(4), null); + } + return getCircularBitmap(bitmap); + } + + /** + * @param c The char to check + * @return True if c is in the English alphabet or is a digit, + * false otherwise + */ + private static boolean isLetterOrDigit(char c) { + //return 'A' <= c && c <= 'Z' || 'a' <= c && c <= 'z' || '0' <= c && c <= '9'; + return Character.isLetterOrDigit(c); + } + + /** + * @param key The key used to generate the tile color + * @return A new or previously chosen color for key used as the + * tile background color + */ + private int pickColor(String key) { + // String.hashCode() is not supposed to change across java versions, so + // this should guarantee the same key always maps to the same color + final int color = Math.abs(key.hashCode()) % NUM_OF_TILE_COLORS; + try { + return mColors.getColor(color, Color.BLACK); + } finally { + // bug with recycler view + //mColors.recycle(); + } + } + + private Bitmap getCircularBitmap(Bitmap bitmap) { + Bitmap output; + + if (bitmap.getWidth() > bitmap.getHeight()) { + output = Bitmap.createBitmap(bitmap.getHeight(), bitmap.getHeight(), Bitmap.Config.ARGB_8888); + } else { + output = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getWidth(), Bitmap.Config.ARGB_8888); + } + + Canvas canvas = new Canvas(output); + + final int color = 0xff424242; + final Paint paint = new Paint(); + final Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight()); + + float r = 0; + + if (bitmap.getWidth() > bitmap.getHeight()) { + r = bitmap.getHeight() / 2; + } else { + r = bitmap.getWidth() / 2; + } + + paint.setAntiAlias(true); + canvas.drawARGB(0, 0, 0, 0); + paint.setColor(color); + canvas.drawCircle(r, r, r, paint); + paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); + canvas.drawBitmap(bitmap, rect, rect, paint); + return output; + } + + public static Bitmap drawableToBitmap (Drawable drawable) { + Bitmap bitmap = null; + + if (drawable instanceof BitmapDrawable) { + BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable; + if(bitmapDrawable.getBitmap() != null) { + return bitmapDrawable.getBitmap(); + } + } + + if(drawable.getIntrinsicWidth() <= 0 || drawable.getIntrinsicHeight() <= 0) { + bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); // Single color bitmap will be created of 1x1 pixel + } else { + bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); + } + + Canvas canvas = new Canvas(bitmap); + drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); + drawable.draw(canvas); + return bitmap; + } + +} diff --git a/extern/MaterialChipsInput/src/main/java/org/sufficientlysecure/materialchips/util/ViewUtil.java b/extern/MaterialChipsInput/src/main/java/org/sufficientlysecure/materialchips/util/ViewUtil.java new file mode 100644 index 000000000..dbde7fdf7 --- /dev/null +++ b/extern/MaterialChipsInput/src/main/java/org/sufficientlysecure/materialchips/util/ViewUtil.java @@ -0,0 +1,81 @@ +package org.sufficientlysecure.materialchips.util; + +import android.content.Context; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.util.DisplayMetrics; +import android.view.KeyCharacterMap; +import android.view.KeyEvent; +import android.view.ViewConfiguration; + +public class ViewUtil { + + private static int windowWidthPortrait = 0; + private static int windowWidthLandscape = 0; + + public static int dpToPx(int dp) { + return (int) (dp * Resources.getSystem().getDisplayMetrics().density); + } + + public static int pxToDp(int px) { + return (int) (px / Resources.getSystem().getDisplayMetrics().density); + } + + public static int getWindowWidth(Context context) { + if(context.getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT){ + return getWindowWidthPortrait(context); + } + else { + return getWindowWidthLandscape(context); + } + } + + private static int getWindowWidthPortrait(Context context) { + if(windowWidthPortrait == 0) { + DisplayMetrics metrics = context.getResources().getDisplayMetrics(); + windowWidthPortrait = metrics.widthPixels; + } + + return windowWidthPortrait; + } + + private static int getWindowWidthLandscape(Context context) { + if(windowWidthLandscape == 0) { + DisplayMetrics metrics = context.getResources().getDisplayMetrics(); + windowWidthLandscape = metrics.widthPixels; + } + + return windowWidthLandscape; + } + + public static int getNavBarHeight(Context context) { + int result = 0; + boolean hasMenuKey = ViewConfiguration.get(context).hasPermanentMenuKey(); + boolean hasBackKey = KeyCharacterMap.deviceHasKey(KeyEvent.KEYCODE_BACK); + + if(!hasMenuKey && !hasBackKey) { + //The device has a navigation bar + Resources resources = context.getResources(); + + int orientation = context.getResources().getConfiguration().orientation; + int resourceId; + if (isTablet(context)){ + resourceId = resources.getIdentifier(orientation == Configuration.ORIENTATION_PORTRAIT ? "navigation_bar_height" : "navigation_bar_height_landscape", "dimen", "android"); + } else { + resourceId = resources.getIdentifier(orientation == Configuration.ORIENTATION_PORTRAIT ? "navigation_bar_height" : "navigation_bar_width", "dimen", "android"); + } + + if (resourceId > 0) { + return context.getResources().getDimensionPixelSize(resourceId); + } + } + return result; + } + + + private static boolean isTablet(Context context) { + return (context.getResources().getConfiguration().screenLayout + & Configuration.SCREENLAYOUT_SIZE_MASK) + >= Configuration.SCREENLAYOUT_SIZE_LARGE; + } +} diff --git a/extern/MaterialChipsInput/src/main/java/org/sufficientlysecure/materialchips/views/ChipsInputEditText.java b/extern/MaterialChipsInput/src/main/java/org/sufficientlysecure/materialchips/views/ChipsInputEditText.java new file mode 100644 index 000000000..07b910490 --- /dev/null +++ b/extern/MaterialChipsInput/src/main/java/org/sufficientlysecure/materialchips/views/ChipsInputEditText.java @@ -0,0 +1,29 @@ +package org.sufficientlysecure.materialchips.views; + + +import android.content.Context; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; + + +public class ChipsInputEditText extends android.support.v7.widget.AppCompatEditText { + + private View filterableListView; + + public ChipsInputEditText(Context context) { + super(context); + } + + public ChipsInputEditText(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public boolean isFilterableListVisible() { + return filterableListView != null && filterableListView.getVisibility() == VISIBLE; + } + + public void setFilterableListView(View filterableListView) { + this.filterableListView = filterableListView; + } +} diff --git a/extern/MaterialChipsInput/src/main/java/org/sufficientlysecure/materialchips/views/DetailedChipView.java b/extern/MaterialChipsInput/src/main/java/org/sufficientlysecure/materialchips/views/DetailedChipView.java new file mode 100644 index 000000000..cd140e11a --- /dev/null +++ b/extern/MaterialChipsInput/src/main/java/org/sufficientlysecure/materialchips/views/DetailedChipView.java @@ -0,0 +1,230 @@ +package org.sufficientlysecure.materialchips.views; + + +import android.content.Context; +import android.content.res.ColorStateList; +import android.graphics.Color; +import android.graphics.PorterDuff; +import android.support.v4.content.ContextCompat; +import android.util.AttributeSet; +import android.view.View; +import android.view.animation.AlphaAnimation; +import android.widget.ImageButton; +import android.widget.LinearLayout; +import android.widget.TextView; + +import org.sufficientlysecure.materialchips.R; +import org.sufficientlysecure.materialchips.model.ChipInterface; +import org.sufficientlysecure.materialchips.util.ColorUtil; +import org.sufficientlysecure.materialchips.util.LetterTileProvider; + + +public class DetailedChipView extends LinearLayout { + + private static final String TAG = DetailedChipView.class.toString(); + // context + private Context mContext; + // xml elements + private LinearLayout mContentLayout; + private TextView mNameTextView; + private TextView mInfoTextView; + private ImageButton mDeleteButton; + // letter tile provider + private static LetterTileProvider mLetterTileProvider; + // attributes + private ColorStateList mBackgroundColor; + + public DetailedChipView(Context context) { + super(context); + mContext = context; + init(null); + } + + public DetailedChipView(Context context, AttributeSet attrs) { + super(context, attrs); + mContext = context; + init(attrs); + } + + /** + * Inflate the view according to attributes + * + * @param attrs the attributes + */ + private void init(AttributeSet attrs) { + // inflate layout + View rootView = inflate(getContext(), R.layout.detailed_chip_view, this); + + mContentLayout = (LinearLayout) rootView.findViewById(R.id.content); + mNameTextView = (TextView) rootView.findViewById(R.id.name); + mInfoTextView = (TextView) rootView.findViewById(R.id.info); + mDeleteButton = (ImageButton) rootView.findViewById(R.id.delete_button); + + // letter tile provider + mLetterTileProvider = new LetterTileProvider(mContext); + + // hide on first + setVisibility(GONE); + // hide on touch outside + hideOnTouchOutside(); + } + + /** + * Hide the view on touch outside of it + */ + private void hideOnTouchOutside() { + // set focusable + setFocusable(true); + setFocusableInTouchMode(true); + setClickable(true); + } + + /** + * Fade in + */ + public void fadeIn() { + AlphaAnimation anim = new AlphaAnimation(0.0f, 1.0f); + anim.setDuration(200); + startAnimation(anim); + setVisibility(VISIBLE); + // focus on the view + requestFocus(); + } + + /** + * Fade out + */ + public void fadeOut() { + AlphaAnimation anim = new AlphaAnimation(1.0f, 0.0f); + anim.setDuration(200); + startAnimation(anim); + setVisibility(GONE); + // fix onclick issue + clearFocus(); + setClickable(false); + } + + public void setName(String name) { + mNameTextView.setText(name); + } + + public void setInfo(String info) { + if(info != null) { + mInfoTextView.setVisibility(VISIBLE); + mInfoTextView.setText(info); + } + else { + mInfoTextView.setVisibility(GONE); + } + } + + public void setTextColor(ColorStateList color) { + mNameTextView.setTextColor(color); + mInfoTextView.setTextColor(ColorUtil.alpha(color.getDefaultColor(), 150)); + } + + public void setBackGroundcolor(ColorStateList color) { + mBackgroundColor = color; + mContentLayout.getBackground().setColorFilter(color.getDefaultColor(), PorterDuff.Mode.SRC_ATOP); + } + + public int getBackgroundColor() { + return mBackgroundColor == null ? ContextCompat.getColor(mContext, R.color.chips_opened_bg) : mBackgroundColor.getDefaultColor(); + } + + public void setDeleteIconColor(ColorStateList color) { + mDeleteButton.getDrawable().mutate().setColorFilter(color.getDefaultColor(), PorterDuff.Mode.SRC_ATOP); + } + + public void setOnDeleteClicked(OnClickListener onClickListener) { + mDeleteButton.setOnClickListener(onClickListener); + } + + public void alignLeft() { + LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) mContentLayout.getLayoutParams(); + params.leftMargin = 0; + mContentLayout.setLayoutParams(params); + } + + public void alignRight() { + LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) mContentLayout.getLayoutParams(); + params.rightMargin = 0; + mContentLayout.setLayoutParams(params); + } + + public static class Builder { + private Context context; + private String name; + private String info; + private ColorStateList textColor; + private ColorStateList backgroundColor; + private ColorStateList deleteIconColor; + + public Builder(Context context) { + this.context = context; + } + + public Builder name(String name) { + this.name = name; + return this; + } + + public Builder info(String info) { + this.info = info; + return this; + } + + public Builder chip(ChipInterface chip) { + this.name = chip.getLabel(); + this.info = chip.getInfo(); + return this; + } + + public Builder textColor(ColorStateList textColor) { + this.textColor = textColor; + return this; + } + + public Builder backgroundColor(ColorStateList backgroundColor) { + this.backgroundColor = backgroundColor; + return this; + } + + public Builder deleteIconColor(ColorStateList deleteIconColor) { + this.deleteIconColor = deleteIconColor; + return this; + } + + public DetailedChipView build() { + return DetailedChipView.newInstance(this); + } + } + + private static DetailedChipView newInstance(Builder builder) { + DetailedChipView detailedChipView = new DetailedChipView(builder.context); + // avatar + // background color + if(builder.backgroundColor != null) + detailedChipView.setBackGroundcolor(builder.backgroundColor); + + // text color + if(builder.textColor != null) + detailedChipView.setTextColor(builder.textColor); + else if(ColorUtil.isColorDark(detailedChipView.getBackgroundColor())) + detailedChipView.setTextColor(ColorStateList.valueOf(Color.WHITE)); + else + detailedChipView.setTextColor(ColorStateList.valueOf(Color.BLACK)); + + // delete icon color + if(builder.deleteIconColor != null) + detailedChipView.setDeleteIconColor(builder.deleteIconColor); + else if(ColorUtil.isColorDark(detailedChipView.getBackgroundColor())) + detailedChipView.setDeleteIconColor(ColorStateList.valueOf(Color.WHITE)); + else + detailedChipView.setDeleteIconColor(ColorStateList.valueOf(Color.BLACK)); + + detailedChipView.setName(builder.name); + detailedChipView.setInfo(builder.info); + return detailedChipView; + } +} diff --git a/extern/MaterialChipsInput/src/main/java/org/sufficientlysecure/materialchips/views/DropdownListView.java b/extern/MaterialChipsInput/src/main/java/org/sufficientlysecure/materialchips/views/DropdownListView.java new file mode 100644 index 000000000..c36c061fd --- /dev/null +++ b/extern/MaterialChipsInput/src/main/java/org/sufficientlysecure/materialchips/views/DropdownListView.java @@ -0,0 +1,103 @@ +package org.sufficientlysecure.materialchips.views; + + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.res.Configuration; +import android.os.Build; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.view.animation.AlphaAnimation; +import android.widget.RelativeLayout; + +import org.sufficientlysecure.materialchips.R; +import org.sufficientlysecure.materialchips.adapter.FilterableAdapter; +import org.sufficientlysecure.materialchips.util.ViewUtil; + + +@SuppressLint("ViewConstructor") // this is a dropdown view, it doesn't come up in preview +public class DropdownListView extends RelativeLayout { + private RecyclerView recyclerView; + private ViewGroup rootView; + + public DropdownListView(Context context, ViewGroup layout) { + super(context); + this.rootView = layout; + init(); + } + + private void init() { + View view = inflate(getContext(), R.layout.list_filterable_view, this); + + recyclerView = view.findViewById(R.id.recycler_view); + recyclerView.setLayoutManager(new LinearLayoutManager(getContext(), LinearLayoutManager.VERTICAL, false)); + + setVisibility(GONE); + } + + public void build(FilterableAdapter filterableAdapter) { + recyclerView.setAdapter(filterableAdapter); + + rootView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + // size + RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams( + ViewUtil.getWindowWidth(getContext()), + ViewGroup.LayoutParams.WRAP_CONTENT); + + layoutParams.addRule(RelativeLayout.ALIGN_PARENT_TOP); + layoutParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT); + + if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) { + layoutParams.bottomMargin = ViewUtil.getNavBarHeight(getContext()); + } + + // If this child view is already added to the parent rootView, then remove it first + ViewGroup parent = (ViewGroup) DropdownListView.this.getParent(); + if (parent != null) { + parent.removeView(DropdownListView.this); + } + // add view + rootView.addView(DropdownListView.this, layoutParams); + + // remove the listener: + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { + rootView.getViewTreeObserver().removeGlobalOnLayoutListener(this); + } else { + rootView.getViewTreeObserver().removeOnGlobalLayoutListener(this); + } + } + + }); + } + + public RecyclerView getRecyclerView() { + return recyclerView; + } + + public void fadeIn() { + if (getVisibility() == VISIBLE) { + return; + } + + AlphaAnimation anim = new AlphaAnimation(0.0f, 1.0f); + anim.setDuration(200); + startAnimation(anim); + setVisibility(VISIBLE); + } + + public void fadeOut() { + if (getVisibility() == GONE) { + return; + } + + AlphaAnimation anim = new AlphaAnimation(1.0f, 0.0f); + anim.setDuration(200); + startAnimation(anim); + setVisibility(GONE); + } +} diff --git a/extern/MaterialChipsInput/src/main/java/org/sufficientlysecure/materialchips/views/ScrollViewMaxHeight.java b/extern/MaterialChipsInput/src/main/java/org/sufficientlysecure/materialchips/views/ScrollViewMaxHeight.java new file mode 100644 index 000000000..92fbfb131 --- /dev/null +++ b/extern/MaterialChipsInput/src/main/java/org/sufficientlysecure/materialchips/views/ScrollViewMaxHeight.java @@ -0,0 +1,49 @@ +package org.sufficientlysecure.materialchips.views; + + +import android.content.Context; +import android.content.res.TypedArray; +import android.support.v4.widget.NestedScrollView; +import android.util.AttributeSet; + +import org.sufficientlysecure.materialchips.R; +import org.sufficientlysecure.materialchips.util.ViewUtil; + +public class ScrollViewMaxHeight extends NestedScrollView { + + private int mMaxHeight; + private int mWidthMeasureSpec; + + public ScrollViewMaxHeight(Context context) { + super(context); + } + + public ScrollViewMaxHeight(Context context, AttributeSet attrs) { + super(context, attrs); + + TypedArray a = context.getTheme().obtainStyledAttributes( + attrs, + R.styleable.ScrollViewMaxHeight, + 0, 0); + + try { + mMaxHeight = a.getDimensionPixelSize(R.styleable.ScrollViewMaxHeight_maxHeight, ViewUtil.dpToPx(300)); + } + finally { + a.recycle(); + } + } + + public void setMaxHeight(int height) { + mMaxHeight = height; + int heightMeasureSpec = MeasureSpec.makeMeasureSpec(mMaxHeight, MeasureSpec.AT_MOST); + measure(mWidthMeasureSpec, heightMeasureSpec); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + mWidthMeasureSpec = widthMeasureSpec; + heightMeasureSpec = MeasureSpec.makeMeasureSpec(mMaxHeight, MeasureSpec.AT_MOST); + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } +} diff --git a/extern/MaterialChipsInput/src/main/res/drawable-v21/ripple_chip_view.xml b/extern/MaterialChipsInput/src/main/res/drawable-v21/ripple_chip_view.xml new file mode 100644 index 000000000..aea2c3a4f --- /dev/null +++ b/extern/MaterialChipsInput/src/main/res/drawable-v21/ripple_chip_view.xml @@ -0,0 +1,11 @@ + + + + + + + + + + \ No newline at end of file diff --git a/extern/MaterialChipsInput/src/main/res/drawable/bg_chip_view.xml b/extern/MaterialChipsInput/src/main/res/drawable/bg_chip_view.xml new file mode 100644 index 000000000..58b375ecc --- /dev/null +++ b/extern/MaterialChipsInput/src/main/res/drawable/bg_chip_view.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/extern/MaterialChipsInput/src/main/res/drawable/bg_chip_view_opened.xml b/extern/MaterialChipsInput/src/main/res/drawable/bg_chip_view_opened.xml new file mode 100644 index 000000000..69dbc5bde --- /dev/null +++ b/extern/MaterialChipsInput/src/main/res/drawable/bg_chip_view_opened.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/extern/MaterialChipsInput/src/main/res/drawable/ic_cancel_grey_24dp.xml b/extern/MaterialChipsInput/src/main/res/drawable/ic_cancel_grey_24dp.xml new file mode 100644 index 000000000..f9b266324 --- /dev/null +++ b/extern/MaterialChipsInput/src/main/res/drawable/ic_cancel_grey_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/extern/MaterialChipsInput/src/main/res/drawable/ic_cancel_white_24dp.xml b/extern/MaterialChipsInput/src/main/res/drawable/ic_cancel_white_24dp.xml new file mode 100644 index 000000000..e6545bf8a --- /dev/null +++ b/extern/MaterialChipsInput/src/main/res/drawable/ic_cancel_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/extern/MaterialChipsInput/src/main/res/drawable/ic_person_outline_white_24dp.xml b/extern/MaterialChipsInput/src/main/res/drawable/ic_person_outline_white_24dp.xml new file mode 100644 index 000000000..69453b4e1 --- /dev/null +++ b/extern/MaterialChipsInput/src/main/res/drawable/ic_person_outline_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/extern/MaterialChipsInput/src/main/res/drawable/ic_person_white_24dp.xml b/extern/MaterialChipsInput/src/main/res/drawable/ic_person_white_24dp.xml new file mode 100644 index 000000000..22ca15668 --- /dev/null +++ b/extern/MaterialChipsInput/src/main/res/drawable/ic_person_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/extern/MaterialChipsInput/src/main/res/drawable/ripple_chip_view.xml b/extern/MaterialChipsInput/src/main/res/drawable/ripple_chip_view.xml new file mode 100644 index 000000000..58b375ecc --- /dev/null +++ b/extern/MaterialChipsInput/src/main/res/drawable/ripple_chip_view.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/extern/MaterialChipsInput/src/main/res/layout/chip_view.xml b/extern/MaterialChipsInput/src/main/res/layout/chip_view.xml new file mode 100644 index 000000000..34e73036c --- /dev/null +++ b/extern/MaterialChipsInput/src/main/res/layout/chip_view.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/extern/MaterialChipsInput/src/main/res/layout/chips_input.xml b/extern/MaterialChipsInput/src/main/res/layout/chips_input.xml new file mode 100644 index 000000000..87f8786c2 --- /dev/null +++ b/extern/MaterialChipsInput/src/main/res/layout/chips_input.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/extern/MaterialChipsInput/src/main/res/layout/detailed_chip_view.xml b/extern/MaterialChipsInput/src/main/res/layout/detailed_chip_view.xml new file mode 100644 index 000000000..0533d3e5d --- /dev/null +++ b/extern/MaterialChipsInput/src/main/res/layout/detailed_chip_view.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/extern/MaterialChipsInput/src/main/res/layout/item_list_filterable.xml b/extern/MaterialChipsInput/src/main/res/layout/item_list_filterable.xml new file mode 100644 index 000000000..6298b696c --- /dev/null +++ b/extern/MaterialChipsInput/src/main/res/layout/item_list_filterable.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/extern/MaterialChipsInput/src/main/res/layout/list_filterable_view.xml b/extern/MaterialChipsInput/src/main/res/layout/list_filterable_view.xml new file mode 100644 index 000000000..3dc228971 --- /dev/null +++ b/extern/MaterialChipsInput/src/main/res/layout/list_filterable_view.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/extern/MaterialChipsInput/src/main/res/values/attrs.xml b/extern/MaterialChipsInput/src/main/res/values/attrs.xml new file mode 100644 index 000000000..189c0eec7 --- /dev/null +++ b/extern/MaterialChipsInput/src/main/res/values/attrs.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/extern/MaterialChipsInput/src/main/res/values/colors.xml b/extern/MaterialChipsInput/src/main/res/values/colors.xml new file mode 100644 index 000000000..da3e72dcb --- /dev/null +++ b/extern/MaterialChipsInput/src/main/res/values/colors.xml @@ -0,0 +1,9 @@ + + + + #E0E0E0 + #009688 + #ababab + #b9ffffff + + \ No newline at end of file diff --git a/extern/MaterialChipsInput/src/main/res/values/strings.xml b/extern/MaterialChipsInput/src/main/res/values/strings.xml new file mode 100644 index 000000000..9b3be0701 --- /dev/null +++ b/extern/MaterialChipsInput/src/main/res/values/strings.xml @@ -0,0 +1,20 @@ + + + + + #f16364 + #f58559 + #f9a43e + #e4c62e + #67bf74 + #59a2be + #2093cd + #ad62a7 + + + + 17sp + + 32dp + + diff --git a/extern/MaterialChipsInput/src/test/java/org/sufficientlysecure/materialchips/ExampleUnitTest.java b/extern/MaterialChipsInput/src/test/java/org/sufficientlysecure/materialchips/ExampleUnitTest.java new file mode 100644 index 000000000..f5a54cacb --- /dev/null +++ b/extern/MaterialChipsInput/src/test/java/org/sufficientlysecure/materialchips/ExampleUnitTest.java @@ -0,0 +1,17 @@ +package org.sufficientlysecure.materialchips; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() throws Exception { + assertEquals(4, 2 + 2); + } +} \ No newline at end of file diff --git a/extern/openpgp-api-lib b/extern/openpgp-api-lib index c2ddaa76b..3d77d9418 160000 --- a/extern/openpgp-api-lib +++ b/extern/openpgp-api-lib @@ -1 +1 @@ -Subproject commit c2ddaa76bbb8819dafff55ae4af00ac40c94e6fb +Subproject commit 3d77d941868e81ac2a5d6317c24a3df6739e187a diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 3482ab125..98d7a1155 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,5 +3,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.4.1-all.zip -distributionSha256Sum=dd9b24950dc4fca7d1ca5f1ccd57ca8c5b9eb407e3e6e0f48174fde4bb19ed06 +distributionUrl=https\://services.gradle.org/distributions/gradle-4.8-all.zip +distributionSha256Sum=da9600da2a28a43f5f77364deecbb9b01c1ddb7d3ecafe1d5c93bcd8a8059ab1 diff --git a/settings.gradle b/settings.gradle index 7c3b44eab..01b3c106c 100644 --- a/settings.gradle +++ b/settings.gradle @@ -4,6 +4,7 @@ include ':extern:bouncycastle:core' include ':extern:bouncycastle:pg' include ':extern:bouncycastle:prov' include ':extern:minidns' +include ':extern:MaterialChipsInput:library' // Workaround for Android Gradle Plugin 2.0, as described in http://stackoverflow.com/a/36544850 //include ':libkeychain'