diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/ParcelableHkpKeyserver.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/ParcelableHkpKeyserver.java index 05a058f5b..5a29d2a70 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/ParcelableHkpKeyserver.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/ParcelableHkpKeyserver.java @@ -466,6 +466,29 @@ public class ParcelableHkpKeyserver extends Keyserver implements Parcelable { return getHostID(); } + @Override + public boolean equals(Object obj) { + if (! (obj instanceof ParcelableHkpKeyserver)) { + return false; + } + + ParcelableHkpKeyserver other = (ParcelableHkpKeyserver) obj; + if (other.mUrl == null ^ mUrl == null) { + return false; + } + if (other.mOnion == null ^ mOnion == null) { + return false; + } + if (mUrl != null && !mUrl.equals(other.mUrl)) { + return false; + } + if (mOnion != null && !mOnion.equals(other.mOnion)) { + return false; + } + + return true; + } + /** * Tries to find a server responsible for a given domain * diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ImportOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ImportOperation.java index 3b37fdea9..2e0d14faf 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ImportOperation.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ImportOperation.java @@ -188,6 +188,11 @@ public class ImportOperation extends BaseReadWriteOperation log.add(LogType.MSG_IMPORT_FETCH_ERROR_NOT_FOUND, 2); missingKeys += 1; + byte[] fingerprintHex = entry.getExpectedFingerprint(); + if (fingerprintHex != null) { + mKeyWritableRepository.renewKeyLastUpdatedTime( + KeyFormattingUtils.getKeyIdFromFingerprint(fingerprintHex), false); + } continue; } @@ -234,7 +239,7 @@ public class ImportOperation extends BaseReadWriteOperation } if (!skipSave) { - mKeyWritableRepository.renewKeyLastUpdatedTime(key.getMasterKeyId()); + mKeyWritableRepository.renewKeyLastUpdatedTime(key.getMasterKeyId(), keyWasDownloaded); } } 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 a21db2cce..5277cd393 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeyRepository.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeyRepository.java @@ -280,6 +280,32 @@ public class KeyRepository { return lastUpdateTime; } + @Nullable + Boolean getSeenOnKeyservers(long masterKeyId) { + Cursor cursor = mContentResolver.query( + UpdatedKeys.CONTENT_URI, + new String[] { UpdatedKeys.SEEN_ON_KEYSERVERS }, + UpdatedKeys.MASTER_KEY_ID + " = ?", + new String[] { "" + masterKeyId }, + null + ); + if (cursor == null) { + return null; + } + + Boolean seenOnKeyservers; + try { + if (!cursor.moveToNext()) { + return null; + } + seenOnKeyservers = cursor.isNull(0) ? null : cursor.getInt(0) != 0; + } finally { + cursor.close(); + } + return seenOnKeyservers; + } + + public final byte[] loadPublicKeyRingData(long masterKeyId) throws NotFoundException { byte[] data = (byte[]) getGenericDataOrNull(KeyRingData.buildPublicKeyRingUri(masterKeyId), KeyRingData.KEY_RING_DATA, FIELD_TYPE_BLOB); 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 0f92bc6af..7004a367f 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeyWritableRepository.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeyWritableRepository.java @@ -1348,12 +1348,26 @@ public class KeyWritableRepository extends KeyRepository { return ContentProviderOperation.newInsert(uri).withValues(values).build(); } - public Uri renewKeyLastUpdatedTime(long masterKeyId) { + public Uri renewKeyLastUpdatedTime(long masterKeyId, boolean seenOnKeyservers) { + boolean isFirstKeyserverStatusCheck = getSeenOnKeyservers(masterKeyId) == null; + ContentValues values = new ContentValues(); values.put(UpdatedKeys.MASTER_KEY_ID, masterKeyId); values.put(UpdatedKeys.LAST_UPDATED, GregorianCalendar.getInstance().getTimeInMillis() / 1000); + if (seenOnKeyservers || isFirstKeyserverStatusCheck) { + values.put(UpdatedKeys.SEEN_ON_KEYSERVERS, seenOnKeyservers); + } + // this will actually update/replace, doing the right thing™ for seenOnKeyservers value + // see `KeychainProvider.insert()` return mContentResolver.insert(UpdatedKeys.CONTENT_URI, values); } + public void resetAllLastUpdatedTimes() { + ContentValues values = new ContentValues(); + values.putNull(UpdatedKeys.LAST_UPDATED); + values.putNull(UpdatedKeys.SEEN_ON_KEYSERVERS); + mContentResolver.update(UpdatedKeys.CONTENT_URI, values, null, null); + } + } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainContract.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainContract.java index 2f6f4042e..79bef905c 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainContract.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainContract.java @@ -55,6 +55,7 @@ public class KeychainContract { interface UpdatedKeysColumns { String MASTER_KEY_ID = "master_key_id"; // not a database id String LAST_UPDATED = "last_updated"; // time since epoch in seconds + String SEEN_ON_KEYSERVERS = "seen_on_keyservers"; } interface UserPacketsColumns { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainDatabase.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainDatabase.java index 58266db08..8c45264ab 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainDatabase.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainDatabase.java @@ -52,7 +52,7 @@ import org.sufficientlysecure.keychain.util.Log; */ public class KeychainDatabase extends SQLiteOpenHelper { private static final String DATABASE_NAME = "openkeychain.db"; - private static final int DATABASE_VERSION = 21; + private static final int DATABASE_VERSION = 22; private Context mContext; public interface Tables { @@ -151,6 +151,7 @@ public class KeychainDatabase extends SQLiteOpenHelper { "CREATE TABLE IF NOT EXISTS " + Tables.UPDATED_KEYS + " (" + UpdatedKeysColumns.MASTER_KEY_ID + " INTEGER PRIMARY KEY, " + UpdatedKeysColumns.LAST_UPDATED + " INTEGER, " + + UpdatedKeysColumns.SEEN_ON_KEYSERVERS + " INTEGER, " + "FOREIGN KEY(" + UpdatedKeysColumns.MASTER_KEY_ID + ") REFERENCES " + Tables.KEY_RINGS_PUBLIC + "(" + KeyRingsColumns.MASTER_KEY_ID + ") ON DELETE CASCADE" + ")"; @@ -197,6 +198,7 @@ public class KeychainDatabase extends SQLiteOpenHelper { db.execSQL(CREATE_UPDATE_KEYS); db.execSQL(CREATE_API_APPS); db.execSQL(CREATE_API_APPS_ALLOWED_KEYS); + db.execSQL(CREATE_OVERRIDDEN_WARNINGS); db.execSQL("CREATE INDEX keys_by_rank ON keys (" + KeysColumns.RANK + ");"); db.execSQL("CREATE INDEX uids_by_rank ON user_packets (" + UserPacketsColumns.RANK + ", " @@ -307,8 +309,16 @@ public class KeychainDatabase extends SQLiteOpenHelper { } */ case 20: - db.execSQL(CREATE_OVERRIDDEN_WARNINGS); - if (oldVersion == 18 || oldVersion == 19 || oldVersion == 20) { + db.execSQL( + "CREATE TABLE IF NOT EXISTS overridden_warnings (" + + "_id INTEGER PRIMARY KEY AUTOINCREMENT, " + + "identifier TEXT NOT NULL UNIQUE " + + ")"); + + case 21: + db.execSQL("ALTER TABLE updated_keys ADD COLUMN seen_on_keyservers INTEGER;"); + + if (oldVersion == 18 || oldVersion == 19 || oldVersion == 20 || oldVersion == 21) { // no consolidate for now, often crashes! return; } 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 1554cd46b..2e8177d88 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java @@ -639,10 +639,10 @@ public class KeychainProvider extends ContentProvider { case UPDATED_KEYS_SPECIFIC: { HashMap projectionMap = new HashMap<>(); qb.setTables(Tables.UPDATED_KEYS); - projectionMap.put(UpdatedKeys.MASTER_KEY_ID, Tables.UPDATED_KEYS + "." - + UpdatedKeys.MASTER_KEY_ID); - projectionMap.put(UpdatedKeys.LAST_UPDATED, Tables.UPDATED_KEYS + "." - + UpdatedKeys.LAST_UPDATED); + projectionMap.put(UpdatedKeys.MASTER_KEY_ID, Tables.UPDATED_KEYS + "." + UpdatedKeys.MASTER_KEY_ID); + projectionMap.put(UpdatedKeys.LAST_UPDATED, Tables.UPDATED_KEYS + "." + UpdatedKeys.LAST_UPDATED); + projectionMap.put(UpdatedKeys.SEEN_ON_KEYSERVERS, + Tables.UPDATED_KEYS + "." + UpdatedKeys.SEEN_ON_KEYSERVERS); qb.setProjectionMap(projectionMap); if (match == UPDATED_KEYS_SPECIFIC) { qb.appendWhere(UpdatedKeys.MASTER_KEY_ID + " = "); @@ -780,9 +780,14 @@ public class KeychainProvider extends ContentProvider { break; } case UPDATED_KEYS: { - long updatedKeyId = db.replace(Tables.UPDATED_KEYS, null, values); - rowUri = UpdatedKeys.CONTENT_URI.buildUpon().appendPath("" + updatedKeyId) - .build(); + keyId = values.getAsLong(UpdatedKeys.MASTER_KEY_ID); + try { + db.insertOrThrow(Tables.UPDATED_KEYS, null, values); + } catch (SQLiteConstraintException e) { + db.update(Tables.UPDATED_KEYS, values, + UpdatedKeys.MASTER_KEY_ID + " = ?", new String[] { Long.toString(keyId) }); + } + rowUri = UpdatedKeys.CONTENT_URI; break; } case API_APPS: { @@ -911,6 +916,19 @@ public class KeychainProvider extends ContentProvider { buildDefaultApiAppsSelection(uri, selection), selectionArgs); break; } + case UPDATED_KEYS: { + if (values.size() != 2 || + !values.containsKey(UpdatedKeys.SEEN_ON_KEYSERVERS) || + !values.containsKey(UpdatedKeys.LAST_UPDATED) || + values.get(UpdatedKeys.LAST_UPDATED) != null || + values.get(UpdatedKeys.SEEN_ON_KEYSERVERS) != null || + selection != null || selectionArgs != null) { + throw new UnsupportedOperationException("can only reset all keys"); + } + + db.update(Tables.UPDATED_KEYS, values, null, null); + break; + } default: { throw new UnsupportedOperationException("Unknown uri: " + uri); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsKeyserverFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsKeyserverFragment.java index 2175b9af9..fd5feef87 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsKeyserverFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsKeyserverFragment.java @@ -18,6 +18,11 @@ package org.sufficientlysecure.keychain.ui; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + import android.os.Bundle; import android.os.Handler; import android.os.Message; @@ -38,6 +43,8 @@ import android.widget.ImageView; import android.widget.TextView; import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.keyimport.ParcelableHkpKeyserver; +import org.sufficientlysecure.keychain.provider.KeyWritableRepository; import org.sufficientlysecure.keychain.ui.dialog.AddEditKeyserverDialogFragment; import org.sufficientlysecure.keychain.ui.util.FormattingUtils; import org.sufficientlysecure.keychain.ui.util.Notify; @@ -45,20 +52,20 @@ import org.sufficientlysecure.keychain.ui.util.recyclerview.ItemTouchHelperAdapt import org.sufficientlysecure.keychain.ui.util.recyclerview.ItemTouchHelperDragCallback; import org.sufficientlysecure.keychain.ui.util.recyclerview.ItemTouchHelperViewHolder; import org.sufficientlysecure.keychain.ui.util.recyclerview.RecyclerItemClickListener; -import org.sufficientlysecure.keychain.keyimport.ParcelableHkpKeyserver; import org.sufficientlysecure.keychain.util.Preferences; -import java.util.ArrayList; -import java.util.Collections; public class SettingsKeyserverFragment extends Fragment implements RecyclerItemClickListener.OnItemClickListener { private static final String ARG_KEYSERVER_ARRAY = "arg_keyserver_array"; private ItemTouchHelper mItemTouchHelper; - private ArrayList mKeyservers; + private ArrayList mKeyserversMutable; + private List mKeyservers; private KeyserverListAdapter mAdapter; + private KeyWritableRepository databaseReadWriteInteractor; + public static SettingsKeyserverFragment newInstance(ArrayList keyservers) { Bundle args = new Bundle(); args.putParcelableArrayList(ARG_KEYSERVER_ARRAY, keyservers); @@ -72,6 +79,7 @@ public class SettingsKeyserverFragment extends Fragment implements RecyclerItemC @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + databaseReadWriteInteractor = KeyWritableRepository.createDatabaseReadWriteInteractor(getContext()); return inflater.inflate(R.layout.settings_keyserver_fragment, null); } @@ -80,9 +88,10 @@ public class SettingsKeyserverFragment extends Fragment implements RecyclerItemC public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - mKeyservers = getArguments().getParcelableArrayList(ARG_KEYSERVER_ARRAY); + mKeyserversMutable = getArguments().getParcelableArrayList(ARG_KEYSERVER_ARRAY); + mKeyservers = Collections.unmodifiableList(new ArrayList<>(mKeyserversMutable)); - mAdapter = new KeyserverListAdapter(mKeyservers); + mAdapter = new KeyserverListAdapter(mKeyserversMutable); RecyclerView recyclerView = (RecyclerView) view.findViewById(R.id.keyserver_recycler_view); // recyclerView.setHasFixedSize(true); // the size of the first item changes @@ -143,7 +152,7 @@ public class SettingsKeyserverFragment extends Fragment implements RecyclerItemC if (deleted) { Notify.create(getActivity(), getActivity().getString( - R.string.keyserver_preference_deleted, mKeyservers.get(position)), + R.string.keyserver_preference_deleted, mKeyserversMutable.get(position)), Notify.Style.OK) .show(); deleteKeyserver(position); @@ -187,27 +196,27 @@ public class SettingsKeyserverFragment extends Fragment implements RecyclerItemC } private void addKeyserver(ParcelableHkpKeyserver keyserver) { - mKeyservers.add(keyserver); - mAdapter.notifyItemInserted(mKeyservers.size() - 1); + mKeyserversMutable.add(keyserver); + mAdapter.notifyItemInserted(mKeyserversMutable.size() - 1); saveKeyserverList(); } private void editKeyserver(ParcelableHkpKeyserver newKeyserver, int position) { - mKeyservers.set(position, newKeyserver); + mKeyserversMutable.set(position, newKeyserver); mAdapter.notifyItemChanged(position); saveKeyserverList(); } private void deleteKeyserver(int position) { - if (mKeyservers.size() == 1) { + if (mKeyserversMutable.size() == 1) { Notify.create(getActivity(), R.string.keyserver_preference_cannot_delete_last, Notify.Style.ERROR).show(); return; } - mKeyservers.remove(position); + mKeyserversMutable.remove(position); // we use this mAdapter.notifyItemRemoved(position); - if (position == 0 && mKeyservers.size() > 0) { + if (position == 0 && mKeyserversMutable.size() > 0) { // if we deleted the first item, we need the adapter to redraw the new first item mAdapter.notifyItemChanged(0); } @@ -215,13 +224,20 @@ public class SettingsKeyserverFragment extends Fragment implements RecyclerItemC } private void saveKeyserverList() { - Preferences.getPreferences(getActivity()).setKeyServers(mKeyservers); + if (mKeyserversMutable.equals(mKeyservers)) { + return; + } + + Preferences.getPreferences(getActivity()).setKeyServers(mKeyserversMutable); + mKeyservers = Collections.unmodifiableList(new ArrayList<>(mKeyserversMutable)); + + databaseReadWriteInteractor.resetAllLastUpdatedTimes(); } @Override public void onItemClick(View view, int position) { startEditKeyserverDialog(AddEditKeyserverDialogFragment.DialogAction.EDIT, - mKeyservers.get(position), position); + mKeyserversMutable.get(position), position); } public class KeyserverListAdapter extends RecyclerView.Adapter 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 bd61897c1..a5e6af591 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 @@ -35,10 +35,12 @@ import org.sufficientlysecure.keychain.operations.results.OperationResult; import org.sufficientlysecure.keychain.ui.base.LoaderFragment; import org.sufficientlysecure.keychain.ui.keyview.presenter.IdentitiesPresenter; import org.sufficientlysecure.keychain.ui.keyview.presenter.KeyHealthPresenter; +import org.sufficientlysecure.keychain.ui.keyview.presenter.KeyserverStatusPresenter; import org.sufficientlysecure.keychain.ui.keyview.presenter.SystemContactPresenter; import org.sufficientlysecure.keychain.ui.keyview.presenter.ViewKeyMvpView; import org.sufficientlysecure.keychain.ui.keyview.view.IdentitiesCardView; import org.sufficientlysecure.keychain.ui.keyview.view.KeyHealthView; +import org.sufficientlysecure.keychain.ui.keyview.view.KeyserverStatusView; import org.sufficientlysecure.keychain.ui.keyview.view.SystemContactCardView; @@ -51,6 +53,7 @@ public class ViewKeyFragment extends LoaderFragment implements ViewKeyMvpView { 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 mIdentitiesCardView; private IdentitiesPresenter mIdentitiesPresenter; @@ -59,8 +62,10 @@ public class ViewKeyFragment extends LoaderFragment implements ViewKeyMvpView { SystemContactPresenter mSystemContactPresenter; KeyHealthView mKeyStatusHealth; + KeyserverStatusView mKeyStatusKeyserver; KeyHealthPresenter mKeyHealthPresenter; + KeyserverStatusPresenter mKeyserverStatusPresenter; /** * Creates new instance of this fragment @@ -85,6 +90,7 @@ public class ViewKeyFragment extends LoaderFragment implements ViewKeyMvpView { mSystemContactCard = (SystemContactCardView) view.findViewById(R.id.linked_system_contact_card); mKeyStatusHealth = (KeyHealthView) view.findViewById(R.id.key_status_health); + mKeyStatusKeyserver = (KeyserverStatusView) view.findViewById(R.id.key_status_keyserver); return root; } @@ -107,6 +113,10 @@ public class ViewKeyFragment extends LoaderFragment implements ViewKeyMvpView { mKeyHealthPresenter = new KeyHealthPresenter( getContext(), mKeyStatusHealth, LOADER_ID_SUBKEY_STATUS, masterKeyId, mIsSecret); mKeyHealthPresenter.startLoader(getLoaderManager()); + + mKeyserverStatusPresenter = new KeyserverStatusPresenter( + getContext(), mKeyStatusKeyserver, LOADER_ID_KEYSERVER_STATUS, masterKeyId, mIsSecret); + mKeyserverStatusPresenter.startLoader(getLoaderManager()); } @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/IdentityLoader.java index cf62eec0a..a2edbaffe 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/IdentityLoader.java @@ -34,6 +34,7 @@ import com.google.auto.value.AutoValue; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.linked.LinkedAttribute; import org.sufficientlysecure.keychain.linked.UriAttribute; +import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets; import org.sufficientlysecure.keychain.ui.keyview.loader.IdentityLoader.IdentityInfo; @@ -72,6 +73,7 @@ public class IdentityLoader extends AsyncTaskLoader> { private List cachedResult; + private ForceLoadContentObserver identityObserver; public IdentityLoader(Context context, ContentResolver contentResolver, long masterKeyId, boolean showLinkedIds) { super(context); @@ -79,6 +81,8 @@ public class IdentityLoader extends AsyncTaskLoader> { this.contentResolver = contentResolver; this.masterKeyId = masterKeyId; this.showLinkedIds = showLinkedIds; + + this.identityObserver = new ForceLoadContentObserver(); } @Override @@ -169,6 +173,16 @@ public class IdentityLoader extends AsyncTaskLoader> { 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 { 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/KeyserverStatusLoader.java new file mode 100644 index 000000000..2ae535acf --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/loader/KeyserverStatusLoader.java @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2017 Vincent Breitmoser + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui.keyview.loader; + + +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; + + +public class KeyserverStatusLoader extends AsyncTaskLoader { + public static final String[] PROJECTION = new String[] { + UpdatedKeys.LAST_UPDATED, + UpdatedKeys.SEEN_ON_KEYSERVERS + }; + private static final int INDEX_LAST_UPDATED = 0; + private static final int INDEX_SEEN_ON_KEYSERVERS = 1; + + + private final ContentResolver contentResolver; + 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(); + } + + @Override + public KeyserverStatus loadInBackground() { + Cursor cursor = contentResolver.query(UpdatedKeys.CONTENT_URI, PROJECTION, + UpdatedKeys.MASTER_KEY_ID + " = ?", new String[] { Long.toString(masterKeyId) }, null); + if (cursor == null) { + Log.e(Constants.TAG, "Error loading key items!"); + return null; + } + + try { + if (cursor.moveToFirst()) { + if (cursor.isNull(INDEX_SEEN_ON_KEYSERVERS) || cursor.isNull(INDEX_LAST_UPDATED)) { + return new KeyserverStatus(masterKeyId); + } + + boolean isPublished = cursor.getInt(INDEX_SEEN_ON_KEYSERVERS) != 0; + Date lastUpdated = new Date(cursor.getLong(INDEX_LAST_UPDATED) * 1000); + + return new KeyserverStatus(masterKeyId, isPublished, lastUpdated); + } + + return new KeyserverStatus(masterKeyId); + } finally { + cursor.close(); + } + } + + @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; + private final Date lastUpdated; + + KeyserverStatus(long masterKeyId, boolean isPublished, Date lastUpdated) { + this.masterKeyId = masterKeyId; + this.isPublished = isPublished; + this.lastUpdated = lastUpdated; + } + + KeyserverStatus(long masterKeyId) { + this.masterKeyId = masterKeyId; + this.isPublished = false; + this.lastUpdated = null; + } + + long getMasterKeyId() { + return masterKeyId; + } + + public boolean hasBeenUpdated() { + return lastUpdated != null; + } + + public boolean isPublished() { + if (lastUpdated == null) { + throw new IllegalStateException("Cannot get publication state if key has never been updated!"); + } + return isPublished; + } + + public Date getLastUpdated() { + return lastUpdated; + } + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/presenter/KeyHealthPresenter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/presenter/KeyHealthPresenter.java index c3fdb832b..979875c1a 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 @@ -115,9 +115,11 @@ public class KeyHealthPresenter implements LoaderCallbacks { view.setKeyStatus(keyHealthStatus); view.setPrimaryExpiryDate(subkeyStatus.keyCertify.mExpiry); view.setShowExpander(false); + view.hideExpandedInfo(); } else { view.setKeyStatus(keyHealthStatus); view.setShowExpander(keyHealthStatus != KeyHealthStatus.REVOKED); + view.hideExpandedInfo(); } } 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 new file mode 100644 index 000000000..3a1070e7f --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/presenter/KeyserverStatusPresenter.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2017 Vincent Breitmoser + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui.keyview.presenter; + + +import java.util.Date; + +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 org.sufficientlysecure.keychain.ui.keyview.loader.KeyserverStatusLoader; +import org.sufficientlysecure.keychain.ui.keyview.loader.KeyserverStatusLoader.KeyserverStatus; + + +public class KeyserverStatusPresenter implements LoaderCallbacks { + 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, + 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); + } + + @Override + public Loader onCreateLoader(int id, Bundle args) { + return new KeyserverStatusLoader(context, context.getContentResolver(), masterKeyId); + } + + @Override + public void onLoadFinished(Loader loader, KeyserverStatus keyserverStatus) { + if (keyserverStatus.hasBeenUpdated()) { + if (keyserverStatus.isPublished()) { + view.setDisplayStatusPublished(); + } else { + view.setDisplayStatusNotPublished(); + } + view.setLastUpdated(keyserverStatus.getLastUpdated()); + } else { + view.setDisplayStatusUnknown(); + } + } + + @Override + public void onLoaderReset(Loader loader) { + + } + + public interface KeyserverStatusMvpView { + void setDisplayStatusPublished(); + void setDisplayStatusNotPublished(); + void setLastUpdated(Date lastUpdated); + void setDisplayStatusUnknown(); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/view/KeyserverStatusView.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/view/KeyserverStatusView.java new file mode 100644 index 000000000..0d0e3491e --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/view/KeyserverStatusView.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2017 Vincent Breitmoser + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui.keyview.view; + + +import java.util.Date; + +import android.content.Context; +import android.support.annotation.ColorRes; +import android.support.annotation.DrawableRes; +import android.support.annotation.StringRes; +import android.support.v4.content.ContextCompat; +import android.text.format.DateFormat; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.TextView; + +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.ui.keyview.presenter.KeyserverStatusPresenter.KeyserverStatusMvpView; + + +public class KeyserverStatusView extends FrameLayout implements KeyserverStatusMvpView { + private final View vLayout; + private final TextView vTitle; + private final TextView vSubtitle; + private final ImageView vIcon; + + public KeyserverStatusView(Context context, AttributeSet attrs) { + super(context, attrs); + + View view = LayoutInflater.from(context).inflate(R.layout.key_keyserver_status_layout, this, true); + + vLayout = view.findViewById(R.id.keyserver_status_layout); + + vTitle = (TextView) view.findViewById(R.id.keyserver_status_title); + vSubtitle = (TextView) view.findViewById(R.id.keyserver_status_subtitle); + vIcon = (ImageView) view.findViewById(R.id.keyserver_icon); +// vExpander = (ImageView) view.findViewById(R.id.key_health_expander); + } + + private enum KeyserverDisplayStatus { + PUBLISHED (R.string.keyserver_title_published, R.drawable.ic_cloud_black_24dp, R.color.md_grey_900), + NOT_PUBLISHED (R.string.keyserver_title_not_published, R.drawable.ic_cloud_off_24dp, R.color.md_grey_900), + UNKNOWN (R.string.keyserver_title_unknown, R.drawable.ic_cloud_unknown_24dp, R.color.md_grey_900); + + @StringRes + private final int title; + @DrawableRes + private final int icon; + @ColorRes + private final int iconColor; + + KeyserverDisplayStatus(@StringRes int title, @DrawableRes int icon, @ColorRes int iconColor) { + this.title = title; + this.icon = icon; + this.iconColor = iconColor; + } + } + + @Override + public void setDisplayStatusPublished() { + setDisplayStatus(KeyserverDisplayStatus.PUBLISHED); + } + + @Override + public void setDisplayStatusNotPublished() { + setDisplayStatus(KeyserverDisplayStatus.NOT_PUBLISHED); + } + + @Override + public void setDisplayStatusUnknown() { + setDisplayStatus(KeyserverDisplayStatus.UNKNOWN); + vSubtitle.setText(R.string.keyserver_last_updated_never); + } + + @Override + public void setLastUpdated(Date lastUpdated) { + String lastUpdatedText = DateFormat.getMediumDateFormat(getContext()).format(lastUpdated); + vSubtitle.setText(getResources().getString(R.string.keyserver_last_updated, lastUpdatedText)); + } + + private void setDisplayStatus(KeyserverDisplayStatus displayStatus) { + vTitle.setText(displayStatus.title); + vIcon.setImageResource(displayStatus.icon); + vIcon.setColorFilter(ContextCompat.getColor(getContext(), displayStatus.iconColor)); + + setVisibility(View.VISIBLE); + } +} diff --git a/OpenKeychain/src/main/res/drawable-hdpi/ic_cloud_off_24dp.png b/OpenKeychain/src/main/res/drawable-hdpi/ic_cloud_off_24dp.png new file mode 100644 index 000000000..fabb499c7 Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-hdpi/ic_cloud_off_24dp.png differ diff --git a/OpenKeychain/src/main/res/drawable-hdpi/ic_cloud_unknown_24dp.png b/OpenKeychain/src/main/res/drawable-hdpi/ic_cloud_unknown_24dp.png new file mode 100644 index 000000000..a4c7be5df Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-hdpi/ic_cloud_unknown_24dp.png differ diff --git a/OpenKeychain/src/main/res/drawable-mdpi/ic_cloud_off_24dp.png b/OpenKeychain/src/main/res/drawable-mdpi/ic_cloud_off_24dp.png new file mode 100644 index 000000000..62dc80873 Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-mdpi/ic_cloud_off_24dp.png differ diff --git a/OpenKeychain/src/main/res/drawable-mdpi/ic_cloud_unknown_24dp.png b/OpenKeychain/src/main/res/drawable-mdpi/ic_cloud_unknown_24dp.png new file mode 100644 index 000000000..2e60f4c83 Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-mdpi/ic_cloud_unknown_24dp.png differ diff --git a/OpenKeychain/src/main/res/drawable-xhdpi/ic_cloud_off_24dp.png b/OpenKeychain/src/main/res/drawable-xhdpi/ic_cloud_off_24dp.png new file mode 100644 index 000000000..fa6dd8da2 Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-xhdpi/ic_cloud_off_24dp.png differ diff --git a/OpenKeychain/src/main/res/drawable-xhdpi/ic_cloud_unknown_24dp.png b/OpenKeychain/src/main/res/drawable-xhdpi/ic_cloud_unknown_24dp.png new file mode 100644 index 000000000..19f43d423 Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-xhdpi/ic_cloud_unknown_24dp.png differ diff --git a/OpenKeychain/src/main/res/drawable-xxhdpi/ic_cloud_off_24dp.png b/OpenKeychain/src/main/res/drawable-xxhdpi/ic_cloud_off_24dp.png new file mode 100644 index 000000000..79d9717f8 Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-xxhdpi/ic_cloud_off_24dp.png differ diff --git a/OpenKeychain/src/main/res/drawable-xxhdpi/ic_cloud_unknown_24dp.png b/OpenKeychain/src/main/res/drawable-xxhdpi/ic_cloud_unknown_24dp.png new file mode 100644 index 000000000..49a9935d4 Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-xxhdpi/ic_cloud_unknown_24dp.png differ diff --git a/OpenKeychain/src/main/res/drawable-xxxhdpi/ic_cloud_off_24dp.png b/OpenKeychain/src/main/res/drawable-xxxhdpi/ic_cloud_off_24dp.png new file mode 100644 index 000000000..93003e9a0 Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-xxxhdpi/ic_cloud_off_24dp.png differ diff --git a/OpenKeychain/src/main/res/drawable-xxxhdpi/ic_cloud_unknown_24dp.png b/OpenKeychain/src/main/res/drawable-xxxhdpi/ic_cloud_unknown_24dp.png new file mode 100644 index 000000000..f9196f6f2 Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-xxxhdpi/ic_cloud_unknown_24dp.png differ diff --git a/OpenKeychain/src/main/res/layout/key_health_card_content.xml b/OpenKeychain/src/main/res/layout/key_health_card_content.xml index 2aaf6e619..31b942a59 100644 --- a/OpenKeychain/src/main/res/layout/key_health_card_content.xml +++ b/OpenKeychain/src/main/res/layout/key_health_card_content.xml @@ -70,7 +70,9 @@ android:layout_width="match_parent" android:layout_height="1dip" android:id="@+id/key_health_divider" - android:background="?android:attr/listDivider" /> + android:background="?android:attr/listDivider" + android:visibility="gone" + tools:visibility="visible" /> + tools:visibility="gone"> + + + + + + + + + + + + + + + + + + + + diff --git a/OpenKeychain/src/main/res/layout/subkey_status_card_content.xml b/OpenKeychain/src/main/res/layout/subkey_status_card_content.xml index 51f980ad5..ab6a23a74 100644 --- a/OpenKeychain/src/main/res/layout/subkey_status_card_content.xml +++ b/OpenKeychain/src/main/res/layout/subkey_status_card_content.xml @@ -1,7 +1,9 @@ + tools:parentTag="LinearLayout" + tools:layout_width="match_parent" + tools:layout_height="match_parent"> + + + + diff --git a/OpenKeychain/src/main/res/values/strings.xml b/OpenKeychain/src/main/res/values/strings.xml index 31a81e651..680d27bac 100644 --- a/OpenKeychain/src/main/res/values/strings.xml +++ b/OpenKeychain/src/main/res/values/strings.xml @@ -1842,6 +1842,12 @@ "Healthy (Partially Stripped)" "Click for details" + "Published" + "Not Published" + "Keyserver status: Unknown" + "Last checked: %s" + Last checked: Never + "This key uses the %1$s algorithm with a strength of %2$s bits. A secure key should have a strength of 2048 bits." "This key can\'t be upgraded. For secure communication, the owner must generate a new key." diff --git a/graphics/drawables/ic_cloud_off.svg b/graphics/drawables/ic_cloud_off.svg new file mode 100644 index 000000000..b2373ff16 --- /dev/null +++ b/graphics/drawables/ic_cloud_off.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/graphics/drawables/ic_cloud_unknown.svg b/graphics/drawables/ic_cloud_unknown.svg new file mode 100644 index 000000000..8fda8d19f --- /dev/null +++ b/graphics/drawables/ic_cloud_unknown.svg @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/graphics/update-drawables.sh b/graphics/update-drawables.sh index 4407315b7..f4c998e4b 100755 --- a/graphics/update-drawables.sh +++ b/graphics/update-drawables.sh @@ -22,7 +22,7 @@ SRC_DIR=./drawables/ #inkscape -w 512 -h 512 -e "$PLAY_DIR/$NAME.png" $NAME.svg -for NAME in "broken_heart" "ic_cloud_search" "ic_action_encrypt_file" "ic_action_encrypt_text" "ic_action_verified_cutout" "ic_action_encrypt_copy" "ic_action_encrypt_paste" "ic_action_encrypt_save" "ic_action_encrypt_share" "status_lock_closed" "status_lock_error" "status_lock_open" "status_signature_expired_cutout" "status_signature_invalid_cutout" "status_signature_revoked_cutout" "status_signature_unknown_cutout" "status_signature_unverified_cutout" "status_signature_verified_cutout" "key_flag_authenticate" "key_flag_certify" "key_flag_encrypt" "key_flag_sign" "yubi_icon" "ic_stat_notify" "status_signature_verified_inner" "link" "octo_link" +for NAME in "ic_cloud_unknown" "ic_cloud_off" "broken_heart" "ic_cloud_search" "ic_action_encrypt_file" "ic_action_encrypt_text" "ic_action_verified_cutout" "ic_action_encrypt_copy" "ic_action_encrypt_paste" "ic_action_encrypt_save" "ic_action_encrypt_share" "status_lock_closed" "status_lock_error" "status_lock_open" "status_signature_expired_cutout" "status_signature_invalid_cutout" "status_signature_revoked_cutout" "status_signature_unknown_cutout" "status_signature_unverified_cutout" "status_signature_verified_cutout" "key_flag_authenticate" "key_flag_certify" "key_flag_encrypt" "key_flag_sign" "yubi_icon" "ic_stat_notify" "status_signature_verified_inner" "link" "octo_link" do echo $NAME inkscape -w 24 -h 24 -e "$MDPI_DIR/${NAME}_24dp.png" "$SRC_DIR/$NAME.svg"