diff --git a/OpenKeychain/src/main/AndroidManifest.xml b/OpenKeychain/src/main/AndroidManifest.xml index 5d3a106de..0e7baf039 100644 --- a/OpenKeychain/src/main/AndroidManifest.xml +++ b/OpenKeychain/src/main/AndroidManifest.xml @@ -65,6 +65,7 @@ + { @@ -119,6 +121,7 @@ public class ViewKeyFragment extends LoaderFragment implements /** * Checks if a system contact exists for given masterKeyId, and if it does, sets name, picture * and onClickListener for the linked system contact's layout + * In the case of a secret key, "me" contact details are loaded * * @param masterKeyId */ @@ -126,19 +129,35 @@ public class ViewKeyFragment extends LoaderFragment implements final Context context = mSystemContactName.getContext(); final ContentResolver resolver = context.getContentResolver(); - final long contactId = ContactHelper.findContactId(resolver, masterKeyId); - final String contactName = ContactHelper.getContactName(resolver, contactId); + long contactId; + String contactName = null; + + if (mIsSecret) {//all secret keys are linked to "me" profile in contacts + contactId = ContactHelper.getMainProfileContactId(resolver); + List mainProfileNames = ContactHelper.getMainProfileContactName(context); + if (mainProfileNames != null) contactName = mainProfileNames.get(0); + + } else { + contactId = ContactHelper.findContactId(resolver, masterKeyId); + contactName = ContactHelper.getContactName(resolver, contactId); + } if (contactName != null) {//contact name exists for given master key mSystemContactName.setText(contactName); - Bitmap picture = ContactHelper.loadPhotoByMasterKeyId(resolver, masterKeyId, true); + Bitmap picture; + if (mIsSecret) { + picture = ContactHelper.loadMainProfilePhoto(resolver, false); + } else { + picture = ContactHelper.loadPhotoByMasterKeyId(resolver, masterKeyId, false); + } if (picture != null) mSystemContactPicture.setImageBitmap(picture); + final long finalContactId = contactId; mSystemContactLayout.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - launchContactActivity(contactId, context); + launchContactActivity(finalContactId, context); } }); mSystemContactLoaded = true; @@ -239,14 +258,14 @@ public class ViewKeyFragment extends LoaderFragment implements switch (loader.getId()) { case LOADER_ID_UNIFIED: { if (data.moveToFirst()) { + + mIsSecret = data.getInt(INDEX_HAS_ANY_SECRET) != 0; + //TODO system to allow immediate refreshing of system contact on verification if (!mSystemContactLoaded) {//ensure we load linked system contact only once long masterKeyId = data.getLong(INDEX_MASTER_KEY_ID); loadLinkedSystemContact(masterKeyId); } - - mIsSecret = data.getInt(INDEX_HAS_ANY_SECRET) != 0; - // load user ids after we know if it's a secret key mUserIdsAdapter = new UserIdsAdapter(getActivity(), null, 0, !mIsSecret, null); mUserIds.setAdapter(mUserIdsAdapter); 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 b3eb36157..c07d7a36b 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ContactHelper.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ContactHelper.java @@ -190,7 +190,7 @@ public class ContactHelper { * @param context * @return */ - private static List getMainProfileContactName(Context context) { + public static List getMainProfileContactName(Context context) { ContentResolver resolver = context.getContentResolver(); Cursor profileCursor = resolver.query( ContactsContract.Profile.CONTENT_URI, @@ -214,6 +214,53 @@ public class ContactHelper { return new ArrayList<>(names); } + /** + * returns the CONTACT_ID of the main ("me") contact + * http://developer.android.com/reference/android/provider/ContactsContract.Profile.html + * + * @param resolver + * @return + */ + public static long getMainProfileContactId(ContentResolver resolver) { + Cursor profileCursor = resolver.query( + ContactsContract.Profile.CONTENT_URI, + new String[]{ + ContactsContract.Profile._ID + }, + null, null, null); + if (profileCursor == null) { + return -1; + } + + profileCursor.moveToNext(); + return profileCursor.getLong(0); + } + + /** + * loads the profile picture of the main ("me") contact + * http://developer.android.com/reference/android/provider/ContactsContract.Profile.html + * + * @param contentResolver + * @param highRes true for large image if present, false for thumbnail + * @return bitmap of loaded photo + */ + public static Bitmap loadMainProfilePhoto(ContentResolver contentResolver, boolean highRes) { + try { + long mainProfileContactId = getMainProfileContactId(contentResolver); + + Uri contactUri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_URI, + Long.toString(mainProfileContactId)); + InputStream photoInputStream = + ContactsContract.Contacts.openContactPhotoInputStream(contentResolver, contactUri, highRes); + if (photoInputStream == null) { + return null; + } + return BitmapFactory.decodeStream(photoInputStream); + } catch (Throwable ignored) { + return null; + } + } + public static List getContactMails(Context context) { ContentResolver resolver = context.getContentResolver(); Cursor mailCursor = resolver.query(ContactsContract.CommonDataKinds.Email.CONTENT_URI, @@ -269,7 +316,7 @@ public class ContactHelper { /** * returns the CONTACT_ID of the raw contact to which a masterKeyId is associated, if the - * raw contact has not been marked for deletion + * raw contact has not been marked for deletion. * * @param resolver * @param masterKeyId @@ -363,7 +410,8 @@ public class ContactHelper { KeychainContract.KeyRings.IS_EXPIRED, KeychainContract.KeyRings.IS_REVOKED, KeychainContract.KeyRings.VERIFIED, - KeychainContract.KeyRings.HAS_SECRET}; + KeychainContract.KeyRings.HAS_SECRET, + KeychainContract.KeyRings.HAS_ANY_SECRET}; public static final int INDEX_MASTER_KEY_ID = 0; public static final int INDEX_USER_ID = 1; @@ -371,6 +419,7 @@ public class ContactHelper { 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; /** * Write/Update the current OpenKeychain keys to the contact db @@ -379,6 +428,8 @@ public class ContactHelper { ContentResolver resolver = context.getContentResolver(); Set deletedKeys = getRawContactMasterKeyIds(resolver); + writeKeysToMainProfileContact(context); + if (Constants.DEBUG_SYNC_REMOVE_CONTACTS) { debugDeleteRawContacts(resolver); } @@ -395,9 +446,13 @@ public class ContactHelper { // e.printStackTrace(); // } - // Load all Keys from OK - Cursor cursor = resolver.query(KeychainContract.KeyRings.buildUnifiedKeyRingsUri(), KEYS_TO_CONTACT_PROJECTION, - null, null, null); + // Load all public Keys from OK + // TODO: figure out why using selectionArgs does not work in this case + Cursor cursor = resolver.query(KeychainContract.KeyRings.buildUnifiedKeyRingsUri(), + KEYS_TO_CONTACT_PROJECTION, + KeychainContract.KeyRings.HAS_ANY_SECRET + "=0", + null, null); + if (cursor != null) { while (cursor.moveToNext()) { long masterKeyId = cursor.getLong(INDEX_MASTER_KEY_ID); @@ -406,7 +461,6 @@ public class ContactHelper { boolean isExpired = cursor.getInt(INDEX_IS_EXPIRED) != 0; boolean isRevoked = cursor.getInt(INDEX_IS_REVOKED) > 0; boolean isVerified = cursor.getInt(INDEX_VERIFIED) > 0; - boolean isSecret = cursor.getInt(INDEX_HAS_SECRET) != 0; Log.d(Constants.TAG, "masterKeyId: " + masterKeyId); @@ -418,9 +472,11 @@ public class ContactHelper { ArrayList ops = new ArrayList<>(); - // Do not store expired or revoked keys in contact db - and remove them if they already exist - if (isExpired || isRevoked || !isVerified&&!isSecret) { - Log.d(Constants.TAG, "Expired or revoked or unverified: Deleting rawContactId " + rawContactId); + // 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) { + Log.d(Constants.TAG, "Expired or revoked or unverified: Deleting rawContactId " + + rawContactId); if (rawContactId != -1) { deleteRawContactById(resolver, rawContactId); } @@ -456,43 +512,166 @@ public class ContactHelper { } /** - * Delete all raw contacts associated to OpenKeychain. + * Links all keys with secrets to the main ("me") contact + * http://developer.android.com/reference/android/provider/ContactsContract.Profile.html + * + * @param context + */ + public static void writeKeysToMainProfileContact(Context context) { + ContentResolver resolver = context.getContentResolver(); + Set keysToDelete = getMainProfileMasterKeyIds(resolver); + + // get all keys which have associated secret keys + // TODO: figure out why using selectionArgs does not work in this case + Cursor cursor = resolver.query(KeychainContract.KeyRings.buildUnifiedKeyRingsUri(), + KEYS_TO_CONTACT_PROJECTION, + KeychainContract.KeyRings.HAS_ANY_SECRET + "!=0", + null, null); + if (cursor != null) { + 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; + + if (!isExpired && !isRevoked) { + // 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 + + String keyIdShort = KeyFormattingUtils.convertKeyIdToHexShort(masterKeyId); + Log.d(Constants.TAG, "masterKeyId with secret " + masterKeyId); + + ArrayList ops = new ArrayList<>(); + insertMainProfileRawContact(ops, masterKeyId); + writeContactKey(ops, context, rawContactId, masterKeyId, keyIdShort); + + try { + resolver.applyBatch(ContactsContract.AUTHORITY, ops); + } catch (Exception e) { + Log.w(Constants.TAG, e); + } + } + } + } + } + + for (long masterKeyId : keysToDelete) { + deleteMainProfileRawContactByMasterKeyId(resolver, masterKeyId); + Log.d(Constants.TAG, "Delete main profile raw contact with masterKeyId " + masterKeyId); + } + } + + /** + * Inserts a raw contact into the table defined by ContactsContract.Profile + * http://developer.android.com/reference/android/provider/ContactsContract.Profile.html + * + * @param ops + * @param masterKeyId + */ + private static void insertMainProfileRawContact(ArrayList ops, + long masterKeyId) { + ops.add(ContentProviderOperation.newInsert(ContactsContract.Profile.CONTENT_RAW_CONTACTS_URI) + .withValue(ContactsContract.RawContacts.ACCOUNT_NAME, Constants.ACCOUNT_NAME) + .withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, Constants.ACCOUNT_TYPE) + .withValue(ContactsContract.RawContacts.SOURCE_ID, Long.toString(masterKeyId)) + .build()); + } + + /** + * deletes a raw contact from the main profile table ("me" contact) + * http://developer.android.com/reference/android/provider/ContactsContract.Profile.html + * + * @param resolver + * @param masterKeyId + * @return + */ + private static int deleteMainProfileRawContactByMasterKeyId(ContentResolver resolver, + long masterKeyId) { + // CALLER_IS_SYNCADAPTER allows us to actually wipe the RawContact from the device, otherwise + // would be just flagged for deletion + Uri deleteUri = ContactsContract.Profile.CONTENT_RAW_CONTACTS_URI.buildUpon(). + appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true").build(); + + return resolver.delete(deleteUri, + ContactsContract.RawContacts.ACCOUNT_TYPE + "=? AND " + + ContactsContract.RawContacts.SOURCE_ID + "=?", + new String[]{ + Constants.ACCOUNT_TYPE, Long.toString(masterKeyId) + }); + } + + /** + * Delete all raw contacts associated to OpenKeychain, including those from "me" contact + * defined by ContactsContract.Profile + * + * @return number of rows deleted */ private static int debugDeleteRawContacts(ContentResolver resolver) { - //allows us to actually wipe the RawContact from the device, otherwise would be just flagged - //for deletion + // CALLER_IS_SYNCADAPTER allows us to actually wipe the RawContact from the device, otherwise + // would be just flagged for deletion Uri deleteUri = ContactsContract.RawContacts.CONTENT_URI.buildUpon(). appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true").build(); Log.d(Constants.TAG, "Deleting all raw contacts associated to OK..."); - return resolver.delete(deleteUri, + int delete = resolver.delete(deleteUri, ContactsContract.RawContacts.ACCOUNT_TYPE + "=?", new String[]{ Constants.ACCOUNT_TYPE }); + + Uri mainProfileDeleteUri = ContactsContract.Profile.CONTENT_RAW_CONTACTS_URI.buildUpon() + .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true").build(); + + delete += resolver.delete(mainProfileDeleteUri, + ContactsContract.RawContacts.ACCOUNT_TYPE + "=?", + new String[]{ + Constants.ACCOUNT_TYPE + }); + + return delete; } + /** + * Deletes raw contacts from ContactsContract.RawContacts based on rawContactId. Does not + * delete contacts from the "me" contact defined in ContactsContract.Profile + * + * @param resolver + * @param rawContactId + * @return number of rows deleted + */ private static int deleteRawContactById(ContentResolver resolver, long rawContactId) { - //allows us to actually wipe the RawContact from the device, otherwise would be just flagged - //for deletion + // CALLER_IS_SYNCADAPTER allows us to actually wipe the RawContact from the device, otherwise + // would be just flagged for deletion Uri deleteUri = ContactsContract.RawContacts.CONTENT_URI.buildUpon(). appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true").build(); return resolver.delete(deleteUri, - ContactsContract.RawContacts.ACCOUNT_TYPE + "=? AND " + ContactsContract.RawContacts._ID + "=?", + ContactsContract.RawContacts.ACCOUNT_TYPE + "=? AND " + + ContactsContract.RawContacts._ID + "=?", new String[]{ Constants.ACCOUNT_TYPE, Long.toString(rawContactId) }); } + /** + * Deletes raw contacts from ContactsContract.RawContacts based on masterKeyId. Does not + * delete contacts from the "me" contact defined in ContactsContract.Profile + * + * @param resolver + * @param masterKeyId + * @return number of rows deleted + */ private static int deleteRawContactByMasterKeyId(ContentResolver resolver, long masterKeyId) { - //allows us to actually wipe the RawContact from the device, otherwise would be just flagged - //for deletion + // CALLER_IS_SYNCADAPTER allows us to actually wipe the RawContact from the device, otherwise + // would be just flagged for deletion Uri deleteUri = ContactsContract.RawContacts.CONTENT_URI.buildUpon(). appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true").build(); return resolver.delete(deleteUri, - ContactsContract.RawContacts.ACCOUNT_TYPE + "=? AND " + ContactsContract.RawContacts.SOURCE_ID + "=?", + ContactsContract.RawContacts.ACCOUNT_TYPE + "=? AND " + + ContactsContract.RawContacts.SOURCE_ID + "=?", new String[]{ Constants.ACCOUNT_TYPE, Long.toString(masterKeyId) }); @@ -520,6 +699,28 @@ public class ContactHelper { return result; } + /** + * @return a set of all key master key ids currently present in the contact db + */ + private static Set getMainProfileMasterKeyIds(ContentResolver resolver) { + HashSet result = new HashSet<>(); + Cursor masterKeyIds = resolver.query(ContactsContract.Profile.CONTENT_RAW_CONTACTS_URI, + new String[]{ + ContactsContract.RawContacts.SOURCE_ID + }, + ContactsContract.RawContacts.ACCOUNT_TYPE + "=?", + new String[]{ + Constants.ACCOUNT_TYPE + }, null); + if (masterKeyIds != null) { + while (masterKeyIds.moveToNext()) { + result.add(masterKeyIds.getLong(0)); + } + masterKeyIds.close(); + } + return result; + } + /** * This will search the contact db for a raw contact with a given master key id *