diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/AutocryptPeerDataAccessObject.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/AutocryptPeerDataAccessObject.java index 32c8c3ee1..d7d4707dd 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/AutocryptPeerDataAccessObject.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/AutocryptPeerDataAccessObject.java @@ -72,10 +72,11 @@ public class AutocryptPeerDataAccessObject { 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() { @@ -102,9 +103,11 @@ public class AutocryptPeerDataAccessObject { }; } - public AutocryptPeerDataAccessObject(SimpleContentResolverInterface queryInterface, String packageName) { + public AutocryptPeerDataAccessObject(SimpleContentResolverInterface queryInterface, String packageName, + DatabaseNotifyManager databaseNotifyManager) { this.queryInterface = queryInterface; this.packageName = packageName; + this.databaseNotifyManager = databaseNotifyManager; } public Long getMasterKeyIdForAutocryptPeer(String autocryptId) { @@ -190,6 +193,7 @@ public class AutocryptPeerDataAccessObject { 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) { @@ -211,10 +215,13 @@ public class AutocryptPeerDataAccessObject { 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) { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/DatabaseNotifyManager.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/DatabaseNotifyManager.java new file mode 100644 index 000000000..b283dd576 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/DatabaseNotifyManager.java @@ -0,0 +1,42 @@ +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 index ac71b52b2..2f1847a17 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeyRepository.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeyRepository.java @@ -53,7 +53,7 @@ public class KeyRepository { public static final int FIELD_TYPE_STRING = 4; public static final int FIELD_TYPE_BLOB = 5; - final ContentResolver mContentResolver; + final ContentResolver contentResolver; final LocalPublicKeyStorage mLocalPublicKeyStorage; OperationLog mLog; int mIndent; @@ -71,7 +71,7 @@ public class KeyRepository { KeyRepository(ContentResolver contentResolver, LocalPublicKeyStorage localPublicKeyStorage, OperationLog log, int indent) { - mContentResolver = contentResolver; + this.contentResolver = contentResolver; mLocalPublicKeyStorage = localPublicKeyStorage; mIndent = indent; mLog = log; @@ -121,7 +121,7 @@ public class KeyRepository { private HashMap getGenericData(Uri uri, String[] proj, int[] types, String selection) throws NotFoundException { - Cursor cursor = mContentResolver.query(uri, proj, selection, null, null); + Cursor cursor = contentResolver.query(uri, proj, selection, null, null); try { HashMap result = new HashMap<>(proj.length); @@ -184,7 +184,7 @@ public class KeyRepository { } public CanonicalizedPublicKeyRing getCanonicalizedPublicKeyRing(Uri queryUri) throws NotFoundException { - Cursor cursor = mContentResolver.query(queryUri, + Cursor cursor = contentResolver.query(queryUri, new String[] { KeyRings.MASTER_KEY_ID, KeyRings.VERIFIED }, null, null, null); try { if (cursor != null && cursor.moveToFirst()) { @@ -208,7 +208,7 @@ public class KeyRepository { } public CanonicalizedSecretKeyRing getCanonicalizedSecretKeyRing(Uri queryUri) throws NotFoundException { - Cursor cursor = mContentResolver.query(queryUri, + 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()) { @@ -232,7 +232,7 @@ public class KeyRepository { } public ArrayList getConfirmedUserIds(long masterKeyId) throws NotFoundException { - Cursor cursor = mContentResolver.query(UserPackets.buildUserIdsUri(masterKeyId), + Cursor cursor = contentResolver.query(UserPackets.buildUserIdsUri(masterKeyId), new String[]{UserPackets.USER_ID}, UserPackets.VERIFIED + " = " + Certs.VERIFIED_SECRET, null, null ); if (cursor == null) { @@ -276,12 +276,12 @@ public class KeyRepository { } public ContentResolver getContentResolver() { - return mContentResolver; + return contentResolver; } @Nullable Long getLastUpdateTime(long masterKeyId) { - Cursor lastUpdatedCursor = mContentResolver.query( + Cursor lastUpdatedCursor = contentResolver.query( UpdatedKeys.CONTENT_URI, new String[] { UpdatedKeys.LAST_UPDATED }, UpdatedKeys.MASTER_KEY_ID + " = ?", diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeyWritableRepository.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeyWritableRepository.java index 9aff7d75a..373332d26 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeyWritableRepository.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeyWritableRepository.java @@ -83,32 +83,38 @@ import timber.log.Timber; public class KeyWritableRepository extends KeyRepository { private static final int MAX_CACHED_KEY_SIZE = 1024 * 50; - private final Context mContext; + private final Context context; private final LastUpdateInteractor lastUpdateInteractor; + private DatabaseNotifyManager databaseNotifyManager; public static KeyWritableRepository create(Context context) { LocalPublicKeyStorage localPublicKeyStorage = LocalPublicKeyStorage.getInstance(context); LastUpdateInteractor lastUpdateInteractor = LastUpdateInteractor.create(context); + DatabaseNotifyManager databaseNotifyManager = DatabaseNotifyManager.create(context); - return new KeyWritableRepository(context, localPublicKeyStorage, lastUpdateInteractor); + return new KeyWritableRepository(context, localPublicKeyStorage, lastUpdateInteractor, + databaseNotifyManager); } @VisibleForTesting KeyWritableRepository(Context context, LocalPublicKeyStorage localPublicKeyStorage, - LastUpdateInteractor lastUpdateInteractor) { - this(context, localPublicKeyStorage, lastUpdateInteractor, new OperationLog(), 0); + LastUpdateInteractor lastUpdateInteractor, DatabaseNotifyManager databaseNotifyManager) { + this(context, localPublicKeyStorage, lastUpdateInteractor, new OperationLog(), 0, + databaseNotifyManager); } private KeyWritableRepository(Context context, LocalPublicKeyStorage localPublicKeyStorage, - LastUpdateInteractor lastUpdateInteractor, OperationLog log, int indent) { + LastUpdateInteractor lastUpdateInteractor, OperationLog log, int indent, + DatabaseNotifyManager databaseNotifyManager) { super(context.getContentResolver(), localPublicKeyStorage, log, indent); - mContext = context; + this.context = context; + this.databaseNotifyManager = databaseNotifyManager; this.lastUpdateInteractor = lastUpdateInteractor; } private LongSparseArray getTrustedMasterKeys() { - Cursor cursor = mContentResolver.query(KeyRings.buildUnifiedKeyRingsUri(), new String[] { + 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 @@ -530,7 +536,7 @@ public class KeyWritableRepository extends KeyRepository { try { // delete old version of this keyRing (from database only!), which also deletes all keys and userIds on cascade - int deleted = mContentResolver.delete( + int deleted = contentResolver.delete( KeyRingData.buildPublicKeyRingUri(masterKeyId), null, null); if (deleted > 0) { log(LogType.MSG_IP_DELETE_OLD_OK); @@ -540,7 +546,8 @@ public class KeyWritableRepository extends KeyRepository { } log(LogType.MSG_IP_APPLY_BATCH); - mContentResolver.applyBatch(KeychainContract.CONTENT_AUTHORITY, operations); + contentResolver.applyBatch(KeychainContract.CONTENT_AUTHORITY, operations); + databaseNotifyManager.notifyKeyChange(masterKeyId); log(LogType.MSG_IP_SUCCESS); return result; @@ -603,7 +610,7 @@ public class KeyWritableRepository extends KeyRepository { // insert new version of this keyRing Uri uri = KeyRingData.buildSecretKeyRingUri(masterKeyId); - return mContentResolver.insert(uri, values); + return contentResolver.insert(uri, values); } public boolean deleteKeyRing(long masterKeyId) { @@ -613,8 +620,11 @@ public class KeyWritableRepository extends KeyRepository { Timber.e(e, "Could not delete file!"); return false; } - mContentResolver.delete(ApiAutocryptPeer.buildByMasterKeyId(masterKeyId),null, null); - int deletedRows = mContentResolver.delete(KeyRingData.buildPublicKeyRingUri(masterKeyId), null, null); + contentResolver.delete(ApiAutocryptPeer.buildByMasterKeyId(masterKeyId),null, null); + int deletedRows = contentResolver.delete(KeyRingData.buildPublicKeyRingUri(masterKeyId), null, null); + + databaseNotifyManager.notifyKeyChange(masterKeyId); + return deletedRows > 0; } @@ -690,7 +700,7 @@ public class KeyWritableRepository extends KeyRepository { // first, mark all keys as not available ContentValues values = new ContentValues(); values.put(Keys.HAS_SECRET, SecretKeyType.GNU_DUMMY.getNum()); - mContentResolver.update(uri, values, null, null); + contentResolver.update(uri, values, null, null); // then, mark exactly the keys we have available log(LogType.MSG_IS_IMPORTING_SUBKEYS); @@ -699,7 +709,7 @@ public class KeyWritableRepository extends KeyRepository { long id = sub.getKeyId(); SecretKeyType mode = sub.getSecretKeyTypeSuperExpensive(); values.put(Keys.HAS_SECRET, mode.getNum()); - int upd = mContentResolver.update(uri, values, Keys.KEY_ID + " = ?", + int upd = contentResolver.update(uri, values, Keys.KEY_ID + " = ?", new String[]{Long.toString(id)}); if (upd == 1) { switch (mode) { @@ -1030,11 +1040,11 @@ public class KeyWritableRepository extends KeyRepository { log.add(LogType.MSG_TRUST, 0); Cursor cursor; - Preferences preferences = Preferences.getPreferences(mContext); + Preferences preferences = Preferences.getPreferences(context); boolean isTrustDbInitialized = preferences.isKeySignaturesTableInitialized(); if (!isTrustDbInitialized) { log.add(LogType.MSG_TRUST_INITIALIZE, 1); - cursor = mContentResolver.query(KeyRings.buildUnifiedKeyRingsUri(), + cursor = contentResolver.query(KeyRings.buildUnifiedKeyRingsUri(), new String[] { KeyRings.MASTER_KEY_ID }, null, null, null); } else { String[] signerMasterKeyIdStrings = new String[signerMasterKeyIds.size()]; @@ -1044,7 +1054,7 @@ public class KeyWritableRepository extends KeyRepository { signerMasterKeyIdStrings[i++] = Long.toString(masterKeyId); } - cursor = mContentResolver.query(KeyRings.buildUnifiedKeyRingsFilterBySigner(), + cursor = contentResolver.query(KeyRings.buildUnifiedKeyRingsFilterBySigner(), new String[] { KeyRings.MASTER_KEY_ID }, null, signerMasterKeyIdStrings, null); } 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 a3d160151..d581de0f5 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java @@ -50,6 +50,7 @@ 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 timber.log.Timber; import static android.database.DatabaseUtils.dumpCursorToString; @@ -101,7 +102,7 @@ public class KeychainProvider extends ContentProvider implements SimpleContentRe String authority = KeychainContract.CONTENT_AUTHORITY; - /** + /* * list key_rings * *
@@ -124,7 +125,7 @@ public class KeychainProvider extends ContentProvider implements SimpleContentRe
                         + "/" + KeychainContract.PATH_USER_IDS,
                 KEY_RINGS_USER_IDS);
 
-        /**
+        /*
          * find by criteria other than master key id
          *
          * key_rings/find/email/_
@@ -144,7 +145,7 @@ public class KeychainProvider extends ContentProvider implements SimpleContentRe
                         + KeychainContract.PATH_FILTER + "/" + KeychainContract.PATH_BY_SIGNER,
                 KEY_RINGS_FILTER_BY_SIGNER);
 
-        /**
+        /*
          * list key_ring specifics
          *
          * 
@@ -189,7 +190,7 @@ public class KeychainProvider extends ContentProvider implements SimpleContentRe
                         + KeychainContract.PATH_CERTS + "/*/*",
                 KEY_RING_CERTS_SPECIFIC);
 
-        /**
+        /*
          * API apps
          *
          * 
@@ -205,7 +206,7 @@ public class KeychainProvider extends ContentProvider implements SimpleContentRe
         matcher.addURI(authority, KeychainContract.BASE_API_APPS + "/*/"
                 + KeychainContract.PATH_ALLOWED_KEYS, API_ALLOWED_KEYS);
 
-        /**
+        /*
          * Trust Identity access
          *
          * 
@@ -221,7 +222,7 @@ public class KeychainProvider extends ContentProvider implements SimpleContentRe
                 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);
@@ -835,7 +836,7 @@ public class KeychainProvider extends ContentProvider implements SimpleContentRe
 
         SQLiteDatabase db = getDb().getReadableDatabase();
 
-        Cursor cursor = qb.query(db, projection, selection, selectionArgs, groupBy, having, orderBy);
+        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);
@@ -849,27 +850,7 @@ public class KeychainProvider extends ContentProvider implements SimpleContentRe
 
         if (Constants.DEBUG && Constants.DEBUG_EXPLAIN_QUERIES) {
             String rawQuery = qb.buildQuery(projection, selection, groupBy, having, orderBy, null);
-            Cursor explainCursor = db.rawQuery("EXPLAIN QUERY PLAN " + rawQuery, selectionArgs);
-
-            // this is a debugging feature, we can be a little careless
-            explainCursor.moveToFirst();
-
-            StringBuilder line = new StringBuilder();
-            for (int i = 0; i < explainCursor.getColumnCount(); i++) {
-                line.append(explainCursor.getColumnName(i)).append(", ");
-            }
-            Timber.d(line.toString());
-
-            while (!explainCursor.isAfterLast()) {
-                line = new StringBuilder();
-                for (int i = 0; i < explainCursor.getColumnCount(); i++) {
-                    line.append(explainCursor.getString(i)).append(", ");
-                }
-                Timber.d(line.toString());
-                explainCursor.moveToNext();
-            }
-
-            explainCursor.close();
+            DatabaseUtil.explainQuery(db, rawQuery);
         }
 
         return cursor;
@@ -966,9 +947,6 @@ public class KeychainProvider extends ContentProvider implements SimpleContentRe
                 rowUri = uri;
             }
 
-            // notify of changes in db
-            getContext().getContentResolver().notifyChange(uri, null);
-
         } catch (SQLiteConstraintException e) {
             Timber.d(e, "Constraint exception on insert! Entry already existing?");
         }
@@ -1003,7 +981,6 @@ public class KeychainProvider extends ContentProvider implements SimpleContentRe
                 }
                 // corresponding keys and userIds are deleted by ON DELETE CASCADE
                 count = db.delete(Tables.KEY_RINGS_PUBLIC, selection, selectionArgs);
-                contentResolver.notifyChange(KeyRings.buildGenericKeyRingUri(uri.getPathSegments().get(1)), null);
                 break;
             }
             case KEY_RING_SECRET: {
@@ -1013,7 +990,6 @@ public class KeychainProvider extends ContentProvider implements SimpleContentRe
                     selection += " AND (" + additionalSelection + ")";
                 }
                 count = db.delete(Tables.KEY_RINGS_SECRET, selection, selectionArgs);
-                contentResolver.notifyChange(KeyRings.buildGenericKeyRingUri(uri.getPathSegments().get(1)), null);
                 break;
             }
 
@@ -1024,20 +1000,7 @@ public class KeychainProvider extends ContentProvider implements SimpleContentRe
                 String selection = ApiAutocryptPeer.PACKAGE_NAME + " = ? AND " + ApiAutocryptPeer.IDENTIFIER + " = ?";
                 selectionArgs = new String[] { packageName, autocryptPeer };
 
-                Cursor cursor = db.query(Tables.API_AUTOCRYPT_PEERS, new String[] { ApiAutocryptPeer.MASTER_KEY_ID },
-                        selection, selectionArgs, null, null, null);
-                Long masterKeyId = null;
-                if (cursor != null && cursor.moveToNext() && !cursor.isNull(0)) {
-                    masterKeyId = cursor.getLong(0);
-                }
-
                 count = db.delete(Tables.API_AUTOCRYPT_PEERS, selection, selectionArgs);
-
-                if (masterKeyId != null) {
-                    contentResolver.notifyChange(KeyRings.buildGenericKeyRingUri(masterKeyId), null);
-                }
-                contentResolver.notifyChange(
-                        ApiAutocryptPeer.buildByPackageNameAndAutocryptId(packageName, autocryptPeer), null);
                 break;
             }
 
@@ -1047,7 +1010,6 @@ public class KeychainProvider extends ContentProvider implements SimpleContentRe
                     selection += " AND (" + additionalSelection + ")";
                 }
                 count = db.delete(Tables.API_AUTOCRYPT_PEERS, selection, selectionArgs);
-                contentResolver.notifyChange(KeyRings.buildGenericKeyRingUri(uri.getLastPathSegment()), null);
                 break;
 
             case API_APPS_BY_PACKAGE_NAME: {
@@ -1076,7 +1038,6 @@ public class KeychainProvider extends ContentProvider implements SimpleContentRe
         Timber.v("update(uri=" + uri + ", values=" + values.toString() + ")");
 
         final SQLiteDatabase db = getDb().getWritableDatabase();
-        ContentResolver contentResolver = getContext().getContentResolver();
 
         int count = 0;
         try {
@@ -1127,25 +1088,12 @@ public class KeychainProvider extends ContentProvider implements SimpleContentRe
                         db.insertOrThrow(Tables.API_AUTOCRYPT_PEERS, null, values);
                     }
 
-                    Long masterKeyId = values.getAsLong(ApiAutocryptPeer.MASTER_KEY_ID);
-                    if (masterKeyId != null) {
-                        contentResolver.notifyChange(KeyRings.buildGenericKeyRingUri(masterKeyId), null);
-                    }
-                    Long gossipMasterKeyId = values.getAsLong(ApiAutocryptPeer.GOSSIP_MASTER_KEY_ID);
-                    if (gossipMasterKeyId != null && !gossipMasterKeyId.equals(masterKeyId)) {
-                        contentResolver.notifyChange(KeyRings.buildGenericKeyRingUri(gossipMasterKeyId), null);
-                    }
-
                     break;
                 }
                 default: {
                     throw new UnsupportedOperationException("Unknown uri: " + uri);
                 }
             }
-
-            // notify of changes in db
-            contentResolver.notifyChange(uri, null);
-
         } catch (SQLiteConstraintException e) {
             Timber.d(e, "Constraint exception on update! Entry already existing?");
         }
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/LastUpdateInteractor.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/LastUpdateInteractor.java
index 83f58ae2c..89520cb1a 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/LastUpdateInteractor.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/LastUpdateInteractor.java
@@ -15,14 +15,16 @@ 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());
+        return new LastUpdateInteractor(context.getContentResolver(), DatabaseNotifyManager.create(context));
     }
 
-    private LastUpdateInteractor(ContentResolver contentResolver) {
+    private LastUpdateInteractor(ContentResolver contentResolver, DatabaseNotifyManager databaseNotifyManager) {
         this.contentResolver = contentResolver;
+        this.databaseNotifyManager = databaseNotifyManager;
     }
 
     @Nullable
@@ -69,6 +71,8 @@ public class LastUpdateInteractor {
 
         // this will actually update/replace, doing the right thing™ for seenOnKeyservers value
         // see `KeychainProvider.insert()`
-        return contentResolver.insert(UpdatedKeys.CONTENT_URI, values);
+        Uri insert = contentResolver.insert(UpdatedKeys.CONTENT_URI, values);
+        databaseNotifyManager.notifyKeyserverStatusChange(masterKeyId);
+        return insert;
     }
 }
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 328c1ca8d..2253e3e4d 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/KeychainExternalProvider.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/KeychainExternalProvider.java
@@ -25,6 +25,7 @@ import java.util.List;
 
 import android.content.ContentProvider;
 import android.content.ContentValues;
+import android.content.Context;
 import android.content.UriMatcher;
 import android.database.Cursor;
 import android.database.DatabaseUtils;
@@ -40,6 +41,7 @@ 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.provider.KeychainContract.Certs;
 import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
@@ -72,6 +74,7 @@ public class KeychainExternalProvider extends ContentProvider implements SimpleC
     private UriMatcher uriMatcher;
     private ApiPermissionHelper apiPermissionHelper;
     private KeychainProvider internalKeychainProvider;
+    private DatabaseNotifyManager databaseNotifyManager;
 
 
     /**
@@ -103,9 +106,15 @@ public class KeychainExternalProvider extends ContentProvider implements SimpleC
     public boolean onCreate() {
         uriMatcher = buildUriMatcher();
 
+        Context context = getContext();
+        if (context == null) {
+            throw new NullPointerException("Context can't be null during onCreate!");
+        }
+
         internalKeychainProvider = new KeychainProvider();
-        internalKeychainProvider.attachInfo(getContext(), null);
-        apiPermissionHelper = new ApiPermissionHelper(getContext(), new ApiDataAccessObject(internalKeychainProvider));
+        internalKeychainProvider.attachInfo(context, null);
+        apiPermissionHelper = new ApiPermissionHelper(context, new ApiDataAccessObject(internalKeychainProvider));
+        databaseNotifyManager = DatabaseNotifyManager.create(context);
         return true;
     }
 
@@ -270,7 +279,8 @@ public class KeychainExternalProvider extends ContentProvider implements SimpleC
                 }
                 if (!isWildcardSelector && queriesAutocryptResult) {
                     AutocryptPeerDataAccessObject autocryptPeerDao =
-                            new AutocryptPeerDataAccessObject(internalKeychainProvider, callingPackageName);
+                            new AutocryptPeerDataAccessObject(internalKeychainProvider, callingPackageName,
+                                    databaseNotifyManager);
                     fillTempTableWithAutocryptRecommendations(db, autocryptPeerDao, selectionArgs);
                 }
 
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 b02b61b5c..f3bb61d51 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
@@ -37,10 +37,10 @@ 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.IdentityLoader.IdentityInfo;
-import org.sufficientlysecure.keychain.ui.keyview.loader.IdentityLoader.LinkedIdInfo;
-import org.sufficientlysecure.keychain.ui.keyview.loader.IdentityLoader.AutocryptPeerInfo;
-import org.sufficientlysecure.keychain.ui.keyview.loader.IdentityLoader.UserIdInfo;
+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;
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 27c04e1ae..b1e675583 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
@@ -18,6 +18,11 @@
 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.Intent;
 import android.os.Bundle;
 import android.os.Handler;
@@ -36,6 +41,10 @@ import org.sufficientlysecure.keychain.R;
 import org.sufficientlysecure.keychain.compatibility.DialogFragmentWorkaround;
 import org.sufficientlysecure.keychain.operations.results.OperationResult;
 import org.sufficientlysecure.keychain.ui.base.LoaderFragment;
+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.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;
@@ -53,11 +62,6 @@ public class ViewKeyFragment extends LoaderFragment implements ViewKeyMvpView, O
 
     boolean mIsSecret = false;
 
-    private static final int LOADER_IDENTITIES = 1;
-    private static final int LOADER_ID_LINKED_CONTACT = 2;
-    private static final int LOADER_ID_SUBKEY_STATUS = 3;
-    private static final int LOADER_ID_KEYSERVER_STATUS = 4;
-
     private IdentitiesCardView identitiesCardView;
     private IdentitiesPresenter identitiesPresenter;
 
@@ -100,6 +104,41 @@ public class ViewKeyFragment extends LoaderFragment implements ViewKeyMvpView, O
         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();
+            }
+            return identityInfo;
+        }
+
+        LiveData getSubkeyStatus(KeyHealthPresenter keyHealthPresenter) {
+            if (subkeyStatus == null) {
+                subkeyStatus = keyHealthPresenter.getLiveDataInstance();
+            }
+            return subkeyStatus;
+        }
+
+        LiveData getSystemContactInfo(SystemContactPresenter systemContactPresenter) {
+            if (systemContactInfo == null) {
+                systemContactInfo = systemContactPresenter.getLiveDataInstance();
+            }
+            return systemContactInfo;
+        }
+
+        LiveData getKeyserverStatus(KeyserverStatusPresenter keyserverStatusPresenter) {
+            if (keyserverStatus == null) {
+                keyserverStatus = keyserverStatusPresenter.getLiveDataInstance();
+            }
+            return keyserverStatus;
+        }
+    }
+
     @Override
     public void onActivityCreated(Bundle savedInstanceState) {
         super.onActivityCreated(savedInstanceState);
@@ -107,21 +146,22 @@ public class ViewKeyFragment extends LoaderFragment implements ViewKeyMvpView, O
         long masterKeyId = getArguments().getLong(ARG_MASTER_KEY_ID);
         mIsSecret = getArguments().getBoolean(ARG_IS_SECRET);
 
+        KeyFragmentViewModel model = ViewModelProviders.of(this).get(KeyFragmentViewModel.class);
+
         identitiesPresenter = new IdentitiesPresenter(
-                getContext(), identitiesCardView, this, LOADER_IDENTITIES, masterKeyId, mIsSecret);
-        identitiesPresenter.startLoader(getLoaderManager());
+                getContext(), identitiesCardView, this, masterKeyId, mIsSecret);
+        model.getIdentityInfo(identitiesPresenter).observe(this, identitiesPresenter);
 
         systemContactPresenter = new SystemContactPresenter(
-                getContext(), systemContactCard, LOADER_ID_LINKED_CONTACT, masterKeyId, mIsSecret);
-        systemContactPresenter.startLoader(getLoaderManager());
+                getContext(), systemContactCard, masterKeyId, mIsSecret);
+        model.getSystemContactInfo(systemContactPresenter).observe(this, systemContactPresenter);
 
-        keyHealthPresenter = new KeyHealthPresenter(
-                getContext(), keyStatusHealth, LOADER_ID_SUBKEY_STATUS, masterKeyId, mIsSecret);
-        keyHealthPresenter.startLoader(getLoaderManager());
+        keyHealthPresenter = new KeyHealthPresenter(getContext(), keyStatusHealth, masterKeyId);
+        model.getSubkeyStatus(keyHealthPresenter).observe(this, keyHealthPresenter);
 
         keyserverStatusPresenter = new KeyserverStatusPresenter(
-                getContext(), keyStatusKeyserver, LOADER_ID_KEYSERVER_STATUS, masterKeyId, mIsSecret);
-        keyserverStatusPresenter.startLoader(getLoaderManager());
+                getContext(), keyStatusKeyserver, masterKeyId, mIsSecret);
+        model.getKeyserverStatus(keyserverStatusPresenter).observe(this, keyserverStatusPresenter);
     }
 
     @Override
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/loader/IdentityLoader.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/loader/IdentityDao.java
similarity index 80%
rename from OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/loader/IdentityLoader.java
rename to OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/loader/IdentityDao.java
index bafd1e74b..8a3458a51 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/loader/IdentityLoader.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/loader/IdentityDao.java
@@ -26,11 +26,11 @@ import java.util.List;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
+import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.database.Cursor;
 import android.graphics.drawable.Drawable;
 import android.support.annotation.Nullable;
-import android.support.v4.content.AsyncTaskLoader;
 
 import com.google.auto.value.AutoValue;
 import org.openintents.openpgp.util.OpenPgpApi;
@@ -38,14 +38,12 @@ 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.KeyRings;
 import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets;
-import org.sufficientlysecure.keychain.ui.keyview.loader.IdentityLoader.IdentityInfo;
 import org.sufficientlysecure.keychain.ui.util.PackageIconGetter;
 import timber.log.Timber;
 
 
-public class IdentityLoader extends AsyncTaskLoader> {
+public class IdentityDao {
     private static final String[] USER_PACKETS_PROJECTION = new String[]{
             UserPackets._ID,
             UserPackets.TYPE,
@@ -84,41 +82,34 @@ public class IdentityLoader extends AsyncTaskLoader> {
 
     private final ContentResolver contentResolver;
     private final PackageIconGetter packageIconGetter;
-    private final long masterKeyId;
-    private final boolean showLinkedIds;
+    private final PackageManager packageManager;
 
-    private List cachedResult;
-
-    private ForceLoadContentObserver identityObserver;
-
-
-    public IdentityLoader(Context context, ContentResolver contentResolver, long masterKeyId, boolean showLinkedIds) {
-        super(context);
-
-        this.contentResolver = contentResolver;
-        this.masterKeyId = masterKeyId;
-        this.showLinkedIds = showLinkedIds;
-
-        this.identityObserver = new ForceLoadContentObserver();
-        this.packageIconGetter = PackageIconGetter.getInstance(context);
-
-        this.identityObserver = new ForceLoadContentObserver();
+    static IdentityDao getInstance(Context context) {
+        ContentResolver contentResolver = context.getContentResolver();
+        PackageManager packageManager = context.getPackageManager();
+        PackageIconGetter iconGetter = PackageIconGetter.getInstance(context);
+        return new IdentityDao(contentResolver, packageManager, iconGetter);
     }
 
-    @Override
-    public List loadInBackground() {
+    private IdentityDao(ContentResolver contentResolver, PackageManager packageManager, PackageIconGetter iconGetter) {
+        this.packageManager = packageManager;
+        this.contentResolver = contentResolver;
+        this.packageIconGetter = iconGetter;
+    }
+
+    List getIdentityInfos(long masterKeyId, boolean showLinkedIds) {
         ArrayList identities = new ArrayList<>();
 
         if (showLinkedIds) {
-            loadLinkedIds(identities);
+            loadLinkedIds(identities, masterKeyId);
         }
-        loadUserIds(identities);
-        correlateOrAddAutocryptPeers(identities);
+        loadUserIds(identities, masterKeyId);
+        correlateOrAddAutocryptPeers(identities, masterKeyId);
 
         return Collections.unmodifiableList(identities);
     }
 
-    private void correlateOrAddAutocryptPeers(ArrayList identities) {
+    private void correlateOrAddAutocryptPeers(ArrayList identities, long masterKeyId) {
         Cursor cursor = contentResolver.query(ApiAutocryptPeer.buildByMasterKeyId(masterKeyId),
                 AUTOCRYPT_PEER_PROJECTION, null, null, null);
         if (cursor == null) {
@@ -158,7 +149,7 @@ public class IdentityLoader extends AsyncTaskLoader> {
         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
         intent.putExtra(OpenPgpApi.EXTRA_AUTOCRYPT_PEER_ID, autocryptPeer);
 
-        List resolveInfos = getContext().getPackageManager().queryIntentActivities(intent, 0);
+        List resolveInfos = packageManager.queryIntentActivities(intent, 0);
         if (resolveInfos != null && !resolveInfos.isEmpty()) {
             return intent;
         } else {
@@ -178,7 +169,7 @@ public class IdentityLoader extends AsyncTaskLoader> {
         return null;
     }
 
-    private void loadLinkedIds(ArrayList identities) {
+    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) {
@@ -208,7 +199,7 @@ public class IdentityLoader extends AsyncTaskLoader> {
         }
     }
 
-    private void loadUserIds(ArrayList identities) {
+    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) {
@@ -236,36 +227,6 @@ public class IdentityLoader extends AsyncTaskLoader> {
         }
     }
 
-    @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();
-        }
-
-        getContext().getContentResolver().registerContentObserver(
-                KeyRings.buildGenericKeyRingUri(masterKeyId), true, identityObserver);
-    }
-
-    @Override
-    protected void onAbandon() {
-        super.onAbandon();
-
-        getContext().getContentResolver().unregisterContentObserver(identityObserver);
-    }
-
     public interface IdentityInfo {
         int getRank();
         int getVerified();
@@ -287,7 +248,7 @@ public class IdentityLoader extends AsyncTaskLoader> {
 
         static UserIdInfo create(int rank, int verified, boolean isPrimary, String name, String email,
                 String comment) {
-            return new AutoValue_IdentityLoader_UserIdInfo(rank, verified, isPrimary, name, email, comment);
+            return new AutoValue_IdentityDao_UserIdInfo(rank, verified, isPrimary, name, email, comment);
         }
     }
 
@@ -300,7 +261,7 @@ public class IdentityLoader extends AsyncTaskLoader> {
         public abstract UriAttribute getUriAttribute();
 
         static LinkedIdInfo create(int rank, int verified, boolean isPrimary, UriAttribute uriAttribute) {
-            return new AutoValue_IdentityLoader_LinkedIdInfo(rank, verified, isPrimary, uriAttribute);
+            return new AutoValue_IdentityDao_LinkedIdInfo(rank, verified, isPrimary, uriAttribute);
         }
     }
 
@@ -321,12 +282,12 @@ public class IdentityLoader extends AsyncTaskLoader> {
 
         static AutocryptPeerInfo create(UserIdInfo userIdInfo, String autocryptPeer, String packageName,
                 Drawable appIcon, Intent autocryptPeerIntent) {
-            return new AutoValue_IdentityLoader_AutocryptPeerInfo(userIdInfo.getRank(), userIdInfo.getVerified(),
+            return new AutoValue_IdentityDao_AutocryptPeerInfo(userIdInfo.getRank(), userIdInfo.getVerified(),
                     userIdInfo.isPrimary(), autocryptPeer, packageName, appIcon, userIdInfo, autocryptPeerIntent);
         }
 
         static AutocryptPeerInfo create(String autocryptPeer, String packageName, Drawable appIcon, Intent autocryptPeerIntent) {
-            return new AutoValue_IdentityLoader_AutocryptPeerInfo(
+            return new AutoValue_IdentityDao_AutocryptPeerInfo(
                     0, Certs.VERIFIED_SELF, false, autocryptPeer, packageName, appIcon, null, autocryptPeerIntent);
         }
     }
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/loader/KeyserverStatusLoader.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/loader/KeyserverStatusDao.java
similarity index 67%
rename from OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/loader/KeyserverStatusLoader.java
rename to OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/loader/KeyserverStatusDao.java
index 5f643ee1c..16e6828c6 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/loader/KeyserverStatusLoader.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/loader/KeyserverStatusDao.java
@@ -23,17 +23,12 @@ import java.util.Date;
 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.provider.KeychainContract.UpdatedKeys;
-import org.sufficientlysecure.keychain.ui.keyview.loader.KeyserverStatusLoader.KeyserverStatus;
 import timber.log.Timber;
 
 
-public class KeyserverStatusLoader extends AsyncTaskLoader {
+public class KeyserverStatusDao {
     public static final String[] PROJECTION = new String[] {
             UpdatedKeys.LAST_UPDATED,
             UpdatedKeys.SEEN_ON_KEYSERVERS
@@ -43,23 +38,17 @@ public class KeyserverStatusLoader extends AsyncTaskLoader {
 
 
     private final ContentResolver contentResolver;
-    private final long masterKeyId;
 
-    private KeyserverStatus cachedResult;
-
-    private ForceLoadContentObserver keyserverStatusObserver;
-
-    public KeyserverStatusLoader(Context context, ContentResolver contentResolver, long masterKeyId) {
-        super(context);
-
-        this.contentResolver = contentResolver;
-        this.masterKeyId = masterKeyId;
-
-        this.keyserverStatusObserver = new ForceLoadContentObserver();
+    public static KeyserverStatusDao getInstance(Context context) {
+        ContentResolver contentResolver = context.getContentResolver();
+        return new KeyserverStatusDao(contentResolver);
     }
 
-    @Override
-    public KeyserverStatus loadInBackground() {
+    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) {
@@ -85,36 +74,6 @@ public class KeyserverStatusLoader extends AsyncTaskLoader {
         }
     }
 
-    @Override
-    public void deliverResult(KeyserverStatus keySubkeyStatus) {
-        cachedResult = keySubkeyStatus;
-
-        if (isStarted()) {
-            super.deliverResult(keySubkeyStatus);
-        }
-    }
-
-    @Override
-    protected void onStartLoading() {
-        if (cachedResult != null) {
-            deliverResult(cachedResult);
-        }
-
-        if (takeContentChanged() || cachedResult == null) {
-            forceLoad();
-        }
-
-        getContext().getContentResolver().registerContentObserver(
-                KeyRings.buildGenericKeyRingUri(masterKeyId), true, keyserverStatusObserver);
-    }
-
-    @Override
-    protected void onAbandon() {
-        super.onAbandon();
-
-        getContext().getContentResolver().unregisterContentObserver(keyserverStatusObserver);
-    }
-
     public static class KeyserverStatus {
         private final long masterKeyId;
         private final boolean isPublished;
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/loader/SubkeyStatusLoader.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/loader/SubkeyStatusDao.java
similarity index 81%
rename from OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/loader/SubkeyStatusLoader.java
rename to OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/loader/SubkeyStatusDao.java
index 82816d05d..8caf8b261 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/loader/SubkeyStatusLoader.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/loader/SubkeyStatusDao.java
@@ -28,19 +28,15 @@ import android.content.ContentResolver;
 import android.content.Context;
 import android.database.Cursor;
 import android.support.annotation.NonNull;
-import android.support.v4.content.AsyncTaskLoader;
-import android.util.Log;
 
-import org.sufficientlysecure.keychain.Constants;
 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 org.sufficientlysecure.keychain.ui.keyview.loader.SubkeyStatusLoader.KeySubkeyStatus;
 import timber.log.Timber;
 
 
-public class SubkeyStatusLoader extends AsyncTaskLoader {
+public class SubkeyStatusDao {
     public static final String[] PROJECTION = new String[] {
             Keys.KEY_ID,
             Keys.CREATION,
@@ -68,23 +64,18 @@ public class SubkeyStatusLoader extends AsyncTaskLoader {
 
 
     private final ContentResolver contentResolver;
-    private final long masterKeyId;
-    private final Comparator comparator;
-
-    private KeySubkeyStatus cachedResult;
 
 
-    public SubkeyStatusLoader(Context context, ContentResolver contentResolver, long masterKeyId,
-            Comparator comparator) {
-        super(context);
-
-        this.contentResolver = contentResolver;
-        this.masterKeyId = masterKeyId;
-        this.comparator = comparator;
+    public static SubkeyStatusDao getInstance(Context context) {
+        ContentResolver contentResolver = context.getContentResolver();
+        return new SubkeyStatusDao(contentResolver);
     }
 
-    @Override
-    public KeySubkeyStatus loadInBackground() {
+    private SubkeyStatusDao(ContentResolver contentResolver) {
+        this.contentResolver = contentResolver;
+    }
+
+    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!");
@@ -111,7 +102,10 @@ public class SubkeyStatusLoader extends AsyncTaskLoader {
             }
 
             if (keyCertify == null) {
-                throw new IllegalStateException("Certification key must be set at this point, it's a bug otherwise!");
+                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);
@@ -123,26 +117,6 @@ public class SubkeyStatusLoader extends AsyncTaskLoader {
         }
     }
 
-    @Override
-    public void deliverResult(KeySubkeyStatus 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 KeySubkeyStatus {
         @NonNull
         public final SubKeyItem keyCertify;
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/loader/SystemContactInfoLoader.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/loader/SystemContactDao.java
similarity index 76%
rename from OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/loader/SystemContactInfoLoader.java
rename to OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/loader/SystemContactDao.java
index cfbe51f73..e666920c4 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/loader/SystemContactInfoLoader.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/loader/SystemContactDao.java
@@ -20,22 +20,22 @@ package org.sufficientlysecure.keychain.ui.keyview.loader;
 
 import java.util.List;
 
+import android.Manifest;
 import android.content.ContentResolver;
 import android.content.Context;
+import android.content.pm.PackageManager;
 import android.database.Cursor;
 import android.graphics.Bitmap;
 import android.net.Uri;
 import android.provider.ContactsContract;
-import android.support.v4.content.AsyncTaskLoader;
-import android.util.Log;
+import android.support.v4.content.ContextCompat;
 
 import org.sufficientlysecure.keychain.Constants;
-import org.sufficientlysecure.keychain.ui.keyview.loader.SystemContactInfoLoader.SystemContactInfo;
 import org.sufficientlysecure.keychain.util.ContactHelper;
 import timber.log.Timber;
 
 
-public class SystemContactInfoLoader extends AsyncTaskLoader {
+public class SystemContactDao {
     private static final String[] PROJECTION = {
             ContactsContract.RawContacts.CONTACT_ID
     };
@@ -43,23 +43,30 @@ public class SystemContactInfoLoader extends AsyncTaskLoader
     private static final String CONTACT_NOT_DELETED = "0";
 
 
+    private final Context context;
     private final ContentResolver contentResolver;
-    private final long masterKeyId;
-    private final boolean isSecret;
-
-    private SystemContactInfo cachedResult;
+    private final ContactHelper contactHelper;
 
 
-    public SystemContactInfoLoader(Context context, ContentResolver contentResolver, long masterKeyId, boolean isSecret) {
-        super(context);
-
-        this.contentResolver = contentResolver;
-        this.masterKeyId = masterKeyId;
-        this.isSecret = isSecret;
+    public static SystemContactDao getInstance(Context context) {
+        ContactHelper contactHelper = new ContactHelper(context);
+        ContentResolver contentResolver = context.getContentResolver();
+        return new SystemContactDao(context, contactHelper, contentResolver);
     }
 
-    @Override
-    public SystemContactInfo loadInBackground() {
+    private SystemContactDao(Context context, ContactHelper contactHelper, ContentResolver contentResolver) {
+        this.context = context;
+        this.contactHelper = contactHelper;
+        this.contentResolver = contentResolver;
+    }
+
+    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!");
+            return null;
+        }
+
         Uri baseUri = isSecret ? ContactsContract.Profile.CONTENT_RAW_CONTACTS_URI :
                 ContactsContract.RawContacts.CONTENT_URI;
         Cursor cursor = contentResolver.query(baseUri, PROJECTION,
@@ -87,8 +94,6 @@ public class SystemContactInfoLoader extends AsyncTaskLoader
                 return null;
             }
 
-            ContactHelper contactHelper = new ContactHelper(getContext());
-
             String contactName = null;
             if (isSecret) { //all secret keys are linked to "me" profile in contacts
                 List mainProfileNames = contactHelper.getMainProfileContactName();
@@ -116,26 +121,6 @@ public class SystemContactInfoLoader extends AsyncTaskLoader
         }
     }
 
-    @Override
-    public void deliverResult(SystemContactInfo systemContactInfo) {
-        cachedResult = systemContactInfo;
-
-        if (isStarted()) {
-            super.deliverResult(systemContactInfo);
-        }
-    }
-
-    @Override
-    protected void onStartLoading() {
-        if (cachedResult != null) {
-            deliverResult(cachedResult);
-        }
-
-        if (takeContentChanged() || cachedResult == null) {
-            forceLoad();
-        }
-    }
-
     public static class SystemContactInfo {
         final long masterKeyId;
         public final long contactId;
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
new file mode 100644
index 000000000..c9bb64dd9
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/loader/ViewKeyLiveData.java
@@ -0,0 +1,98 @@
+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
index de4cf3e1e..28cd69b5b 100644
--- 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
@@ -21,13 +21,12 @@ 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.os.Bundle;
-import android.support.v4.app.LoaderManager;
-import android.support.v4.app.LoaderManager.LoaderCallbacks;
-import android.support.v4.content.Loader;
+import android.support.annotation.Nullable;
 import android.view.View;
 
 import org.sufficientlysecure.keychain.provider.AutocryptPeerDataAccessObject;
@@ -37,21 +36,20 @@ 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.IdentityLoader;
-import org.sufficientlysecure.keychain.ui.keyview.loader.IdentityLoader.AutocryptPeerInfo;
-import org.sufficientlysecure.keychain.ui.keyview.loader.IdentityLoader.IdentityInfo;
-import org.sufficientlysecure.keychain.ui.keyview.loader.IdentityLoader.LinkedIdInfo;
-import org.sufficientlysecure.keychain.ui.keyview.loader.IdentityLoader.UserIdInfo;
+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 LoaderCallbacks> {
+public class IdentitiesPresenter implements Observer> {
     private final Context context;
     private final IdentitiesMvpView view;
     private final ViewKeyMvpView viewKeyMvpView;
-    private final int loaderId;
 
     private final IdentityAdapter identitiesAdapter;
 
@@ -60,11 +58,10 @@ public class IdentitiesPresenter implements LoaderCallbacks>
     private final boolean showLinkedIds;
 
     public IdentitiesPresenter(Context context, IdentitiesMvpView view, ViewKeyMvpView viewKeyMvpView,
-            int loaderId, long masterKeyId, boolean isSecret) {
+            long masterKeyId, boolean isSecret) {
         this.context = context;
         this.view = view;
         this.viewKeyMvpView = viewKeyMvpView;
-        this.loaderId = loaderId;
 
         this.masterKeyId = masterKeyId;
         this.isSecret = isSecret;
@@ -90,24 +87,10 @@ public class IdentitiesPresenter implements LoaderCallbacks>
         view.setIdentitiesCardListener(() -> addLinkedIdentity());
     }
 
-    public void startLoader(LoaderManager loaderManager) {
-        loaderManager.restartLoader(loaderId, null, this);
-    }
-
     @Override
-    public Loader> onCreateLoader(int id, Bundle args) {
-        return new IdentityLoader(context, context.getContentResolver(), masterKeyId, showLinkedIds);
-    }
-
-    @Override
-    public void onLoadFinished(Loader> loader, List data) {
+    public void onChanged(@Nullable List identityInfos) {
         viewKeyMvpView.setContentShown(true, false);
-        identitiesAdapter.setData(data);
-    }
-
-    @Override
-    public void onLoaderReset(Loader loader) {
-        identitiesAdapter.setData(null);
+        identitiesAdapter.setData(identityInfos);
     }
 
     private void showIdentityInfo(final int position) {
@@ -168,6 +151,10 @@ public class IdentitiesPresenter implements LoaderCallbacks>
         autocryptPeerDao.delete(info.getIdentity());
     }
 
+    public LiveData> getLiveDataInstance() {
+        return new IdentityLiveData(context, masterKeyId, showLinkedIds);
+    }
+
     public interface IdentitiesMvpView {
         void setIdentitiesAdapter(IdentityAdapter userIdsAdapter);
         void setIdentitiesCardListener(IdentitiesCardListener identitiesCardListener);
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
index a8bb68552..3a9e11d63 100644
--- 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
@@ -21,21 +21,21 @@ 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.os.Bundle;
-import android.support.v4.app.LoaderManager;
-import android.support.v4.app.LoaderManager.LoaderCallbacks;
-import android.support.v4.content.Loader;
+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;
-import org.sufficientlysecure.keychain.ui.keyview.loader.SubkeyStatusLoader;
-import org.sufficientlysecure.keychain.ui.keyview.loader.SubkeyStatusLoader.KeySubkeyStatus;
-import org.sufficientlysecure.keychain.ui.keyview.loader.SubkeyStatusLoader.SubKeyItem;
 
 
-public class KeyHealthPresenter implements LoaderCallbacks {
+public class KeyHealthPresenter implements Observer {
     private static final Comparator SUBKEY_COMPARATOR = new Comparator() {
         @Override
         public int compare(SubKeyItem one, SubKeyItem two) {
@@ -58,22 +58,16 @@ public class KeyHealthPresenter implements LoaderCallbacks {
 
     private final Context context;
     private final KeyHealthMvpView view;
-    private final int loaderId;
-
     private final long masterKeyId;
-    private final boolean isSecret;
 
     private KeySubkeyStatus subkeyStatus;
     private boolean showingExpandedInfo;
 
 
-    public KeyHealthPresenter(Context context, KeyHealthMvpView view, int loaderId, long masterKeyId, boolean isSecret) {
+    public KeyHealthPresenter(Context context, KeyHealthMvpView view, long masterKeyId) {
         this.context = context;
         this.view = view;
-        this.loaderId = loaderId;
-
         this.masterKeyId = masterKeyId;
-        this.isSecret = isSecret;
 
         view.setOnHealthClickListener(new KeyHealthClickListener() {
             @Override
@@ -83,18 +77,12 @@ public class KeyHealthPresenter implements LoaderCallbacks {
         });
     }
 
-    public void startLoader(LoaderManager loaderManager) {
-        loaderManager.restartLoader(loaderId, null, this);
-    }
-
     @Override
-    public Loader onCreateLoader(int id, Bundle args) {
-        return new SubkeyStatusLoader(context, context.getContentResolver(), masterKeyId, SUBKEY_COMPARATOR);
-    }
-
-    @Override
-    public void onLoadFinished(Loader loader, KeySubkeyStatus subkeyStatus) {
+    public void onChanged(@Nullable KeySubkeyStatus subkeyStatus) {
         this.subkeyStatus = subkeyStatus;
+        if (subkeyStatus == null) {
+            return;
+        }
 
         KeyHealthStatus keyHealthStatus = determineKeyHealthStatus(subkeyStatus);
 
@@ -195,11 +183,6 @@ public class KeyHealthPresenter implements LoaderCallbacks {
         return KeyHealthStatus.OK;
     }
 
-    @Override
-    public void onLoaderReset(Loader loader) {
-
-    }
-
     private void onKeyHealthClick() {
         if (showingExpandedInfo) {
             showingExpandedInfo = false;
@@ -262,6 +245,10 @@ public class KeyHealthPresenter implements LoaderCallbacks {
         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
     }
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
index 112144db6..76e63f15e 100644
--- 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
@@ -20,46 +20,43 @@ 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.os.Bundle;
-import android.support.v4.app.LoaderManager;
-import android.support.v4.app.LoaderManager.LoaderCallbacks;
-import android.support.v4.content.Loader;
+import android.support.annotation.Nullable;
 
-import org.sufficientlysecure.keychain.ui.keyview.loader.KeyserverStatusLoader;
-import org.sufficientlysecure.keychain.ui.keyview.loader.KeyserverStatusLoader.KeyserverStatus;
+import org.sufficientlysecure.keychain.ui.keyview.loader.KeyserverStatusDao.KeyserverStatus;
+import org.sufficientlysecure.keychain.ui.keyview.loader.ViewKeyLiveData.KeyserverStatusLiveData;
 
 
-public class KeyserverStatusPresenter implements LoaderCallbacks {
+public class KeyserverStatusPresenter implements Observer {
     private final Context context;
     private final KeyserverStatusMvpView view;
-    private final int loaderId;
 
     private final long masterKeyId;
     private final boolean isSecret;
 
 
-    public KeyserverStatusPresenter(Context context, KeyserverStatusMvpView view, int loaderId, long masterKeyId,
+    public KeyserverStatusPresenter(Context context, KeyserverStatusMvpView view, long masterKeyId,
             boolean isSecret) {
         this.context = context;
         this.view = view;
-        this.loaderId = loaderId;
 
         this.masterKeyId = masterKeyId;
         this.isSecret = isSecret;
     }
 
-    public void startLoader(LoaderManager loaderManager) {
-        loaderManager.restartLoader(loaderId, null, this);
+    public LiveData getLiveDataInstance() {
+        return new KeyserverStatusLiveData(context, masterKeyId);
     }
 
     @Override
-    public Loader onCreateLoader(int id, Bundle args) {
-        return new KeyserverStatusLoader(context, context.getContentResolver(), masterKeyId);
-    }
+    public void onChanged(@Nullable KeyserverStatus keyserverStatus) {
+        if (keyserverStatus == null) {
+            view.setDisplayStatusUnknown();
+            return;
+        }
 
-    @Override
-    public void onLoadFinished(Loader loader, KeyserverStatus keyserverStatus) {
         if (keyserverStatus.hasBeenUpdated()) {
             if (keyserverStatus.isPublished()) {
                 view.setDisplayStatusPublished();
@@ -72,11 +69,6 @@ public class KeyserverStatusPresenter implements LoaderCallbacks {
+public class SystemContactPresenter implements Observer {
     private final Context context;
     private final SystemContactMvpView view;
-    private final int loaderId;
 
     private final long masterKeyId;
     private final boolean isSecret;
@@ -47,55 +41,29 @@ public class SystemContactPresenter implements LoaderCallbacks getLiveDataInstance() {
+        return new SystemContactInfoLiveData(context, masterKeyId, isSecret);
+    }
+
+    @Override
+    public void onChanged(@Nullable SystemContactInfo systemContactInfo) {
+        if (systemContactInfo == null) {
             view.hideLinkedSystemContact();
             return;
         }
 
-        Bundle linkedContactData = new Bundle();
-
-        // initialises loader for contact query so we can listen to any updates
-        loaderManager.restartLoader(loaderId, linkedContactData, this);
-    }
-
-    @Override
-    public Loader onCreateLoader(int id, Bundle args) {
-        return new SystemContactInfoLoader(context, context.getContentResolver(), masterKeyId, isSecret);
-    }
-
-    @Override
-    public void onLoadFinished(Loader loader, SystemContactInfo data) {
-        if (data == null) {
-            view.hideLinkedSystemContact();
-            return;
-        }
-
-        this.contactId = data.contactId;
-        view.showLinkedSystemContact(data.contactName, data.contactPicture);
-    }
-
-    @Override
-    public void onLoaderReset(Loader loader) {
-
+        this.contactId = systemContactInfo.contactId;
+        view.showLinkedSystemContact(systemContactInfo.contactName, systemContactInfo.contactPicture);
     }
 
     private void onSystemContactClick() {
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 2f9e79ce8..dd21accd6 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,13 @@
 
 package org.sufficientlysecure.keychain.util;
 
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
 import android.text.TextUtils;
 
+import timber.log.Timber;
+
+
 /**
  * Shamelessly copied from android.database.DatabaseUtils
  */
@@ -50,4 +55,28 @@ public class DatabaseUtil {
         System.arraycopy(newValues, 0, result, originalValues.length, newValues.length);
         return result;
     }
+
+    public static void explainQuery(SQLiteDatabase db, String sql) {
+        Cursor explainCursor = db.rawQuery("EXPLAIN QUERY PLAN " + sql, new String[0]);
+
+        // this is a debugging feature, we can be a little careless
+        explainCursor.moveToFirst();
+
+        StringBuilder line = new StringBuilder();
+        for (int i = 0; i < explainCursor.getColumnCount(); i++) {
+            line.append(explainCursor.getColumnName(i)).append(", ");
+        }
+        Timber.d(line.toString());
+
+        while (!explainCursor.isAfterLast()) {
+            line = new StringBuilder();
+            for (int i = 0; i < explainCursor.getColumnCount(); i++) {
+                line.append(explainCursor.getString(i)).append(", ");
+            }
+            Timber.d(line.toString());
+            explainCursor.moveToNext();
+        }
+
+        explainCursor.close();
+    }
 }
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/DatabaseUtils.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/DatabaseUtils.java
deleted file mode 100644
index 69e4f05fe..000000000
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/DatabaseUtils.java
+++ /dev/null
@@ -1,34 +0,0 @@
-package org.sufficientlysecure.keychain.util;
-
-
-import android.database.Cursor;
-import android.database.sqlite.SQLiteDatabase;
-
-import timber.log.Timber;
-
-
-public class DatabaseUtils {
-    public static void explainQuery(SQLiteDatabase db, String sql) {
-        Cursor explainCursor = db.rawQuery("EXPLAIN QUERY PLAN " + sql, new String[0]);
-
-        // this is a debugging feature, we can be a little careless
-        explainCursor.moveToFirst();
-
-        StringBuilder line = new StringBuilder();
-        for (int i = 0; i < explainCursor.getColumnCount(); i++) {
-            line.append(explainCursor.getColumnName(i)).append(", ");
-        }
-        Timber.d(line.toString());
-
-        while (!explainCursor.isAfterLast()) {
-            line = new StringBuilder();
-            for (int i = 0; i < explainCursor.getColumnCount(); i++) {
-                line.append(explainCursor.getString(i)).append(", ");
-            }
-            Timber.d(line.toString());
-            explainCursor.moveToNext();
-        }
-
-        explainCursor.close();
-    }
-}
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 457fc2adb..6cdc1428d 100644
--- a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/provider/InteropTest.java
+++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/provider/InteropTest.java
@@ -244,7 +244,8 @@ public class InteropTest {
 
         KeyWritableRepository helper = new KeyWritableRepository(RuntimeEnvironment.application,
                 LocalPublicKeyStorage.getInstance(RuntimeEnvironment.application),
-                LastUpdateInteractor.create(RuntimeEnvironment.application)) {
+                LastUpdateInteractor.create(RuntimeEnvironment.application),
+                DatabaseNotifyManager.create(RuntimeEnvironment.application)) {
 
             @Override
             public CachedPublicKeyRing getCachedPublicKeyRing(Uri queryUri) throws PgpKeyNotFoundException {