aboutsummaryrefslogtreecommitdiffstats
path: root/OpenKeychain/src/main/java/org/sufficientlysecure/keychain
diff options
context:
space:
mode:
authorVincent Breitmoser <valodim@mugenguild.com>2015-09-14 16:21:04 +0200
committerVincent Breitmoser <valodim@mugenguild.com>2015-09-14 16:21:04 +0200
commit3814ae7d53a22ba89f1e39d7a4661016f76cf8c8 (patch)
treed83d41d3c424a784da144515665fb41ef650aa52 /OpenKeychain/src/main/java/org/sufficientlysecure/keychain
parentdbaf7070ead596f5c70ad48fc55aada2f77a856a (diff)
parentd5dd6a49c8156a699b3fbbbeef06658e1c232c16 (diff)
downloadopen-keychain-3814ae7d53a22ba89f1e39d7a4661016f76cf8c8.tar.gz
open-keychain-3814ae7d53a22ba89f1e39d7a4661016f76cf8c8.tar.bz2
open-keychain-3814ae7d53a22ba89f1e39d7a4661016f76cf8c8.zip
Merge branch 'master' into mime4j
Conflicts: OpenKeychain/build.gradle OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/OperationResult.java OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptListFragment.java OpenKeychain/src/main/res/values/strings.xml
Diffstat (limited to 'OpenKeychain/src/main/java/org/sufficientlysecure/keychain')
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/Constants.java27
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/KeychainApplication.java12
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/HkpKeyserver.java8
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/LinkedAttribute.java32
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/LinkedResource.java25
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/LinkedTokenResource.java300
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/UriAttribute.java79
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/resources/DnsResource.java130
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/resources/GenericHttpsResource.java95
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/resources/GithubResource.java218
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/resources/TwitterResource.java250
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ExportOperation.java2
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ImportOperation.java153
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/KeybaseVerificationOperation.java4
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/RevokeOperation.java52
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/LinkedVerifyResult.java49
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/OperationResult.java117
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedKeyRing.java12
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerifyOperation.java (renamed from OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerify.java)693
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpHelper.java32
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSecurityConstants.java74
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedKeyRing.java11
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedPublicKey.java1
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/WrappedUserAttribute.java8
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainContract.java32
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainDatabase.java18
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java83
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/ProviderHelper.java97
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService.java189
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService2.java72
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppsListFragment.java4
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/SelectSignKeyIdActivity.java25
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/CertifyActionsParcel.java16
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ContactSyncAdapterService.java2
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainService.java4
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeyserverSyncAdapterService.java516
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/PassphraseCacheService.java8
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/BackupFragment.java38
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyFingerprintActivity.java9
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyFingerprintFragment.java47
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyKeyFragment.java2
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptActivity.java25
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptListFragment.java140
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DeleteKeyDialogActivity.java1
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EditKeyFragment.java2
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptDecryptOverviewFragment.java6
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptFilesFragment.java26
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysFileFragment.java4
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java14
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/LogDisplayFragment.java189
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/NfcOperationActivity.java29
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/OrbotRequiredDialogActivity.java63
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PassphraseDialogActivity.java73
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PassphraseWizardActivity.java577
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsActivity.java190
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsKeyServerActivity.java14
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java157
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvActivity.java6
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvUserIdsFragment.java2
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyFragment.java164
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyKeybaseFragment.java (renamed from OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyTrustFragment.java)171
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeyAdapter.java3
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/LinkedIdsAdapter.java238
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/LinkedIdsCertAdapter.java57
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/MultiUserIdsAdapter.java2
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/SelectKeyCursorAdapter.java2
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/UserAttributesAdapter.java19
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/UserIdsAdapter.java2
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseActivity.java2
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/CryptoOperationFragment.java12
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/CryptoOperationHelper.java26
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/FileDialogFragment.java22
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateFinalFragment.java225
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateGithubFragment.java706
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateHttpsStep1Fragment.java127
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateHttpsStep2Fragment.java172
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateTwitterStep1Fragment.java132
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateTwitterStep2Fragment.java121
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdSelectFragment.java105
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdViewFragment.java560
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdWizard.java164
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/ExperimentalWordConfirm.java126
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/KeyFormattingUtils.java4
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/SubtleAttentionSeeker.java24
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/ThemeChanger.java4
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/CertListWidget.java143
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/CertifyKeySpinner.java18
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/KeySpinner.java14
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/PrefixedEditText.java45
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/StatusIndicator.java45
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ExportHelper.java9
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/FileHelper.java197
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/FilterCursorWrapper.java76
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Preferences.java61
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/orbot/OrbotHelper.java36
95 files changed, 6888 insertions, 2010 deletions
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/Constants.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/Constants.java
index 3d58602ab..6a9656b28 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/Constants.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/Constants.java
@@ -19,14 +19,9 @@ package org.sufficientlysecure.keychain;
import android.os.Environment;
-import org.spongycastle.bcpg.HashAlgorithmTags;
-import org.spongycastle.bcpg.SymmetricKeyAlgorithmTags;
import org.spongycastle.jce.provider.BouncyCastleProvider;
-import org.sufficientlysecure.keychain.BuildConfig;
-
import java.io.File;
-import java.net.InetSocketAddress;
import java.net.Proxy;
public final class Constants {
@@ -34,6 +29,7 @@ public final class Constants {
public static final boolean DEBUG = BuildConfig.DEBUG;
public static final boolean DEBUG_LOG_DB_QUERIES = false;
public static final boolean DEBUG_SYNC_REMOVE_CONTACTS = false;
+ public static final boolean DEBUG_KEYSERVER_SYNC = false;
public static final String TAG = DEBUG ? "Keychain D" : "Keychain";
@@ -43,7 +39,7 @@ public final class Constants {
public static final String ACCOUNT_TYPE = BuildConfig.ACCOUNT_TYPE;
public static final String CUSTOM_CONTACT_DATA_MIME_TYPE = "vnd.android.cursor.item/vnd.org.sufficientlysecure.keychain.key";
- public static final String PROVIDER_AUTHORITY = BuildConfig.APPLICATION_ID + ".provider";
+ public static final String PROVIDER_AUTHORITY = BuildConfig.PROVIDER_CONTENT_AUTHORITY;
public static final String TEMPSTORAGE_AUTHORITY = BuildConfig.APPLICATION_ID + ".tempstorage";
public static final String CLIPBOARD_LABEL = "Keychain";
@@ -81,6 +77,11 @@ public final class Constants {
public static final File APP_DIR_FILE = new File(APP_DIR, "export.asc");
}
+ public static final class Notification {
+ public static final int PASSPHRASE_CACHE = 1;
+ public static final int KEYSERVER_SYNC_FAIL_ORBOT = 2;
+ }
+
public static final class Pref {
public static final String PASSPHRASE_CACHE_TTL = "passphraseCacheTtl";
public static final String PASSPHRASE_CACHE_SUBS = "passphraseCacheSubs";
@@ -104,12 +105,24 @@ public final class Constants {
public static final String PROXY_PORT = "proxyPort";
public static final String PROXY_TYPE = "proxyType";
public static final String THEME = "theme";
+ // keyserver sync settings
+ public static final String SYNC_CONTACTS = "syncContacts";
+ public static final String SYNC_KEYSERVER = "syncKeyserver";
+ // other settings
+ public static final String EXPERIMENTAL_ENABLE_WORD_CONFIRM = "experimentalEnableWordConfirm";
+ public static final String EXPERIMENTAL_ENABLE_LINKED_IDENTITIES = "experimentalEnableLinkedIdentities";
+ public static final String EXPERIMENTAL_ENABLE_KEYBASE = "experimentalEnableKeybase";
public static final class Theme {
public static final String LIGHT = "light";
public static final String DARK = "dark";
public static final String DEFAULT = Constants.Pref.Theme.LIGHT;
}
+
+ public static final class ProxyType {
+ public static final String TYPE_HTTP = "proxyHttp";
+ public static final String TYPE_SOCKS = "proxySocks";
+ }
}
/**
@@ -123,7 +136,7 @@ public final class Constants {
public static final class Defaults {
public static final String KEY_SERVERS = "hkps://hkps.pool.sks-keyservers.net, hkps://pgp.mit.edu";
- public static final int PREF_VERSION = 5;
+ public static final int PREF_VERSION = 6;
}
public static final class key {
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/KeychainApplication.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/KeychainApplication.java
index cd24394d7..311ef2d3b 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/KeychainApplication.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/KeychainApplication.java
@@ -23,7 +23,6 @@ import android.app.Application;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
-import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
@@ -35,7 +34,7 @@ import android.widget.Toast;
import org.spongycastle.jce.provider.BouncyCastleProvider;
import org.sufficientlysecure.keychain.provider.KeychainDatabase;
import org.sufficientlysecure.keychain.provider.TemporaryStorageProvider;
-import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.service.KeyserverSyncAdapterService;
import org.sufficientlysecure.keychain.ui.ConsolidateDialogActivity;
import org.sufficientlysecure.keychain.ui.util.FormattingUtils;
import org.sufficientlysecure.keychain.util.Log;
@@ -97,7 +96,7 @@ public class KeychainApplication extends Application {
setupAccountAsNeeded(this);
// Update keyserver list as needed
- Preferences.getPreferences(this).upgradePreferences();
+ Preferences.getPreferences(this).upgradePreferences(this);
TlsHelper.addStaticCA("pool.sks-keyservers.net", getAssets(), "sks-keyservers.netCA.cer");
@@ -136,17 +135,20 @@ public class KeychainApplication extends Application {
}
/**
- * Add OpenKeychain account to Android to link contacts with keys
+ * Add OpenKeychain account to Android to link contacts with keys and keyserver sync
*/
public static void setupAccountAsNeeded(Context context) {
try {
AccountManager manager = AccountManager.get(context);
Account[] accounts = manager.getAccountsByType(Constants.ACCOUNT_TYPE);
- if (accounts == null || accounts.length == 0) {
+
+ if (accounts.length == 0) {
Account account = new Account(Constants.ACCOUNT_NAME, Constants.ACCOUNT_TYPE);
if (manager.addAccountExplicitly(account, null, null)) {
+ // for contact sync
ContentResolver.setIsSyncable(account, ContactsContract.AUTHORITY, 1);
ContentResolver.setSyncAutomatically(account, ContactsContract.AUTHORITY, true);
+ KeyserverSyncAdapterService.enableKeyserverSync(context);
} else {
Log.e(Constants.TAG, "Adding account failed!");
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/HkpKeyserver.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/HkpKeyserver.java
index 2cf6d8b34..558b8ce7d 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/HkpKeyserver.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/HkpKeyserver.java
@@ -196,9 +196,9 @@ public class HkpKeyserver extends Keyserver {
/**
* returns a client with pinned certificate if necessary
*
- * @param url
- * @param proxy
- * @return
+ * @param url url to be queried by client
+ * @param proxy proxy to be used by client
+ * @return client with a pinned certificate if necesary
*/
public static OkHttpClient getClient(URL url, Proxy proxy) throws IOException {
OkHttpClient client = new OkHttpClient();
@@ -360,7 +360,7 @@ public class HkpKeyserver extends Keyserver {
try {
data = query(request, proxy);
} catch (HttpError httpError) {
- Log.e(Constants.TAG, "Failed to get key at HkpKeyserver", httpError);
+ Log.d(Constants.TAG, "Failed to get key at HkpKeyserver", httpError);
throw new QueryFailedException("not found");
}
Matcher matcher = PgpHelper.PGP_PUBLIC_KEY.matcher(data);
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/LinkedAttribute.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/LinkedAttribute.java
new file mode 100644
index 000000000..3b05afbb3
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/LinkedAttribute.java
@@ -0,0 +1,32 @@
+package org.sufficientlysecure.keychain.linked;
+
+import java.net.URI;
+
+import android.content.Context;
+import android.support.annotation.DrawableRes;
+
+public class LinkedAttribute extends UriAttribute {
+
+ public final LinkedResource mResource;
+
+ protected LinkedAttribute(URI uri, LinkedResource resource) {
+ super(uri);
+ if (resource == null) {
+ throw new AssertionError("resource must not be null in a LinkedIdentity!");
+ }
+ mResource = resource;
+ }
+
+ public @DrawableRes int getDisplayIcon() {
+ return mResource.getDisplayIcon();
+ }
+
+ public String getDisplayTitle(Context context) {
+ return mResource.getDisplayTitle(context);
+ }
+
+ public String getDisplayComment(Context context) {
+ return mResource.getDisplayComment(context);
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/LinkedResource.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/LinkedResource.java
new file mode 100644
index 000000000..dffeea65e
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/LinkedResource.java
@@ -0,0 +1,25 @@
+package org.sufficientlysecure.keychain.linked;
+
+import java.net.URI;
+
+import android.content.Context;
+import android.content.Intent;
+import android.support.annotation.DrawableRes;
+import android.support.annotation.StringRes;
+
+public abstract class LinkedResource {
+
+ public abstract URI toUri();
+
+ public abstract @DrawableRes int getDisplayIcon();
+ public abstract @StringRes int getVerifiedText(boolean isSecret);
+ public abstract String getDisplayTitle(Context context);
+ public abstract String getDisplayComment(Context context);
+ public boolean isViewable() {
+ return false;
+ }
+ public Intent getViewIntent() {
+ return null;
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/LinkedTokenResource.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/LinkedTokenResource.java
new file mode 100644
index 000000000..998ec3ad4
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/LinkedTokenResource.java
@@ -0,0 +1,300 @@
+package org.sufficientlysecure.keychain.linked;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpRequestBase;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.params.BasicHttpParams;
+import org.json.JSONException;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.linked.resources.DnsResource;
+import org.sufficientlysecure.keychain.linked.resources.GenericHttpsResource;
+import org.sufficientlysecure.keychain.linked.resources.GithubResource;
+import org.sufficientlysecure.keychain.linked.resources.TwitterResource;
+import org.sufficientlysecure.keychain.operations.results.LinkedVerifyResult;
+import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType;
+import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
+import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
+import org.sufficientlysecure.keychain.util.Log;
+import org.thoughtcrime.ssl.pinning.util.PinningHelper;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import android.content.Context;
+
+
+public abstract class LinkedTokenResource extends LinkedResource {
+
+ protected final URI mSubUri;
+ protected final Set<String> mFlags;
+ protected final HashMap<String,String> mParams;
+
+ public static Pattern magicPattern =
+ Pattern.compile("\\[Verifying my (?:Open|)?PGP key(?::|) openpgp4fpr:([a-zA-Z0-9]+)]");
+
+ protected LinkedTokenResource(Set<String> flags, HashMap<String, String> params, URI uri) {
+ mFlags = flags;
+ mParams = params;
+ mSubUri = uri;
+ }
+
+ @SuppressWarnings("unused")
+ public URI getSubUri () {
+ return mSubUri;
+ }
+
+ public Set<String> getFlags () {
+ return new HashSet<>(mFlags);
+ }
+
+ public HashMap<String,String> getParams () {
+ return new HashMap<>(mParams);
+ }
+
+ public static String generate (byte[] fingerprint) {
+ return String.format("[Verifying my OpenPGP key: openpgp4fpr:%s]",
+ KeyFormattingUtils.convertFingerprintToHex(fingerprint));
+ }
+
+ protected static LinkedTokenResource fromUri (URI uri) {
+
+ if (!"openpgpid+token".equals(uri.getScheme())
+ && !"openpgpid+cookie".equals(uri.getScheme())) {
+ Log.e(Constants.TAG, "unknown uri scheme in (suspected) linked id packet");
+ return null;
+ }
+
+ if (!uri.isOpaque()) {
+ Log.e(Constants.TAG, "non-opaque uri in (suspected) linked id packet");
+ return null;
+ }
+
+ String specific = uri.getSchemeSpecificPart();
+ if (!specific.contains("@")) {
+ Log.e(Constants.TAG, "unknown uri scheme in linked id packet");
+ return null;
+ }
+
+ String[] pieces = specific.split("@", 2);
+ URI subUri = URI.create(pieces[1]);
+
+ Set<String> flags = new HashSet<>();
+ HashMap<String,String> params = new HashMap<>();
+ if (!pieces[0].isEmpty()) {
+ String[] rawParams = pieces[0].split(";");
+ for (String param : rawParams) {
+ String[] p = param.split("=", 2);
+ if (p.length == 1) {
+ flags.add(param);
+ } else {
+ params.put(p[0], p[1]);
+ }
+ }
+ }
+
+ return findResourceType(flags, params, subUri);
+
+ }
+
+ protected static LinkedTokenResource findResourceType (Set<String> flags,
+ HashMap<String,String> params, URI subUri) {
+
+ LinkedTokenResource res;
+
+ res = GenericHttpsResource.create(flags, params, subUri);
+ if (res != null) {
+ return res;
+ }
+ // res = DnsResource.create(flags, params, subUri);
+ // if (res != null) {
+ // return res;
+ // }
+ res = TwitterResource.create(flags, params, subUri);
+ if (res != null) {
+ return res;
+ }
+ res = GithubResource.create(flags, params, subUri);
+ if (res != null) {
+ return res;
+ }
+
+ return null;
+
+ }
+
+ public URI toUri () {
+
+ StringBuilder b = new StringBuilder();
+ b.append("openpgpid+token:");
+
+ // add flags
+ if (mFlags != null) {
+ boolean first = true;
+ for (String flag : mFlags) {
+ if (!first) {
+ b.append(";");
+ }
+ first = false;
+ b.append(flag);
+ }
+ }
+
+ // add parameters
+ if (mParams != null) {
+ boolean first = true;
+ for (Entry<String, String> stringStringEntry : mParams.entrySet()) {
+ if (!first) {
+ b.append(";");
+ }
+ first = false;
+ b.append(stringStringEntry.getKey()).append("=").append(stringStringEntry.getValue());
+ }
+ }
+
+ b.append("@");
+ b.append(mSubUri);
+
+ return URI.create(b.toString());
+
+ }
+
+ public LinkedVerifyResult verify(Context context, byte[] fingerprint) {
+
+ OperationLog log = new OperationLog();
+ log.add(LogType.MSG_LV, 0);
+
+ // Try to fetch resource. Logs for itself
+ String res = null;
+ try {
+ res = fetchResource(context, log, 1);
+ } catch (HttpStatusException e) {
+ // log verbose output to logcat
+ Log.e(Constants.TAG, "http error (" + e.getStatus() + "): " + e.getReason());
+ log.add(LogType.MSG_LV_FETCH_ERROR, 2, Integer.toString(e.getStatus()));
+ } catch (MalformedURLException e) {
+ log.add(LogType.MSG_LV_FETCH_ERROR_URL, 2);
+ } catch (IOException e) {
+ Log.e(Constants.TAG, "io error", e);
+ log.add(LogType.MSG_LV_FETCH_ERROR_IO, 2);
+ } catch (JSONException e) {
+ Log.e(Constants.TAG, "json error", e);
+ log.add(LogType.MSG_LV_FETCH_ERROR_FORMAT, 2);
+ }
+
+ if (res == null) {
+ // if this is null, an error was recorded in fetchResource above
+ return new LinkedVerifyResult(LinkedVerifyResult.RESULT_ERROR, log);
+ }
+
+ Log.d(Constants.TAG, "Resource data: '" + res + "'");
+
+ return verifyString(log, 1, res, fingerprint);
+
+ }
+
+ protected abstract String fetchResource (Context context, OperationLog log, int indent)
+ throws HttpStatusException, IOException, JSONException;
+
+ protected Matcher matchResource (OperationLog log, int indent, String res) {
+ return magicPattern.matcher(res);
+ }
+
+ protected LinkedVerifyResult verifyString (OperationLog log, int indent,
+ String res,
+ byte[] fingerprint) {
+
+ log.add(LogType.MSG_LV_MATCH, indent);
+ Matcher match = matchResource(log, indent+1, res);
+ if (!match.find()) {
+ log.add(LogType.MSG_LV_MATCH_ERROR, 2);
+ return new LinkedVerifyResult(LinkedVerifyResult.RESULT_ERROR, log);
+ }
+
+ String candidateFp = match.group(1).toLowerCase();
+ String fp = KeyFormattingUtils.convertFingerprintToHex(fingerprint);
+ if (!fp.equals(candidateFp)) {
+ log.add(LogType.MSG_LV_FP_ERROR, indent);
+ return new LinkedVerifyResult(LinkedVerifyResult.RESULT_ERROR, log);
+ }
+ log.add(LogType.MSG_LV_FP_OK, indent);
+
+ return new LinkedVerifyResult(LinkedVerifyResult.RESULT_OK, log);
+
+ }
+
+ @SuppressWarnings("deprecation") // HttpRequestBase is deprecated
+ public static String getResponseBody(Context context, HttpRequestBase request)
+ throws IOException, HttpStatusException {
+ return getResponseBody(context, request, null);
+ }
+
+ @SuppressWarnings("deprecation") // HttpRequestBase is deprecated
+ public static String getResponseBody(Context context, HttpRequestBase request, String[] pins)
+ throws IOException, HttpStatusException {
+ StringBuilder sb = new StringBuilder();
+
+ request.setHeader("User-Agent", "Open Keychain");
+
+
+ HttpClient httpClient;
+ if (pins == null) {
+ httpClient = new DefaultHttpClient(new BasicHttpParams());
+ } else {
+ httpClient = PinningHelper.getPinnedHttpClient(context, pins);
+ }
+
+ HttpResponse response = httpClient.execute(request);
+ int statusCode = response.getStatusLine().getStatusCode();
+ String reason = response.getStatusLine().getReasonPhrase();
+
+ if (statusCode != 200) {
+ throw new HttpStatusException(statusCode, reason);
+ }
+
+ HttpEntity entity = response.getEntity();
+ InputStream inputStream = entity.getContent();
+
+ BufferedReader bReader = new BufferedReader(
+ new InputStreamReader(inputStream, "UTF-8"), 8);
+ String line;
+ while ((line = bReader.readLine()) != null) {
+ sb.append(line);
+ }
+
+ return sb.toString();
+ }
+
+ public static class HttpStatusException extends Throwable {
+
+ private final int mStatusCode;
+ private final String mReason;
+
+ HttpStatusException(int statusCode, String reason) {
+ super("http status " + statusCode + ": " + reason);
+ mStatusCode = statusCode;
+ mReason = reason;
+ }
+
+ public int getStatus() {
+ return mStatusCode;
+ }
+
+ public String getReason() {
+ return mReason;
+ }
+
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/UriAttribute.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/UriAttribute.java
new file mode 100644
index 000000000..7a8ece2cb
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/UriAttribute.java
@@ -0,0 +1,79 @@
+package org.sufficientlysecure.keychain.linked;
+
+import org.spongycastle.util.Strings;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.pgp.WrappedUserAttribute;
+import org.sufficientlysecure.keychain.util.Log;
+
+import java.io.IOException;
+import java.net.URI;
+
+import android.content.Context;
+import android.support.annotation.DrawableRes;
+
+/** The RawLinkedIdentity contains raw parsed data from a Linked Identity subpacket. */
+public class UriAttribute {
+
+ public final URI mUri;
+
+ protected UriAttribute(URI uri) {
+ mUri = uri;
+ }
+
+ public byte[] getEncoded() {
+ return Strings.toUTF8ByteArray(mUri.toASCIIString());
+ }
+
+ public static UriAttribute fromAttributeData(byte[] data) throws IOException {
+ WrappedUserAttribute att = WrappedUserAttribute.fromData(data);
+
+ byte[][] subpackets = att.getSubpackets();
+ if (subpackets.length >= 1) {
+ return fromSubpacketData(subpackets[0]);
+ }
+
+ throw new IOException("no subpacket data");
+ }
+
+ static UriAttribute fromSubpacketData(byte[] data) {
+
+ try {
+ String uriStr = Strings.fromUTF8ByteArray(data);
+ URI uri = URI.create(uriStr);
+
+ LinkedResource res = LinkedTokenResource.fromUri(uri);
+ if (res == null) {
+ return new UriAttribute(uri);
+ }
+
+ return new LinkedAttribute(uri, res);
+
+ } catch (IllegalArgumentException e) {
+ Log.e(Constants.TAG, "error parsing uri in (suspected) linked id packet");
+ return null;
+ }
+ }
+
+ public static UriAttribute fromResource (LinkedTokenResource res) {
+ return new UriAttribute(res.toUri());
+ }
+
+
+ public WrappedUserAttribute toUserAttribute () {
+ return WrappedUserAttribute.fromSubpacket(WrappedUserAttribute.UAT_URI_ATTRIBUTE, getEncoded());
+ }
+
+ public @DrawableRes int getDisplayIcon() {
+ return R.drawable.ic_warning_grey_24dp;
+ }
+
+ public String getDisplayTitle(Context context) {
+ return "Unknown Identity";
+ }
+
+ public String getDisplayComment(Context context) {
+ return null;
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/resources/DnsResource.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/resources/DnsResource.java
new file mode 100644
index 000000000..86b672cc1
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/resources/DnsResource.java
@@ -0,0 +1,130 @@
+package org.sufficientlysecure.keychain.linked.resources;
+
+import android.content.Context;
+import android.support.annotation.DrawableRes;
+import android.support.annotation.StringRes;
+
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
+import org.sufficientlysecure.keychain.linked.LinkedTokenResource;
+import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
+
+import java.net.URI;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import de.measite.minidns.Client;
+import de.measite.minidns.DNSMessage;
+import de.measite.minidns.Question;
+import de.measite.minidns.Record;
+import de.measite.minidns.Record.CLASS;
+import de.measite.minidns.Record.TYPE;
+import de.measite.minidns.record.TXT;
+
+public class DnsResource extends LinkedTokenResource {
+
+ final static Pattern magicPattern =
+ Pattern.compile("openpgpid\\+token=([a-zA-Z0-9]+)(?:#|;)([a-zA-Z0-9]+)");
+
+ String mFqdn;
+ CLASS mClass;
+ TYPE mType;
+
+ DnsResource(Set<String> flags, HashMap<String, String> params, URI uri,
+ String fqdn, CLASS clazz, TYPE type) {
+ super(flags, params, uri);
+
+ mFqdn = fqdn;
+ mClass = clazz;
+ mType = type;
+ }
+
+ public static String generateText(byte[] fingerprint) {
+
+ return String.format("openpgp4fpr=%s",
+ KeyFormattingUtils.convertFingerprintToHex(fingerprint));
+
+ }
+
+ public static DnsResource createNew (String domain) {
+ HashSet<String> flags = new HashSet<>();
+ HashMap<String,String> params = new HashMap<>();
+ URI uri = URI.create("dns:" + domain + "?TYPE=TXT");
+ return create(flags, params, uri);
+ }
+
+ public static DnsResource create(Set<String> flags, HashMap<String,String> params, URI uri) {
+ if ( ! ("dns".equals(uri.getScheme())
+ && (flags == null || flags.isEmpty())
+ && (params == null || params.isEmpty()))) {
+ return null;
+ }
+
+ //
+ String spec = uri.getSchemeSpecificPart();
+ // If there are // at the beginning, this includes an authority - we don't support those!
+ if (spec.startsWith("//")) {
+ return null;
+ }
+
+ String[] pieces = spec.split("\\?", 2);
+ // In either case, part before a ? is the fqdn
+ String fqdn = pieces[0];
+ // There may be a query part
+ /*
+ if (pieces.length > 1) {
+ // parse CLASS and TYPE query paramters
+ }
+ */
+
+ CLASS clazz = CLASS.IN;
+ TYPE type = TYPE.TXT;
+
+ return new DnsResource(flags, params, uri, fqdn, clazz, type);
+ }
+
+ @SuppressWarnings("unused")
+ public String getFqdn() {
+ return mFqdn;
+ }
+
+ @Override
+ protected String fetchResource (Context context, OperationLog log, int indent) {
+
+ Client c = new Client();
+ DNSMessage msg = c.query(new Question(mFqdn, mType, mClass));
+ Record aw = msg.getAnswers()[0];
+ TXT txt = (TXT) aw.getPayload();
+ return txt.getText().toLowerCase();
+
+ }
+
+ @Override
+ protected Matcher matchResource(OperationLog log, int indent, String res) {
+ return magicPattern.matcher(res);
+ }
+
+ @Override
+ public @StringRes
+ int getVerifiedText(boolean isSecret) {
+ return isSecret ? R.string.linked_verified_secret_dns : R.string.linked_verified_dns;
+ }
+
+ @Override
+ public @DrawableRes int getDisplayIcon() {
+ return R.drawable.linked_dns;
+ }
+
+ @Override
+ public String getDisplayTitle(Context context) {
+ return context.getString(R.string.linked_title_dns);
+ }
+
+ @Override
+ public String getDisplayComment(Context context) {
+ return mFqdn;
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/resources/GenericHttpsResource.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/resources/GenericHttpsResource.java
new file mode 100644
index 000000000..82240c405
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/resources/GenericHttpsResource.java
@@ -0,0 +1,95 @@
+package org.sufficientlysecure.keychain.linked.resources;
+
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.support.annotation.DrawableRes;
+import android.support.annotation.StringRes;
+
+import org.apache.http.client.methods.HttpGet;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType;
+import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
+import org.sufficientlysecure.keychain.linked.LinkedTokenResource;
+import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
+
+import java.io.IOException;
+import java.net.URI;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Set;
+
+public class GenericHttpsResource extends LinkedTokenResource {
+
+ GenericHttpsResource(Set<String> flags, HashMap<String,String> params, URI uri) {
+ super(flags, params, uri);
+ }
+
+ public static String generateText (Context context, byte[] fingerprint) {
+ String token = LinkedTokenResource.generate(fingerprint);
+
+ return String.format(context.getResources().getString(R.string.linked_id_generic_text),
+ token, "0x" + KeyFormattingUtils.convertFingerprintToHex(fingerprint).substring(24));
+ }
+
+ @SuppressWarnings("deprecation") // HttpGet is deprecated
+ @Override
+ protected String fetchResource (Context context, OperationLog log, int indent)
+ throws HttpStatusException, IOException {
+
+ log.add(LogType.MSG_LV_FETCH, indent, mSubUri.toString());
+ HttpGet httpGet = new HttpGet(mSubUri);
+ return getResponseBody(context, httpGet);
+
+ }
+
+ public static GenericHttpsResource createNew (URI uri) {
+ HashSet<String> flags = new HashSet<>();
+ flags.add("generic");
+ HashMap<String,String> params = new HashMap<>();
+ return create(flags, params, uri);
+ }
+
+ public static GenericHttpsResource create(Set<String> flags, HashMap<String,String> params, URI uri) {
+ if ( ! ("https".equals(uri.getScheme())
+ && flags != null && flags.size() == 1 && flags.contains("generic")
+ && (params == null || params.isEmpty()))) {
+ return null;
+ }
+ return new GenericHttpsResource(flags, params, uri);
+ }
+
+ @Override
+ public @DrawableRes
+ int getDisplayIcon() {
+ return R.drawable.linked_https;
+ }
+
+ @Override
+ public @StringRes
+ int getVerifiedText(boolean isSecret) {
+ return isSecret ? R.string.linked_verified_secret_https : R.string.linked_verified_https;
+ }
+
+ @Override
+ public String getDisplayTitle(Context context) {
+ return context.getString(R.string.linked_title_https);
+ }
+
+ @Override
+ public String getDisplayComment(Context context) {
+ return mSubUri.toString();
+ }
+
+ @Override
+ public boolean isViewable() {
+ return true;
+ }
+
+ @Override
+ public Intent getViewIntent() {
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setData(Uri.parse(mSubUri.toString()));
+ return intent;
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/resources/GithubResource.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/resources/GithubResource.java
new file mode 100644
index 000000000..7a97ffd96
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/resources/GithubResource.java
@@ -0,0 +1,218 @@
+package org.sufficientlysecure.keychain.linked.resources;
+
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.support.annotation.DrawableRes;
+import android.support.annotation.StringRes;
+
+import org.apache.http.client.methods.HttpGet;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType;
+import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
+import org.sufficientlysecure.keychain.linked.LinkedTokenResource;
+import org.sufficientlysecure.keychain.util.Log;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+
+public class GithubResource extends LinkedTokenResource {
+
+ final String mHandle;
+ final String mGistId;
+
+ GithubResource(Set<String> flags, HashMap<String,String> params, URI uri,
+ String handle, String gistId) {
+ super(flags, params, uri);
+
+ mHandle = handle;
+ mGistId = gistId;
+ }
+
+ public static String generate(Context context, byte[] fingerprint) {
+ String token = LinkedTokenResource.generate(fingerprint);
+
+ return String.format(context.getResources().getString(R.string.linked_id_github_text), token);
+ }
+
+ @SuppressWarnings("deprecation") // HttpGet is deprecated
+ @Override
+ protected String fetchResource (Context context, OperationLog log, int indent)
+ throws HttpStatusException, IOException, JSONException {
+
+ log.add(LogType.MSG_LV_FETCH, indent, mSubUri.toString());
+ indent += 1;
+
+ HttpGet httpGet = new HttpGet("https://api.github.com/gists/" + mGistId);
+ String response = getResponseBody(context, httpGet);
+
+ JSONObject obj = new JSONObject(response);
+
+ JSONObject owner = obj.getJSONObject("owner");
+ if (!mHandle.equals(owner.getString("login"))) {
+ log.add(LogType.MSG_LV_ERROR_GITHUB_HANDLE, indent);
+ return null;
+ }
+
+ JSONObject files = obj.getJSONObject("files");
+ Iterator<String> it = files.keys();
+ if (it.hasNext()) {
+ // TODO can there be multiple candidates?
+ JSONObject file = files.getJSONObject(it.next());
+ return file.getString("content");
+ }
+
+ log.add(LogType.MSG_LV_ERROR_GITHUB_NOT_FOUND, indent);
+ return null;
+
+ }
+
+ @Deprecated // not used for now, but could be used to pick up earlier posted gist if already present?
+ @SuppressWarnings({ "deprecation", "unused" })
+ public static GithubResource searchInGithubStream(
+ Context context, String screenName, String needle, OperationLog log) {
+
+ // narrow the needle down to important part
+ Matcher matcher = magicPattern.matcher(needle);
+ if (!matcher.find()) {
+ throw new AssertionError("Needle must contain token pattern! This is a programming error, please report.");
+ }
+ needle = matcher.group();
+
+ try {
+
+ JSONArray array; {
+ HttpGet httpGet =
+ new HttpGet("https://api.github.com/users/" + screenName + "/gists");
+ httpGet.setHeader("Content-Type", "application/json");
+ httpGet.setHeader("User-Agent", "OpenKeychain");
+
+ String response = getResponseBody(context, httpGet);
+ array = new JSONArray(response);
+ }
+
+ for (int i = 0, j = Math.min(array.length(), 5); i < j; i++) {
+ JSONObject obj = array.getJSONObject(i);
+
+ JSONObject files = obj.getJSONObject("files");
+ Iterator<String> it = files.keys();
+ if (it.hasNext()) {
+
+ JSONObject file = files.getJSONObject(it.next());
+ String type = file.getString("type");
+ if (!"text/plain".equals(type)) {
+ continue;
+ }
+ String id = obj.getString("id");
+ HttpGet httpGet = new HttpGet("https://api.github.com/gists/" + id);
+ httpGet.setHeader("User-Agent", "OpenKeychain");
+
+ JSONObject gistObj = new JSONObject(getResponseBody(context, httpGet));
+ JSONObject gistFiles = gistObj.getJSONObject("files");
+ Iterator<String> gistIt = gistFiles.keys();
+ if (!gistIt.hasNext()) {
+ continue;
+ }
+ // TODO can there be multiple candidates?
+ JSONObject gistFile = gistFiles.getJSONObject(gistIt.next());
+ String content = gistFile.getString("content");
+ if (!content.contains(needle)) {
+ continue;
+ }
+
+ URI uri = URI.create("https://gist.github.com/" + screenName + "/" + id);
+ return create(uri);
+ }
+ }
+
+ // update the results with the body of the response
+ log.add(LogType.MSG_LV_FETCH_ERROR_NOTHING, 2);
+ return null;
+
+ } catch (HttpStatusException e) {
+ // log verbose output to logcat
+ Log.e(Constants.TAG, "http error (" + e.getStatus() + "): " + e.getReason());
+ log.add(LogType.MSG_LV_FETCH_ERROR, 2, Integer.toString(e.getStatus()));
+ } catch (MalformedURLException e) {
+ log.add(LogType.MSG_LV_FETCH_ERROR_URL, 2);
+ } catch (IOException e) {
+ Log.e(Constants.TAG, "io error", e);
+ log.add(LogType.MSG_LV_FETCH_ERROR_IO, 2);
+ } catch (JSONException e) {
+ Log.e(Constants.TAG, "json error", e);
+ log.add(LogType.MSG_LV_FETCH_ERROR_FORMAT, 2);
+ }
+
+ return null;
+ }
+
+ public static GithubResource create(URI uri) {
+ return create(new HashSet<String>(), new HashMap<String,String>(), uri);
+ }
+
+ public static GithubResource create(Set<String> flags, HashMap<String,String> params, URI uri) {
+
+ // no params or flags
+ if (!flags.isEmpty() || !params.isEmpty()) {
+ return null;
+ }
+
+ Pattern p = Pattern.compile("https://gist\\.github\\.com/([a-zA-Z0-9_]+)/([0-9a-f]+)");
+ Matcher match = p.matcher(uri.toString());
+ if (!match.matches()) {
+ return null;
+ }
+ String handle = match.group(1);
+ String gistId = match.group(2);
+
+ return new GithubResource(flags, params, uri, handle, gistId);
+
+ }
+
+
+ @Override
+ public @DrawableRes
+ int getDisplayIcon() {
+ return R.drawable.linked_github;
+ }
+
+ @Override
+ public @StringRes
+ int getVerifiedText(boolean isSecret) {
+ return isSecret ? R.string.linked_verified_secret_github : R.string.linked_verified_github;
+ }
+
+ @Override
+ public String getDisplayTitle(Context context) {
+ return context.getString(R.string.linked_title_github);
+ }
+
+ @Override
+ public String getDisplayComment(Context context) {
+ return mHandle;
+ }
+
+ @Override
+ public boolean isViewable() {
+ return true;
+ }
+
+ @Override
+ public Intent getViewIntent() {
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setData(Uri.parse(mSubUri.toString()));
+ return intent;
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/resources/TwitterResource.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/resources/TwitterResource.java
new file mode 100644
index 000000000..73e3d3643
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/resources/TwitterResource.java
@@ -0,0 +1,250 @@
+package org.sufficientlysecure.keychain.linked.resources;
+
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.support.annotation.DrawableRes;
+import android.support.annotation.StringRes;
+import android.util.Log;
+
+import com.textuality.keybase.lib.JWalk;
+
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.entity.StringEntity;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType;
+import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
+import org.sufficientlysecure.keychain.linked.LinkedTokenResource;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class TwitterResource extends LinkedTokenResource {
+
+ public static final String[] CERT_PINS = null; /*(new String[] {
+ // Symantec Class 3 Secure Server CA - G4
+ "513fb9743870b73440418d30930699ff"
+ };*/
+
+ final String mHandle;
+ final String mTweetId;
+
+ TwitterResource(Set<String> flags, HashMap<String,String> params,
+ URI uri, String handle, String tweetId) {
+ super(flags, params, uri);
+
+ mHandle = handle;
+ mTweetId = tweetId;
+ }
+
+ public static TwitterResource create(URI uri) {
+ return create(new HashSet<String>(), new HashMap<String,String>(), uri);
+ }
+
+ public static TwitterResource create(Set<String> flags, HashMap<String,String> params, URI uri) {
+
+ // no params or flags
+ if (!flags.isEmpty() || !params.isEmpty()) {
+ return null;
+ }
+
+ Pattern p = Pattern.compile("https://twitter\\.com/([a-zA-Z0-9_]+)/status/([0-9]+)");
+ Matcher match = p.matcher(uri.toString());
+ if (!match.matches()) {
+ return null;
+ }
+ String handle = match.group(1);
+ String tweetId = match.group(2);
+
+ return new TwitterResource(flags, params, uri, handle, tweetId);
+
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ protected String fetchResource(Context context, OperationLog log, int indent)
+ throws IOException, HttpStatusException, JSONException {
+
+ String authToken;
+ try {
+ authToken = getAuthToken(context);
+ } catch (IOException | HttpStatusException | JSONException e) {
+ log.add(LogType.MSG_LV_ERROR_TWITTER_AUTH, indent);
+ return null;
+ }
+
+ HttpGet httpGet =
+ new HttpGet("https://api.twitter.com/1.1/statuses/show.json"
+ + "?id=" + mTweetId
+ + "&include_entities=false");
+
+ // construct a normal HTTPS request and include an Authorization
+ // header with the value of Bearer <>
+ httpGet.setHeader("Authorization", "Bearer " + authToken);
+ httpGet.setHeader("Content-Type", "application/json");
+
+ try {
+ String response = getResponseBody(context, httpGet, CERT_PINS);
+ JSONObject obj = new JSONObject(response);
+ JSONObject user = obj.getJSONObject("user");
+ if (!mHandle.equalsIgnoreCase(user.getString("screen_name"))) {
+ log.add(LogType.MSG_LV_ERROR_TWITTER_HANDLE, indent);
+ return null;
+ }
+
+ // update the results with the body of the response
+ return obj.getString("text");
+ } catch (JSONException e) {
+ log.add(LogType.MSG_LV_ERROR_TWITTER_RESPONSE, indent);
+ return null;
+ }
+
+ }
+
+ @Override
+ public @DrawableRes int getDisplayIcon() {
+ return R.drawable.linked_twitter;
+ }
+
+ @Override
+ public @StringRes
+ int getVerifiedText(boolean isSecret) {
+ return isSecret ? R.string.linked_verified_secret_twitter : R.string.linked_verified_twitter;
+ }
+
+ @Override
+ public String getDisplayTitle(Context context) {
+ return context.getString(R.string.linked_title_twitter);
+ }
+
+ @Override
+ public String getDisplayComment(Context context) {
+ return "@" + mHandle;
+ }
+
+ @Override
+ public boolean isViewable() {
+ return true;
+ }
+
+ @Override
+ public Intent getViewIntent() {
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setData(Uri.parse(mSubUri.toString()));
+ return intent;
+ }
+
+ @SuppressWarnings("deprecation")
+ public static TwitterResource searchInTwitterStream(
+ Context context, String screenName, String needle, OperationLog log) {
+
+ String authToken;
+ try {
+ authToken = getAuthToken(context);
+ } catch (IOException | HttpStatusException | JSONException e) {
+ log.add(LogType.MSG_LV_ERROR_TWITTER_AUTH, 1);
+ return null;
+ }
+
+ HttpGet httpGet =
+ new HttpGet("https://api.twitter.com/1.1/statuses/user_timeline.json"
+ + "?screen_name=" + screenName
+ + "&count=15"
+ + "&include_rts=false"
+ + "&trim_user=true"
+ + "&exclude_replies=true");
+
+ // construct a normal HTTPS request and include an Authorization
+ // header with the value of Bearer <>
+ httpGet.setHeader("Authorization", "Bearer " + authToken);
+ httpGet.setHeader("Content-Type", "application/json");
+
+ try {
+ String response = getResponseBody(context, httpGet, CERT_PINS);
+ JSONArray array = new JSONArray(response);
+
+ for (int i = 0; i < array.length(); i++) {
+ JSONObject obj = array.getJSONObject(i);
+ String tweet = obj.getString("text");
+ if (tweet.contains(needle)) {
+ String id = obj.getString("id_str");
+ URI uri = URI.create("https://twitter.com/" + screenName + "/status/" + id);
+ return create(uri);
+ }
+ }
+
+ // update the results with the body of the response
+ log.add(LogType.MSG_LV_FETCH_ERROR_NOTHING, 1);
+ return null;
+
+ } catch (HttpStatusException e) {
+ // log verbose output to logcat
+ Log.e(Constants.TAG, "http error (" + e.getStatus() + "): " + e.getReason());
+ log.add(LogType.MSG_LV_FETCH_ERROR, 1, Integer.toString(e.getStatus()));
+ } catch (MalformedURLException e) {
+ log.add(LogType.MSG_LV_FETCH_ERROR_URL, 1);
+ } catch (IOException e) {
+ Log.e(Constants.TAG, "io error", e);
+ log.add(LogType.MSG_LV_FETCH_ERROR_IO, 1);
+ } catch (JSONException e) {
+ Log.e(Constants.TAG, "json error", e);
+ log.add(LogType.MSG_LV_FETCH_ERROR_FORMAT, 1);
+ }
+
+ return null;
+ }
+
+ private static String cachedAuthToken;
+
+ @SuppressWarnings("deprecation")
+ private static String getAuthToken(Context context)
+ throws IOException, HttpStatusException, JSONException {
+ if (cachedAuthToken != null) {
+ return cachedAuthToken;
+ }
+ String base64Encoded = rot13("D293FQqanH0jH29KIaWJER5DomqSGRE2Ewc1LJACn3cbD1c"
+ + "Fq1bmqSAQAz5MI2cIHKOuo3cPoRAQI1OyqmIVFJS6LHMXq2g6MRLkIj") + "==";
+
+ // Step 2: Obtain a bearer token
+ HttpPost httpPost = new HttpPost("https://api.twitter.com/oauth2/token");
+ httpPost.setHeader("Authorization", "Basic " + base64Encoded);
+ httpPost.setHeader("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8");
+ httpPost.setEntity(new StringEntity("grant_type=client_credentials"));
+ JSONObject rawAuthorization = new JSONObject(getResponseBody(context, httpPost, CERT_PINS));
+
+ // Applications should verify that the value associated with the
+ // token_type key of the returned object is bearer
+ if (!"bearer".equals(JWalk.getString(rawAuthorization, "token_type"))) {
+ throw new JSONException("Expected bearer token in response!");
+ }
+
+ cachedAuthToken = rawAuthorization.getString("access_token");
+ return cachedAuthToken;
+
+ }
+
+ public static String rot13(String input) {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < input.length(); i++) {
+ char c = input.charAt(i);
+ if (c >= 'a' && c <= 'm') c += 13;
+ else if (c >= 'A' && c <= 'M') c += 13;
+ else if (c >= 'n' && c <= 'z') c -= 13;
+ else if (c >= 'N' && c <= 'Z') c -= 13;
+ sb.append(c);
+ }
+ return sb.toString();
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ExportOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ExportOperation.java
index a5b70a41f..531ac01f2 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ExportOperation.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ExportOperation.java
@@ -145,6 +145,8 @@ public class ExportOperation extends BaseOperation<ExportKeyringParcel> {
return new ExportResult(ExportResult.RESULT_ERROR, log);
}
+ log.add(LogType.MSG_EXPORT_FILE_NAME, 1, outputFile);
+
// check if storage is ready
if (!FileHelper.isStorageMounted(outputFile)) {
log.add(LogType.MSG_EXPORT_ERROR_STORAGE, 1);
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 4acfd6e30..89575338f 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ImportOperation.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ImportOperation.java
@@ -22,9 +22,8 @@ package org.sufficientlysecure.keychain.operations;
import java.io.IOException;
import java.net.Proxy;
import java.util.ArrayList;
-import java.util.HashSet;
+import java.util.GregorianCalendar;
import java.util.Iterator;
-import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorCompletionService;
@@ -99,29 +98,9 @@ public class ImportOperation extends BaseOperation<ImportKeyringParcel> {
return serialKeyRingImport(entries, num, keyServerUri, mProgressable, proxy);
}
- public ImportKeyResult serialKeyRingImport(List<ParcelableKeyRing> entries,
- String keyServerUri, Proxy proxy) {
-
- Iterator<ParcelableKeyRing> it = entries.iterator();
- int numEntries = entries.size();
-
- return serialKeyRingImport(it, numEntries, keyServerUri, mProgressable, proxy);
-
- }
-
- public ImportKeyResult serialKeyRingImport(List<ParcelableKeyRing> entries, String keyServerUri,
- Progressable progressable, Proxy proxy) {
-
- Iterator<ParcelableKeyRing> it = entries.iterator();
- int numEntries = entries.size();
-
- return serialKeyRingImport(it, numEntries, keyServerUri, progressable, proxy);
-
- }
-
@NonNull
- public ImportKeyResult serialKeyRingImport(ParcelableFileCache<ParcelableKeyRing> cache,
- String keyServerUri, Proxy proxy) {
+ private ImportKeyResult serialKeyRingImport(ParcelableFileCache<ParcelableKeyRing> cache,
+ String keyServerUri, Proxy proxy) {
// get entries from cached file
try {
@@ -143,7 +122,7 @@ public class ImportOperation extends BaseOperation<ImportKeyringParcel> {
/**
* Since the introduction of multithreaded import, we expect calling functions to handle the
- * key sync i,eContactSyncAdapterService.requestSync()
+ * contact-to-key sync i.e ContactSyncAdapterService.requestSync()
*
* @param entries keys to import
* @param num number of keys to import
@@ -152,9 +131,9 @@ public class ImportOperation extends BaseOperation<ImportKeyringParcel> {
* progress of a single key being imported
*/
@NonNull
- public ImportKeyResult serialKeyRingImport(Iterator<ParcelableKeyRing> entries, int num,
- String keyServerUri, Progressable progressable,
- Proxy proxy) {
+ private ImportKeyResult serialKeyRingImport(Iterator<ParcelableKeyRing> entries, int num,
+ String keyServerUri, Progressable progressable,
+ Proxy proxy) {
if (progressable != null) {
progressable.setProgress(R.string.progress_importing, 0, 100);
}
@@ -231,8 +210,8 @@ public class ImportOperation extends BaseOperation<ImportKeyringParcel> {
log.add(LogType.MSG_IMPORT_FETCH_ERROR_DECODE, 3);
}
} catch (Keyserver.QueryFailedException e) {
- Log.e(Constants.TAG, "query failed", e);
- log.add(LogType.MSG_IMPORT_FETCH_KEYSERVER_ERROR, 3, e.getMessage());
+ Log.d(Constants.TAG, "query failed", e);
+ log.add(LogType.MSG_IMPORT_FETCH_ERROR_KEYSERVER, 3, e.getMessage());
}
}
@@ -264,7 +243,7 @@ public class ImportOperation extends BaseOperation<ImportKeyringParcel> {
} catch (Keyserver.QueryFailedException e) {
// download failed, too bad. just proceed
Log.e(Constants.TAG, "query failed", e);
- log.add(LogType.MSG_IMPORT_FETCH_KEYSERVER_ERROR, 3, e.getMessage());
+ log.add(LogType.MSG_IMPORT_FETCH_ERROR_KEYSERVER, 3, e.getMessage());
}
}
}
@@ -275,15 +254,11 @@ public class ImportOperation extends BaseOperation<ImportKeyringParcel> {
continue;
}
- // If we have an expected fingerprint, make sure it matches
- if (entry.mExpectedFingerprint != null) {
- if (!key.containsSubkey(entry.mExpectedFingerprint)) {
- log.add(LogType.MSG_IMPORT_FINGERPRINT_ERROR, 2);
- badKeys += 1;
- continue;
- } else {
- log.add(LogType.MSG_IMPORT_FINGERPRINT_OK, 2);
- }
+ // never import secret keys from keyserver!
+ if (entry.mBytes == null && key.isSecret()) {
+ log.add(LogType.MSG_IMPORT_FETCH_ERROR_KEYSERVER_SECRET, 2);
+ badKeys += 1;
+ continue;
}
// Another check if we have been cancelled
@@ -293,31 +268,44 @@ public class ImportOperation extends BaseOperation<ImportKeyringParcel> {
}
SaveKeyringResult result;
- mProviderHelper.clearLog();
- if (key.isSecret()) {
- result = mProviderHelper.saveSecretKeyRing(key,
- new ProgressScaler(progressable, (int) (position * progSteps),
- (int) ((position + 1) * progSteps), 100));
- } else {
- result = mProviderHelper.savePublicKeyRing(key,
- new ProgressScaler(progressable, (int) (position * progSteps),
- (int) ((position + 1) * progSteps), 100));
+ // synchronizing prevents https://github.com/open-keychain/open-keychain/issues/1221
+ // and https://github.com/open-keychain/open-keychain/issues/1480
+ synchronized (mProviderHelper) {
+ mProviderHelper.clearLog();
+ if (key.isSecret()) {
+ result = mProviderHelper.saveSecretKeyRing(key,
+ new ProgressScaler(progressable, (int) (position * progSteps),
+ (int) ((position + 1) * progSteps), 100));
+ } else {
+ result = mProviderHelper.savePublicKeyRing(key,
+ new ProgressScaler(progressable, (int) (position * progSteps),
+ (int) ((position + 1) * progSteps), 100), entry.mExpectedFingerprint);
+ }
}
if (!result.success()) {
badKeys += 1;
- } else if (result.updated()) {
- updatedKeys += 1;
- importedMasterKeyIds.add(key.getMasterKeyId());
} else {
- newKeys += 1;
- if (key.isSecret()) {
- secret += 1;
+ if (result.updated()) {
+ updatedKeys += 1;
+ importedMasterKeyIds.add(key.getMasterKeyId());
+ } else {
+ newKeys += 1;
+ if (key.isSecret()) {
+ secret += 1;
+ }
+ importedMasterKeyIds.add(key.getMasterKeyId());
+ }
+ if (entry.mBytes == null) {
+ // synonymous to isDownloadFromKeyserver.
+ // If no byte data was supplied, import from keyserver took place
+ // this prevents file imports being noted as keyserver imports
+ mProviderHelper.renewKeyLastUpdatedTime(key.getMasterKeyId(),
+ GregorianCalendar.getInstance().getTimeInMillis(),
+ TimeUnit.MILLISECONDS);
}
- importedMasterKeyIds.add(key.getMasterKeyId());
}
log.add(result, 2);
-
} catch (IOException | PgpGeneralException e) {
Log.e(Constants.TAG, "Encountered bad key on import!", e);
++badKeys;
@@ -327,9 +315,15 @@ public class ImportOperation extends BaseOperation<ImportKeyringParcel> {
}
// Special: consolidate on secret key import (cannot be cancelled!)
+ // synchronized on mProviderHelper to prevent
+ // https://github.com/open-keychain/open-keychain/issues/1221 since a consolidate deletes
+ // and re-inserts keys, which could conflict with a parallel db key update
if (secret > 0) {
setPreventCancel();
- ConsolidateResult result = mProviderHelper.consolidateDatabaseStep1(progressable);
+ ConsolidateResult result;
+ synchronized (mProviderHelper) {
+ result = mProviderHelper.consolidateDatabaseStep1(progressable);
+ }
log.add(result, 1);
}
@@ -386,7 +380,7 @@ public class ImportOperation extends BaseOperation<ImportKeyringParcel> {
@NonNull
@Override
- public OperationResult execute(ImportKeyringParcel importInput, CryptoInputParcel cryptoInput) {
+ public ImportKeyResult execute(ImportKeyringParcel importInput, CryptoInputParcel cryptoInput) {
ArrayList<ParcelableKeyRing> keyList = importInput.mKeyList;
String keyServer = importInput.mKeyserver;
@@ -411,20 +405,8 @@ public class ImportOperation extends BaseOperation<ImportKeyringParcel> {
} else {
proxy = cryptoInput.getParcelableProxy().getProxy();
}
- // if there is more than one key with the same fingerprint, we do a serial import to
- // prevent
- // https://github.com/open-keychain/open-keychain/issues/1221
- HashSet<String> keyFingerprintSet = new HashSet<>();
- for (int i = 0; i < keyList.size(); i++) {
- keyFingerprintSet.add(keyList.get(i).mExpectedFingerprint);
- }
- if (keyFingerprintSet.size() == keyList.size()) {
- // all keys have unique fingerprints
- result = multiThreadedKeyImport(keyList.iterator(), keyList.size(), keyServer,
- proxy);
- } else {
- result = serialKeyRingImport(keyList, keyServer, proxy);
- }
+
+ result = multiThreadedKeyImport(keyList.iterator(), keyList.size(), keyServer, proxy);
}
ContactSyncAdapterService.requestSync();
@@ -462,7 +444,8 @@ public class ImportOperation extends BaseOperation<ImportKeyringParcel> {
ArrayList<ParcelableKeyRing> list = new ArrayList<>();
list.add(pkRing);
- return serialKeyRingImport(list, keyServer, ignoreProgressable, proxy);
+ return serialKeyRingImport(list.iterator(), 1, keyServer,
+ ignoreProgressable, proxy);
}
};
@@ -486,18 +469,18 @@ public class ImportOperation extends BaseOperation<ImportKeyringParcel> {
}
return accumulator.getConsolidatedResult();
}
- return null; // TODO: Decide if we should just crash instead of returning null
+ return new ImportKeyResult(ImportKeyResult.RESULT_FAIL_NOTHING, new OperationLog());
}
/**
* Used to accumulate the results of individual key imports
*/
- private class KeyImportAccumulator {
+ public static class KeyImportAccumulator {
private OperationResult.OperationLog mImportLog = new OperationResult.OperationLog();
Progressable mProgressable;
private int mTotalKeys;
private int mImportedKeys = 0;
- ArrayList<Long> mImportedMasterKeyIds = new ArrayList<Long>();
+ ArrayList<Long> mImportedMasterKeyIds = new ArrayList<>();
private int mBadKeys = 0;
private int mNewKeys = 0;
private int mUpdatedKeys = 0;
@@ -515,21 +498,17 @@ public class ImportOperation extends BaseOperation<ImportKeyringParcel> {
public KeyImportAccumulator(int totalKeys, Progressable externalProgressable) {
mTotalKeys = totalKeys;
mProgressable = externalProgressable;
- mProgressable.setProgress(0, totalKeys);
- }
-
- public int getTotalKeys() {
- return mTotalKeys;
- }
-
- public int getImportedKeys() {
- return mImportedKeys;
+ if (mProgressable != null) {
+ mProgressable.setProgress(0, totalKeys);
+ }
}
public synchronized void accumulateKeyImport(ImportKeyResult result) {
mImportedKeys++;
- mProgressable.setProgress(mImportedKeys, mTotalKeys);
+ if (mProgressable != null) {
+ mProgressable.setProgress(mImportedKeys, mTotalKeys);
+ }
mImportLog.addAll(result.getLog().toList());//accumulates log
mBadKeys += result.mBadKeys;
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/KeybaseVerificationOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/KeybaseVerificationOperation.java
index 30f37dd4f..8f1abde83 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/KeybaseVerificationOperation.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/KeybaseVerificationOperation.java
@@ -43,7 +43,7 @@ import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.operations.results.DecryptVerifyResult;
import org.sufficientlysecure.keychain.operations.results.KeybaseVerificationResult;
import org.sufficientlysecure.keychain.operations.results.OperationResult;
-import org.sufficientlysecure.keychain.pgp.PgpDecryptVerify;
+import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyOperation;
import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyInputParcel;
import org.sufficientlysecure.keychain.pgp.Progressable;
import org.sufficientlysecure.keychain.provider.ProviderHelper;
@@ -141,7 +141,7 @@ public class KeybaseVerificationOperation extends BaseOperation<KeybaseVerificat
}
}
- PgpDecryptVerify op = new PgpDecryptVerify(mContext, mProviderHelper, mProgressable);
+ PgpDecryptVerifyOperation op = new PgpDecryptVerifyOperation(mContext, mProviderHelper, mProgressable);
PgpDecryptVerifyInputParcel input = new PgpDecryptVerifyInputParcel(messageBytes)
.setSignedLiteralData(true)
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/RevokeOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/RevokeOperation.java
index ecf64e1af..975cf541a 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/RevokeOperation.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/RevokeOperation.java
@@ -20,7 +20,6 @@
package org.sufficientlysecure.keychain.operations;
import android.content.Context;
-import android.database.Cursor;
import android.net.Uri;
import android.support.annotation.NonNull;
@@ -39,7 +38,7 @@ import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
import org.sufficientlysecure.keychain.util.Log;
-public class RevokeOperation extends BaseOperation<RevokeKeyringParcel> {
+public class RevokeOperation extends BaseOperation<RevokeKeyringParcel> {
public RevokeOperation(Context context, ProviderHelper providerHelper, Progressable progressable) {
super(context, providerHelper, progressable);
@@ -71,13 +70,15 @@ public class RevokeOperation extends BaseOperation<RevokeKeyringParcel> {
return new RevokeResult(RevokeResult.RESULT_ERROR, log, masterKeyId);
}
- SaveKeyringParcel saveKeyringParcel = getRevokedSaveKeyringParcel(masterKeyId,
- keyRing.getFingerprint());
+ SaveKeyringParcel saveKeyringParcel =
+ new SaveKeyringParcel(masterKeyId, keyRing.getFingerprint());
// all revoke operations are made atomic as of now
saveKeyringParcel.setUpdateOptions(revokeKeyringParcel.mUpload, true,
revokeKeyringParcel.mKeyserver);
+ saveKeyringParcel.mRevokeSubKeys.add(masterKeyId);
+
InputPendingResult revokeAndUploadResult = new EditKeyOperation(mContext,
mProviderHelper, mProgressable, mCancelled)
.execute(saveKeyringParcel, cryptoInputParcel);
@@ -92,54 +93,15 @@ public class RevokeOperation extends BaseOperation<RevokeKeyringParcel> {
log.add(OperationResult.LogType.MSG_REVOKE_OK, 1);
return new RevokeResult(RevokeResult.RESULT_OK, log, masterKeyId);
} else {
- log.add(OperationResult.LogType.MSG_REVOKE_KEY_FAIL, 1);
+ log.add(OperationResult.LogType.MSG_REVOKE_ERROR_KEY_FAIL, 1);
return new RevokeResult(RevokeResult.RESULT_ERROR, log, masterKeyId);
}
} catch (PgpKeyNotFoundException | ProviderHelper.NotFoundException e) {
Log.e(Constants.TAG, "could not find key to revoke", e);
- log.add(OperationResult.LogType.MSG_REVOKE_KEY_FAIL, 1);
+ log.add(OperationResult.LogType.MSG_REVOKE_ERROR_KEY_FAIL, 1);
return new RevokeResult(RevokeResult.RESULT_ERROR, log, masterKeyId);
}
}
- private SaveKeyringParcel getRevokedSaveKeyringParcel(long masterKeyId, byte[] fingerprint) {
- final String[] SUBKEYS_PROJECTION = new String[]{
- KeychainContract.Keys.KEY_ID
- };
- final int INDEX_KEY_ID = 0;
-
- Uri keysUri = KeychainContract.Keys.buildKeysUri(masterKeyId);
- Cursor subKeyCursor =
- mContext.getContentResolver().query(keysUri, SUBKEYS_PROJECTION, null, null, null);
-
- SaveKeyringParcel saveKeyringParcel =
- new SaveKeyringParcel(masterKeyId, fingerprint);
-
- // add all subkeys, for revocation
- while (subKeyCursor != null && subKeyCursor.moveToNext()) {
- saveKeyringParcel.mRevokeSubKeys.add(subKeyCursor.getLong(INDEX_KEY_ID));
- }
- if (subKeyCursor != null) {
- subKeyCursor.close();
- }
-
- final String[] USER_IDS_PROJECTION = new String[]{
- KeychainContract.UserPackets.USER_ID
- };
- final int INDEX_USER_ID = 0;
-
- Uri userIdsUri = KeychainContract.UserPackets.buildUserIdsUri(masterKeyId);
- Cursor userIdCursor = mContext.getContentResolver().query(
- userIdsUri, USER_IDS_PROJECTION, null, null, null);
-
- while (userIdCursor != null && userIdCursor.moveToNext()) {
- saveKeyringParcel.mRevokeUserIds.add(userIdCursor.getString(INDEX_USER_ID));
- }
- if (userIdCursor != null) {
- userIdCursor.close();
- }
-
- return saveKeyringParcel;
- }
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/LinkedVerifyResult.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/LinkedVerifyResult.java
new file mode 100644
index 000000000..42e9ec3f0
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/LinkedVerifyResult.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
+ * Copyright (C) 2014 Vincent Breitmoser <v.breitmoser@mugenguild.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.operations.results;
+
+import android.os.Parcel;
+
+public class LinkedVerifyResult extends OperationResult {
+
+ public LinkedVerifyResult(int result, OperationLog log) {
+ super(result, log);
+ }
+
+ /** Construct from a parcel - trivial because we have no extra data. */
+ public LinkedVerifyResult(Parcel source) {
+ super(source);
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
+ }
+
+ public static Creator<LinkedVerifyResult> CREATOR = new Creator<LinkedVerifyResult>() {
+ public LinkedVerifyResult createFromParcel(final Parcel source) {
+ return new LinkedVerifyResult(source);
+ }
+
+ public LinkedVerifyResult[] newArray(final int size) {
+ return new LinkedVerifyResult[size];
+ }
+ };
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/OperationResult.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/OperationResult.java
index 3c15a2e7b..3856209b3 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/OperationResult.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/OperationResult.java
@@ -20,6 +20,7 @@ package org.sufficientlysecure.keychain.operations.results;
import android.app.Activity;
import android.content.Intent;
+import android.content.res.Resources;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.NonNull;
@@ -52,6 +53,8 @@ import java.util.List;
*/
public abstract class OperationResult implements Parcelable {
+ final static String INDENTATION_WHITESPACE = " ";
+
public static final String EXTRA_RESULT = "operation_result";
/**
@@ -166,6 +169,27 @@ public abstract class OperationResult implements Parcelable {
", mIndent=" + mIndent +
'}';
}
+
+ StringBuilder getPrintableLogEntry(Resources resources, int indent) {
+
+ StringBuilder result = new StringBuilder();
+ int padding = mIndent +indent;
+ if (padding > INDENTATION_WHITESPACE.length()) {
+ padding = INDENTATION_WHITESPACE.length();
+ }
+ result.append(INDENTATION_WHITESPACE, 0, padding);
+ result.append(LOG_LEVEL_NAME[mType.mLevel.ordinal()]).append(' ');
+
+ // special case: first parameter may be a quantity
+ if (mParameters != null && mParameters.length > 0 && mParameters[0] instanceof Integer) {
+ result.append(resources.getQuantityString(mType.getMsgId(), (Integer) mParameters[0], mParameters));
+ } else {
+ result.append(resources.getString(mType.getMsgId(), mParameters));
+ }
+
+ return result;
+ }
+
}
public static class SubLogEntryParcel extends LogEntryParcel {
@@ -202,6 +226,17 @@ public abstract class OperationResult implements Parcelable {
dest.writeParcelable(mSubResult, 0);
}
+ @Override
+ StringBuilder getPrintableLogEntry(Resources resources, int indent) {
+
+ LogEntryParcel subEntry = mSubResult.getLog().getLast();
+ if (subEntry != null) {
+ return subEntry.getPrintableLogEntry(resources, mIndent +indent);
+ } else {
+ return super.getPrintableLogEntry(resources, indent);
+ }
+ }
+
}
public Showable createNotify(final Activity activity) {
@@ -245,15 +280,15 @@ public abstract class OperationResult implements Parcelable {
}
return Notify.create(activity, logText, Notify.LENGTH_LONG, style,
- new ActionListener() {
- @Override
- public void onAction() {
- Intent intent = new Intent(
- activity, LogDisplayActivity.class);
- intent.putExtra(LogDisplayFragment.EXTRA_RESULT, OperationResult.this);
- activity.startActivity(intent);
- }
- }, R.string.snackbar_details);
+ new ActionListener() {
+ @Override
+ public void onAction() {
+ Intent intent = new Intent(
+ activity, LogDisplayActivity.class);
+ intent.putExtra(LogDisplayFragment.EXTRA_RESULT, OperationResult.this);
+ activity.startActivity(intent);
+ }
+ }, R.string.snackbar_details);
}
@@ -289,6 +324,8 @@ public abstract class OperationResult implements Parcelable {
MSG_IP_ERROR_IO_EXC (LogLevel.ERROR, R.string.msg_ip_error_io_exc),
MSG_IP_ERROR_OP_EXC (LogLevel.ERROR, R.string.msg_ip_error_op_exc),
MSG_IP_ERROR_REMOTE_EX (LogLevel.ERROR, R.string.msg_ip_error_remote_ex),
+ MSG_IP_FINGERPRINT_ERROR (LogLevel.ERROR, R.string.msg_ip_fingerprint_error),
+ MSG_IP_FINGERPRINT_OK (LogLevel.INFO, R.string.msg_ip_fingerprint_ok),
MSG_IP_INSERT_KEYRING (LogLevel.DEBUG, R.string.msg_ip_insert_keyring),
MSG_IP_INSERT_SUBKEYS (LogLevel.DEBUG, R.string.msg_ip_insert_keys),
MSG_IP_PREPARE (LogLevel.DEBUG, R.string.msg_ip_prepare),
@@ -631,6 +668,7 @@ public abstract class OperationResult implements Parcelable {
MSG_DC_TRAIL_SYM (LogLevel.DEBUG, R.string.msg_dc_trail_sym),
MSG_DC_TRAIL_UNKNOWN (LogLevel.DEBUG, R.string.msg_dc_trail_unknown),
MSG_DC_UNLOCKING (LogLevel.INFO, R.string.msg_dc_unlocking),
+ MSG_DC_INSECURE_ENCRYPTION_KEY (LogLevel.WARN, R.string.msg_dc_insecure_encryption_key),
MSG_DC_INSECURE_SYMMETRIC_ENCRYPTION_ALGO(LogLevel.WARN, R.string.msg_dc_insecure_symmetric_encryption_algo),
MSG_DC_INSECURE_HASH_ALGO(LogLevel.ERROR, R.string.msg_dc_insecure_hash_algo),
MSG_DC_INSECURE_MDC_MISSING(LogLevel.ERROR, R.string.msg_dc_insecure_mdc_missing),
@@ -704,21 +742,21 @@ public abstract class OperationResult implements Parcelable {
MSG_IMPORT_FETCH_ERROR (LogLevel.ERROR, R.string.msg_import_fetch_error),
MSG_IMPORT_FETCH_ERROR_DECODE (LogLevel.ERROR, R.string.msg_import_fetch_error_decode),
+ MSG_IMPORT_FETCH_ERROR_KEYSERVER(LogLevel.ERROR, R.string.msg_import_fetch_error_keyserver),
+ MSG_IMPORT_FETCH_ERROR_KEYSERVER_SECRET (LogLevel.ERROR, R.string.msg_import_fetch_error_keyserver_secret),
+ MSG_IMPORT_FETCH_KEYBASE (LogLevel.INFO, R.string.msg_import_fetch_keybase),
MSG_IMPORT_FETCH_KEYSERVER (LogLevel.INFO, R.string.msg_import_fetch_keyserver),
MSG_IMPORT_FETCH_KEYSERVER_OK (LogLevel.DEBUG, R.string.msg_import_fetch_keyserver_ok),
- MSG_IMPORT_FETCH_KEYSERVER_ERROR (LogLevel.ERROR, R.string.msg_import_fetch_keyserver_error),
- MSG_IMPORT_FETCH_KEYBASE (LogLevel.INFO, R.string.msg_import_fetch_keybase),
MSG_IMPORT_KEYSERVER (LogLevel.DEBUG, R.string.msg_import_keyserver),
MSG_IMPORT_MERGE (LogLevel.DEBUG, R.string.msg_import_merge),
MSG_IMPORT_MERGE_ERROR (LogLevel.ERROR, R.string.msg_import_merge_error),
- MSG_IMPORT_FINGERPRINT_ERROR (LogLevel.ERROR, R.string.msg_import_fingerprint_error),
- MSG_IMPORT_FINGERPRINT_OK (LogLevel.DEBUG, R.string.msg_import_fingerprint_ok),
MSG_IMPORT_ERROR (LogLevel.ERROR, R.string.msg_import_error),
MSG_IMPORT_ERROR_IO (LogLevel.ERROR, R.string.msg_import_error_io),
MSG_IMPORT_PARTIAL (LogLevel.ERROR, R.string.msg_import_partial),
MSG_IMPORT_SUCCESS (LogLevel.OK, R.string.msg_import_success),
MSG_EXPORT (LogLevel.START, R.plurals.msg_export),
+ MSG_EXPORT_FILE_NAME (LogLevel.INFO, R.string.msg_export_file_name),
MSG_EXPORT_UPLOAD_PUBLIC (LogLevel.START, R.string.msg_export_upload_public),
MSG_EXPORT_PUBLIC (LogLevel.DEBUG, R.string.msg_export_public),
MSG_EXPORT_SECRET (LogLevel.DEBUG, R.string.msg_export_secret),
@@ -763,10 +801,9 @@ public abstract class OperationResult implements Parcelable {
MSG_DEL_FAIL (LogLevel.WARN, R.plurals.msg_del_fail),
MSG_REVOKE_ERROR_EMPTY (LogLevel.ERROR, R.string.msg_revoke_error_empty),
- MSG_REVOKE_ERROR_MULTI_SECRET (LogLevel.DEBUG, R.string.msg_revoke_error_multi_secret),
- MSG_REVOKE_ERROR_NOT_FOUND (LogLevel.DEBUG, R.string.msg_revoke_error_multi_secret),
+ MSG_REVOKE_ERROR_NOT_FOUND (LogLevel.ERROR, R.string.msg_revoke_error_not_found),
MSG_REVOKE (LogLevel.DEBUG, R.string.msg_revoke_key),
- MSG_REVOKE_KEY_FAIL (LogLevel.ERROR, R.string.msg_revoke_key_fail),
+ MSG_REVOKE_ERROR_KEY_FAIL (LogLevel.ERROR, R.string.msg_revoke_key_fail),
MSG_REVOKE_OK (LogLevel.OK, R.string.msg_revoke_ok),
// keybase verification
@@ -781,17 +818,31 @@ public abstract class OperationResult implements Parcelable {
MSG_KEYBASE_ERROR_PAYLOAD_MISMATCH(LogLevel.ERROR,
R.string.msg_keybase_error_msg_payload_mismatch),
- // export log
- MSG_EXPORT_LOG(LogLevel.START,R.string.msg_export_log_start),
- MSG_EXPORT_LOG_EXPORT_ERROR_NO_FILE(LogLevel.ERROR,R.string.msg_export_log_error_no_file),
- MSG_EXPORT_LOG_EXPORT_ERROR_FOPEN(LogLevel.ERROR,R.string.msg_export_log_error_fopen),
- MSG_EXPORT_LOG_EXPORT_ERROR_WRITING(LogLevel.ERROR,R.string.msg_export_log_error_writing),
- MSG_EXPORT_LOG_EXPORT_SUCCESS (LogLevel.OK, R.string.msg_export_log_success),
-
- // mim parsing
+ // mime parsing
MSG_MIME_PARSING(LogLevel.START,R.string.msg_mime_parsing_start),
MSG_MIME_PARSING_ERROR(LogLevel.ERROR,R.string.msg_mime_parsing_error),
MSG_MIME_PARSING_SUCCESS(LogLevel.OK,R.string.msg_mime_parsing_success),
+
+ MSG_LV (LogLevel.START, R.string.msg_lv),
+ MSG_LV_MATCH (LogLevel.DEBUG, R.string.msg_lv_match),
+ MSG_LV_MATCH_ERROR (LogLevel.ERROR, R.string.msg_lv_match_error),
+ MSG_LV_FP_OK (LogLevel.DEBUG, R.string.msg_lv_fp_ok),
+ MSG_LV_FP_ERROR (LogLevel.ERROR, R.string.msg_lv_fp_error),
+
+ MSG_LV_ERROR_TWITTER_AUTH (LogLevel.ERROR, R.string.msg_lv_error_twitter_auth),
+ MSG_LV_ERROR_TWITTER_HANDLE (LogLevel.ERROR, R.string.msg_lv_error_twitter_handle),
+ MSG_LV_ERROR_TWITTER_RESPONSE (LogLevel.ERROR, R.string.msg_lv_error_twitter_response),
+ MSG_LV_ERROR_GITHUB_HANDLE (LogLevel.ERROR, R.string.msg_lv_error_github_handle),
+ MSG_LV_ERROR_GITHUB_NOT_FOUND (LogLevel.ERROR, R.string.msg_lv_error_github_not_found),
+
+ MSG_LV_FETCH (LogLevel.DEBUG, R.string.msg_lv_fetch),
+ MSG_LV_FETCH_REDIR (LogLevel.DEBUG, R.string.msg_lv_fetch_redir),
+ MSG_LV_FETCH_OK (LogLevel.DEBUG, R.string.msg_lv_fetch_ok),
+ MSG_LV_FETCH_ERROR (LogLevel.ERROR, R.string.msg_lv_fetch_error),
+ MSG_LV_FETCH_ERROR_URL (LogLevel.ERROR, R.string.msg_lv_fetch_error_url),
+ MSG_LV_FETCH_ERROR_IO (LogLevel.ERROR, R.string.msg_lv_fetch_error_io),
+ MSG_LV_FETCH_ERROR_FORMAT(LogLevel.ERROR, R.string.msg_lv_fetch_error_format),
+ MSG_LV_FETCH_ERROR_NOTHING (LogLevel.ERROR, R.string.msg_lv_fetch_error_nothing),
;
public final int mMsgId;
@@ -815,6 +866,10 @@ public abstract class OperationResult implements Parcelable {
OK, // should occur once at the end of a successful operation
CANCELLED, // should occur once at the end of a cancelled operation
}
+ // for print of debug log. keep those in sync with above!
+ static final String[] LOG_LEVEL_NAME = new String[] {
+ "[DEBUG]", "[INFO]", "[WARN]", "[ERROR]", "[START]", "[OK]", "[CANCEL]"
+ };
@Override
public int describeContents() {
@@ -913,6 +968,20 @@ public abstract class OperationResult implements Parcelable {
public Iterator<LogEntryParcel> iterator() {
return mParcels.iterator();
}
+
+ /**
+ * returns an indented String of an entire OperationLog
+ * @param indent padding to add at the start of all log entries, made for use with SubLogs
+ * @return printable, indented version of passed operationLog
+ */
+ public String getPrintableOperationLog(Resources resources, int indent) {
+ StringBuilder log = new StringBuilder();
+ for (LogEntryParcel entry : this) {
+ log.append(entry.getPrintableLogEntry(resources, indent)).append("\n");
+ }
+ return log.toString().substring(0, log.length() -1); // get rid of extra new line
+ }
+
}
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedKeyRing.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedKeyRing.java
index 770e8de91..18a27dd96 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedKeyRing.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedKeyRing.java
@@ -21,6 +21,7 @@ package org.sufficientlysecure.keychain.pgp;
import org.spongycastle.openpgp.PGPKeyRing;
import org.spongycastle.openpgp.PGPPublicKey;
import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException;
+import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
import org.sufficientlysecure.keychain.util.IterableIterator;
import java.io.IOException;
@@ -28,6 +29,7 @@ import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
+import java.util.Iterator;
import java.util.Set;
@@ -152,4 +154,14 @@ public abstract class CanonicalizedKeyRing extends KeyRing {
return getRing().getEncoded();
}
+ public boolean containsSubkey(String expectedFingerprint) {
+ for (CanonicalizedPublicKey key : publicKeyIterator()) {
+ if (KeyFormattingUtils.convertFingerprintToHex(
+ key.getFingerprint()).equalsIgnoreCase(expectedFingerprint)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerify.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerifyOperation.java
index 1e51403fc..dda15f382 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerify.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerifyOperation.java
@@ -79,9 +79,9 @@ import java.security.SignatureException;
import java.util.Date;
import java.util.Iterator;
-public class PgpDecryptVerify extends BaseOperation<PgpDecryptVerifyInputParcel> {
+public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInputParcel> {
- public PgpDecryptVerify(Context context, ProviderHelper providerHelper, Progressable progressable) {
+ public PgpDecryptVerifyOperation(Context context, ProviderHelper providerHelper, Progressable progressable) {
super(context, providerHelper, progressable);
}
@@ -313,6 +313,27 @@ public class PgpDecryptVerify extends BaseOperation<PgpDecryptVerifyInputParcel>
return result;
}
+ private static class EncryptStreamResult {
+
+ // this is non-null iff an error occured, return directly
+ DecryptVerifyResult errorResult;
+
+ // for verification
+ PGPEncryptedData encryptedData;
+ InputStream cleartextStream;
+
+ int symmetricEncryptionAlgo = 0;
+
+ boolean skippedDisallowedKey = false;
+ boolean insecureEncryptionKey = false;
+
+ // convenience method to return with error
+ public EncryptStreamResult with(DecryptVerifyResult result) {
+ errorResult = result;
+ return this;
+ }
+
+ }
/** Decrypt and/or verify binary or ascii armored pgp data. */
@NonNull
@@ -320,42 +341,14 @@ public class PgpDecryptVerify extends BaseOperation<PgpDecryptVerifyInputParcel>
PgpDecryptVerifyInputParcel input, CryptoInputParcel cryptoInput,
InputStream in, OutputStream out, int indent) throws IOException, PGPException {
- OpenPgpSignatureResultBuilder signatureResultBuilder = new OpenPgpSignatureResultBuilder();
- OpenPgpDecryptionResultBuilder decryptionResultBuilder = new OpenPgpDecryptionResultBuilder();
OperationLog log = new OperationLog();
log.add(LogType.MSG_DC, indent);
indent += 1;
- JcaPGPObjectFactory pgpF = new JcaPGPObjectFactory(in);
- PGPEncryptedDataList enc;
- Object o = pgpF.nextObject();
-
int currentProgress = 0;
updateProgress(R.string.progress_reading_data, currentProgress, 100);
- if (o instanceof PGPEncryptedDataList) {
- enc = (PGPEncryptedDataList) o;
- } else {
- enc = (PGPEncryptedDataList) pgpF.nextObject();
- }
-
- if (enc == null) {
- log.add(LogType.MSG_DC_ERROR_INVALID_DATA, indent);
- return new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log);
- }
-
- InputStream clear;
- PGPEncryptedData encryptedData;
-
- PGPPublicKeyEncryptedData encryptedDataAsymmetric = null;
- PGPPBEEncryptedData encryptedDataSymmetric = null;
- CanonicalizedSecretKey secretEncryptionKey = null;
- Iterator<?> it = enc.getEncryptedDataObjects();
- boolean asymmetricPacketFound = false;
- boolean symmetricPacketFound = false;
- boolean anyPacketFound = false;
-
// If the input stream is armored, and there is a charset specified, take a note for later
// https://tools.ietf.org/html/rfc4880#page56
String charset = null;
@@ -375,8 +368,332 @@ public class PgpDecryptVerify extends BaseOperation<PgpDecryptVerifyInputParcel>
}
}
+ OpenPgpSignatureResultBuilder signatureResultBuilder = new OpenPgpSignatureResultBuilder();
+ OpenPgpDecryptionResultBuilder decryptionResultBuilder = new OpenPgpDecryptionResultBuilder();
+
+ JcaPGPObjectFactory plainFact;
+ Object dataChunk;
+ EncryptStreamResult esResult = null;
+ { // resolve encrypted (symmetric and asymmetric) packets
+ JcaPGPObjectFactory pgpF = new JcaPGPObjectFactory(in);
+ Object obj = pgpF.nextObject();
+
+ if (obj instanceof PGPEncryptedDataList) {
+ esResult = handleEncryptedPacket(
+ input, cryptoInput, (PGPEncryptedDataList) obj, log, indent, currentProgress);
+
+ // if there is an error, nothing left to do here
+ if (esResult.errorResult != null) {
+ return esResult.errorResult;
+ }
+
+ // if this worked out so far, the data is encrypted
+ decryptionResultBuilder.setEncrypted(true);
+
+ if (esResult.insecureEncryptionKey) {
+ log.add(LogType.MSG_DC_INSECURE_SYMMETRIC_ENCRYPTION_ALGO, indent + 1);
+ decryptionResultBuilder.setInsecure(true);
+ }
+
+ // Check for insecure encryption algorithms!
+ if (!PgpSecurityConstants.isSecureSymmetricAlgorithm(esResult.symmetricEncryptionAlgo)) {
+ log.add(LogType.MSG_DC_INSECURE_SYMMETRIC_ENCRYPTION_ALGO, indent + 1);
+ decryptionResultBuilder.setInsecure(true);
+ }
+
+ plainFact = new JcaPGPObjectFactory(esResult.cleartextStream);
+ dataChunk = plainFact.nextObject();
+
+ } else {
+ decryptionResultBuilder.setEncrypted(false);
+
+ plainFact = pgpF;
+ dataChunk = obj;
+ }
+
+ }
+
+ log.add(LogType.MSG_DC_PREP_STREAMS, indent);
+
+ int signatureIndex = -1;
+ CanonicalizedPublicKeyRing signingRing = null;
+ CanonicalizedPublicKey signingKey = null;
+
+ log.add(LogType.MSG_DC_CLEAR, indent);
+ indent += 1;
+
+ // resolve compressed data
+ if (dataChunk instanceof PGPCompressedData) {
+ log.add(LogType.MSG_DC_CLEAR_DECOMPRESS, indent + 1);
+ currentProgress += 2;
+ updateProgress(R.string.progress_decompressing_data, currentProgress, 100);
+
+ PGPCompressedData compressedData = (PGPCompressedData) dataChunk;
+
+ JcaPGPObjectFactory fact = new JcaPGPObjectFactory(compressedData.getDataStream());
+ dataChunk = fact.nextObject();
+ plainFact = fact;
+ }
+
+ // resolve leading signature data
+ PGPOnePassSignature signature = null;
+ if (dataChunk instanceof PGPOnePassSignatureList) {
+ log.add(LogType.MSG_DC_CLEAR_SIGNATURE, indent + 1);
+ currentProgress += 2;
+ updateProgress(R.string.progress_processing_signature, currentProgress, 100);
+
+ PGPOnePassSignatureList sigList = (PGPOnePassSignatureList) dataChunk;
+
+ // NOTE: following code is similar to processSignature, but for PGPOnePassSignature
+
+ // go through all signatures
+ // and find out for which signature we have a key in our database
+ for (int i = 0; i < sigList.size(); ++i) {
+ try {
+ long sigKeyId = sigList.get(i).getKeyID();
+ signingRing = mProviderHelper.getCanonicalizedPublicKeyRing(
+ KeyRings.buildUnifiedKeyRingsFindBySubkeyUri(sigKeyId)
+ );
+ signingKey = signingRing.getPublicKey(sigKeyId);
+ signatureIndex = i;
+ } catch (ProviderHelper.NotFoundException e) {
+ Log.d(Constants.TAG, "key not found, trying next signature...");
+ }
+ }
+
+ if (signingKey != null) {
+ // key found in our database!
+ signature = sigList.get(signatureIndex);
+
+ signatureResultBuilder.initValid(signingRing, signingKey);
+
+ JcaPGPContentVerifierBuilderProvider contentVerifierBuilderProvider =
+ new JcaPGPContentVerifierBuilderProvider()
+ .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME);
+ signature.init(contentVerifierBuilderProvider, signingKey.getPublicKey());
+ } else {
+ // no key in our database -> return "unknown pub key" status including the first key id
+ if (!sigList.isEmpty()) {
+ signatureResultBuilder.setSignatureAvailable(true);
+ signatureResultBuilder.setKnownKey(false);
+ signatureResultBuilder.setKeyId(sigList.get(0).getKeyID());
+ }
+ }
+
+ // check for insecure signing key
+ // TODO: checks on signingRing ?
+ if (signingKey != null && ! PgpSecurityConstants.isSecureKey(signingKey)) {
+ log.add(LogType.MSG_DC_INSECURE_KEY, indent + 1);
+ signatureResultBuilder.setInsecure(true);
+ }
+
+ dataChunk = plainFact.nextObject();
+ }
+
+ if (dataChunk instanceof PGPSignatureList) {
+ // skip
+ dataChunk = plainFact.nextObject();
+ }
+
+ OpenPgpMetadata metadata;
+
+ if ( ! (dataChunk instanceof PGPLiteralData)) {
+
+ log.add(LogType.MSG_DC_ERROR_INVALID_DATA, indent);
+ return new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log);
+
+ }
+
+ log.add(LogType.MSG_DC_CLEAR_DATA, indent + 1);
+ indent += 2;
+ currentProgress += 4;
+ updateProgress(R.string.progress_decrypting, currentProgress, 100);
+
+ PGPLiteralData literalData = (PGPLiteralData) dataChunk;
+
+ String originalFilename = literalData.getFileName();
+ if (originalFilename.contains("/")) {
+ originalFilename = originalFilename.substring(originalFilename.lastIndexOf('/'));
+ }
+ String mimeType = null;
+ if (literalData.getFormat() == PGPLiteralData.TEXT
+ || literalData.getFormat() == PGPLiteralData.UTF8) {
+ mimeType = "text/plain";
+ } else {
+ // try to guess from file ending
+ String extension = MimeTypeMap.getFileExtensionFromUrl(originalFilename);
+ if (extension != null) {
+ MimeTypeMap mime = MimeTypeMap.getSingleton();
+ mimeType = mime.getMimeTypeFromExtension(extension);
+ }
+ if (mimeType == null) {
+ mimeType = "application/octet-stream";
+ }
+ }
+
+ if (!"".equals(originalFilename)) {
+ log.add(LogType.MSG_DC_CLEAR_META_FILE, indent + 1, originalFilename);
+ }
+ log.add(LogType.MSG_DC_CLEAR_META_MIME, indent + 1,
+ mimeType);
+ log.add(LogType.MSG_DC_CLEAR_META_TIME, indent + 1,
+ new Date(literalData.getModificationTime().getTime()).toString());
+
+ // return here if we want to decrypt the metadata only
+ if (input.isDecryptMetadataOnly()) {
+
+ // this operation skips the entire stream to find the data length!
+ Long originalSize = literalData.findDataLength();
+
+ if (originalSize != null) {
+ log.add(LogType.MSG_DC_CLEAR_META_SIZE, indent + 1,
+ Long.toString(originalSize));
+ } else {
+ log.add(LogType.MSG_DC_CLEAR_META_SIZE_UNKNOWN, indent + 1);
+ }
+
+ metadata = new OpenPgpMetadata(
+ originalFilename,
+ mimeType,
+ literalData.getModificationTime().getTime(),
+ originalSize == null ? 0 : originalSize);
+
+ log.add(LogType.MSG_DC_OK_META_ONLY, indent);
+ DecryptVerifyResult result =
+ new DecryptVerifyResult(DecryptVerifyResult.RESULT_OK, log);
+ result.setCharset(charset);
+ result.setDecryptionMetadata(metadata);
+ return result;
+ }
+
+ int endProgress;
+ if (signature != null) {
+ endProgress = 90;
+ } else if (esResult != null && esResult.encryptedData.isIntegrityProtected()) {
+ endProgress = 95;
+ } else {
+ endProgress = 100;
+ }
+ ProgressScaler progressScaler =
+ new ProgressScaler(mProgressable, currentProgress, endProgress, 100);
+
+ InputStream dataIn = literalData.getInputStream();
+
+ long alreadyWritten = 0;
+ long wholeSize = 0; // TODO inputData.getSize() - inputData.getStreamPosition();
+ int length;
+ byte[] buffer = new byte[1 << 16];
+ while ((length = dataIn.read(buffer)) > 0) {
+ // Log.d(Constants.TAG, "read bytes: " + length);
+ if (out != null) {
+ out.write(buffer, 0, length);
+ }
+
+ // update signature buffer if signature is also present
+ if (signature != null) {
+ signature.update(buffer, 0, length);
+ }
+
+ alreadyWritten += length;
+ if (wholeSize > 0) {
+ long progress = 100 * alreadyWritten / wholeSize;
+ // stop at 100% for wrong file sizes...
+ if (progress > 100) {
+ progress = 100;
+ }
+ progressScaler.setProgress((int) progress, 100);
+ }
+ // TODO: slow annealing to fake a progress?
+ }
+
+ metadata = new OpenPgpMetadata(
+ originalFilename, mimeType, literalData.getModificationTime().getTime(), alreadyWritten);
+
+ if (signature != null) {
+ updateProgress(R.string.progress_verifying_signature, 90, 100);
+ log.add(LogType.MSG_DC_CLEAR_SIGNATURE_CHECK, indent);
+
+ PGPSignatureList signatureList = (PGPSignatureList) plainFact.nextObject();
+ PGPSignature messageSignature = signatureList.get(signatureIndex);
+
+ // Verify signature
+ boolean validSignature = signature.verify(messageSignature);
+ if (validSignature) {
+ log.add(LogType.MSG_DC_CLEAR_SIGNATURE_OK, indent + 1);
+ } else {
+ log.add(LogType.MSG_DC_CLEAR_SIGNATURE_BAD, indent + 1);
+ }
+
+ // check for insecure hash algorithms
+ if (!PgpSecurityConstants.isSecureHashAlgorithm(signature.getHashAlgorithm())) {
+ log.add(LogType.MSG_DC_INSECURE_HASH_ALGO, indent + 1);
+ signatureResultBuilder.setInsecure(true);
+ }
+
+ signatureResultBuilder.setValidSignature(validSignature);
+ }
+
+ indent -= 1;
+
+ if (esResult != null) {
+ if (esResult.encryptedData.isIntegrityProtected()) {
+ updateProgress(R.string.progress_verifying_integrity, 95, 100);
+
+ if (esResult.encryptedData.verify()) {
+ log.add(LogType.MSG_DC_INTEGRITY_CHECK_OK, indent);
+ } else {
+ log.add(LogType.MSG_DC_ERROR_INTEGRITY_CHECK, indent);
+ return new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log);
+ }
+ } else if (signature == null) {
+ // If no signature is present, we *require* an MDC!
+ // Handle missing integrity protection like failed integrity protection!
+ // The MDC packet can be stripped by an attacker!
+ log.add(LogType.MSG_DC_INSECURE_MDC_MISSING, indent);
+ decryptionResultBuilder.setInsecure(true);
+ }
+ }
+
+ updateProgress(R.string.progress_done, 100, 100);
+
+ log.add(LogType.MSG_DC_OK, indent);
+
+ // Return a positive result, with metadata and verification info
+ DecryptVerifyResult result = new DecryptVerifyResult(DecryptVerifyResult.RESULT_OK, log);
+
+ result.setCachedCryptoInputParcel(cryptoInput);
+ result.setSignatureResult(signatureResultBuilder.build());
+ result.setCharset(charset);
+ result.setDecryptionResult(decryptionResultBuilder.build());
+ result.setDecryptionMetadata(metadata);
+
+ return result;
+
+ }
+
+ private EncryptStreamResult handleEncryptedPacket(PgpDecryptVerifyInputParcel input, CryptoInputParcel cryptoInput,
+ PGPEncryptedDataList enc, OperationLog log, int indent, int currentProgress) throws PGPException {
+
+ // TODO is this necessary?
+ /*
+ else if (obj instanceof PGPEncryptedDataList) {
+ enc = (PGPEncryptedDataList) pgpF.nextObject();
+ }
+ */
+
+ EncryptStreamResult result = new EncryptStreamResult();
+
+ boolean asymmetricPacketFound = false;
+ boolean symmetricPacketFound = false;
+ boolean anyPacketFound = false;
+
+ PGPPublicKeyEncryptedData encryptedDataAsymmetric = null;
+ PGPPBEEncryptedData encryptedDataSymmetric = null;
+ CanonicalizedSecretKey secretEncryptionKey = null;
+
Passphrase passphrase = null;
- boolean skippedDisallowedKey = false;
+
+ Iterator<?> it = enc.getEncryptedDataObjects();
// go through all objects and find one we can decrypt
while (it.hasNext()) {
@@ -420,7 +737,7 @@ public class PgpDecryptVerify extends BaseOperation<PgpDecryptVerifyInputParcel>
if (!input.getAllowedKeyIds().contains(masterKeyId)) {
// this key is in our db, but NOT allowed!
// continue with the next packet in the while loop
- skippedDisallowedKey = true;
+ result.skippedDisallowedKey = true;
log.add(LogType.MSG_DC_ASKIP_NOT_ALLOWED, indent + 1);
continue;
}
@@ -451,23 +768,23 @@ public class PgpDecryptVerify extends BaseOperation<PgpDecryptVerifyInputParcel>
log.add(LogType.MSG_DC_PASS_CACHED, indent + 1);
} catch (PassphraseCacheInterface.NoSecretKeyException e) {
log.add(LogType.MSG_DC_ERROR_NO_KEY, indent + 1);
- return new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log);
+ return result.with(new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log));
}
// if passphrase was not cached, return here indicating that a passphrase is missing!
if (passphrase == null) {
log.add(LogType.MSG_DC_PENDING_PASSPHRASE, indent + 1);
- return new DecryptVerifyResult(log,
+ return result.with(new DecryptVerifyResult(log,
RequiredInputParcel.createRequiredDecryptPassphrase(
- secretKeyRing.getMasterKeyId(), secretEncryptionKey.getKeyId()),
- cryptoInput);
+ secretKeyRing.getMasterKeyId(), secretEncryptionKey.getKeyId()),
+ cryptoInput));
}
}
// check for insecure encryption key
if ( ! PgpSecurityConstants.isSecureKey(secretEncryptionKey)) {
log.add(LogType.MSG_DC_INSECURE_KEY, indent + 1);
- decryptionResultBuilder.setInsecure(true);
+ result.insecureEncryptionKey = true;
}
// break out of while, only decrypt the first packet where we have a key
@@ -504,9 +821,9 @@ public class PgpDecryptVerify extends BaseOperation<PgpDecryptVerifyInputParcel>
if (passphrase == null) {
log.add(LogType.MSG_DC_PENDING_PASSPHRASE, indent + 1);
- return new DecryptVerifyResult(log,
+ return result.with(new DecryptVerifyResult(log,
RequiredInputParcel.createRequiredSymmetricPassphrase(),
- cryptoInput);
+ cryptoInput));
}
} else {
@@ -533,10 +850,7 @@ public class PgpDecryptVerify extends BaseOperation<PgpDecryptVerifyInputParcel>
}
}
- log.add(LogType.MSG_DC_PREP_STREAMS, indent);
-
// we made sure above one of these two would be true
- int symmetricEncryptionAlgo;
if (symmetricPacketFound) {
currentProgress += 2;
updateProgress(R.string.progress_preparing_streams, currentProgress, 100);
@@ -548,16 +862,16 @@ public class PgpDecryptVerify extends BaseOperation<PgpDecryptVerifyInputParcel>
passphrase.getCharArray());
try {
- clear = encryptedDataSymmetric.getDataStream(decryptorFactory);
+ result.cleartextStream = encryptedDataSymmetric.getDataStream(decryptorFactory);
} catch (PGPDataValidationException e) {
- log.add(LogType.MSG_DC_ERROR_SYM_PASSPHRASE, indent +1);
- return new DecryptVerifyResult(log,
- RequiredInputParcel.createRequiredSymmetricPassphrase(), cryptoInput);
+ log.add(LogType.MSG_DC_ERROR_SYM_PASSPHRASE, indent + 1);
+ return result.with(new DecryptVerifyResult(log,
+ RequiredInputParcel.createRequiredSymmetricPassphrase(), cryptoInput));
}
- encryptedData = encryptedDataSymmetric;
+ result.encryptedData = encryptedDataSymmetric;
- symmetricEncryptionAlgo = encryptedDataSymmetric.getSymmetricAlgorithm(decryptorFactory);
+ result.symmetricEncryptionAlgo = encryptedDataSymmetric.getSymmetricAlgorithm(decryptorFactory);
} else if (asymmetricPacketFound) {
currentProgress += 2;
updateProgress(R.string.progress_extracting_key, currentProgress, 100);
@@ -566,11 +880,11 @@ public class PgpDecryptVerify extends BaseOperation<PgpDecryptVerifyInputParcel>
log.add(LogType.MSG_DC_UNLOCKING, indent + 1);
if (!secretEncryptionKey.unlock(passphrase)) {
log.add(LogType.MSG_DC_ERROR_BAD_PASSPHRASE, indent + 1);
- return new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log);
+ return result.with(new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log));
}
} catch (PgpGeneralException e) {
log.add(LogType.MSG_DC_ERROR_EXTRACT_KEY, indent + 1);
- return new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log);
+ return result.with(new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log));
}
currentProgress += 2;
@@ -585,300 +899,41 @@ public class PgpDecryptVerify extends BaseOperation<PgpDecryptVerifyInputParcel>
&& !decryptorFactory.hasCachedSessionData(encryptedDataAsymmetric)) {
log.add(LogType.MSG_DC_PENDING_NFC, indent + 1);
- return new DecryptVerifyResult(log, RequiredInputParcel.createNfcDecryptOperation(
+ return result.with(new DecryptVerifyResult(log, RequiredInputParcel.createNfcDecryptOperation(
secretEncryptionKey.getRing().getMasterKeyId(),
secretEncryptionKey.getKeyId(), encryptedDataAsymmetric.getSessionKey()[0]
- ),
- cryptoInput);
+ ), cryptoInput));
}
try {
- clear = encryptedDataAsymmetric.getDataStream(decryptorFactory);
+ result.cleartextStream = encryptedDataAsymmetric.getDataStream(decryptorFactory);
} catch (PGPKeyValidationException | ArrayIndexOutOfBoundsException e) {
log.add(LogType.MSG_DC_ERROR_CORRUPT_DATA, indent + 1);
- return new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log);
+ return result.with(new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log));
}
- symmetricEncryptionAlgo = encryptedDataAsymmetric.getSymmetricAlgorithm(decryptorFactory);
+ result.symmetricEncryptionAlgo = encryptedDataAsymmetric.getSymmetricAlgorithm(decryptorFactory);
+ result.encryptedData = encryptedDataAsymmetric;
cryptoInput.addCryptoData(decryptorFactory.getCachedSessionKeys());
- encryptedData = encryptedDataAsymmetric;
} else {
// there wasn't even any useful data
if (!anyPacketFound) {
log.add(LogType.MSG_DC_ERROR_NO_DATA, indent + 1);
- return new DecryptVerifyResult(DecryptVerifyResult.RESULT_NO_DATA, log);
+ return result.with(new DecryptVerifyResult(DecryptVerifyResult.RESULT_NO_DATA, log));
}
// there was data but key wasn't allowed
- if (skippedDisallowedKey) {
+ if (result.skippedDisallowedKey) {
log.add(LogType.MSG_DC_ERROR_NO_KEY, indent + 1);
- return new DecryptVerifyResult(DecryptVerifyResult.RESULT_KEY_DISALLOWED, log);
+ return result.with(new DecryptVerifyResult(DecryptVerifyResult.RESULT_KEY_DISALLOWED, log));
}
// no packet has been found where we have the corresponding secret key in our db
log.add(LogType.MSG_DC_ERROR_NO_KEY, indent + 1);
- return new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log);
- }
- decryptionResultBuilder.setEncrypted(true);
-
- // Check for insecure encryption algorithms!
- if (!PgpSecurityConstants.isSecureSymmetricAlgorithm(symmetricEncryptionAlgo)) {
- log.add(LogType.MSG_DC_INSECURE_SYMMETRIC_ENCRYPTION_ALGO, indent + 1);
- decryptionResultBuilder.setInsecure(true);
- }
-
- JcaPGPObjectFactory plainFact = new JcaPGPObjectFactory(clear);
- Object dataChunk = plainFact.nextObject();
- int signatureIndex = -1;
- CanonicalizedPublicKeyRing signingRing = null;
- CanonicalizedPublicKey signingKey = null;
-
- log.add(LogType.MSG_DC_CLEAR, indent);
- indent += 1;
-
- if (dataChunk instanceof PGPCompressedData) {
- log.add(LogType.MSG_DC_CLEAR_DECOMPRESS, indent + 1);
- currentProgress += 2;
- updateProgress(R.string.progress_decompressing_data, currentProgress, 100);
-
- PGPCompressedData compressedData = (PGPCompressedData) dataChunk;
-
- JcaPGPObjectFactory fact = new JcaPGPObjectFactory(compressedData.getDataStream());
- dataChunk = fact.nextObject();
- plainFact = fact;
- }
-
- PGPOnePassSignature signature = null;
- if (dataChunk instanceof PGPOnePassSignatureList) {
- log.add(LogType.MSG_DC_CLEAR_SIGNATURE, indent + 1);
- currentProgress += 2;
- updateProgress(R.string.progress_processing_signature, currentProgress, 100);
-
- PGPOnePassSignatureList sigList = (PGPOnePassSignatureList) dataChunk;
-
- // NOTE: following code is similar to processSignature, but for PGPOnePassSignature
-
- // go through all signatures
- // and find out for which signature we have a key in our database
- for (int i = 0; i < sigList.size(); ++i) {
- try {
- long sigKeyId = sigList.get(i).getKeyID();
- signingRing = mProviderHelper.getCanonicalizedPublicKeyRing(
- KeyRings.buildUnifiedKeyRingsFindBySubkeyUri(sigKeyId)
- );
- signingKey = signingRing.getPublicKey(sigKeyId);
- signatureIndex = i;
- } catch (ProviderHelper.NotFoundException e) {
- Log.d(Constants.TAG, "key not found, trying next signature...");
- }
- }
-
- if (signingKey != null) {
- // key found in our database!
- signature = sigList.get(signatureIndex);
-
- signatureResultBuilder.initValid(signingRing, signingKey);
-
- JcaPGPContentVerifierBuilderProvider contentVerifierBuilderProvider =
- new JcaPGPContentVerifierBuilderProvider()
- .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME);
- signature.init(contentVerifierBuilderProvider, signingKey.getPublicKey());
- } else {
- // no key in our database -> return "unknown pub key" status including the first key id
- if (!sigList.isEmpty()) {
- signatureResultBuilder.setSignatureAvailable(true);
- signatureResultBuilder.setKnownKey(false);
- signatureResultBuilder.setKeyId(sigList.get(0).getKeyID());
- }
- }
-
- // check for insecure signing key
- // TODO: checks on signingRing ?
- if (signingKey != null && ! PgpSecurityConstants.isSecureKey(signingKey)) {
- log.add(LogType.MSG_DC_INSECURE_KEY, indent + 1);
- signatureResultBuilder.setInsecure(true);
- }
-
- dataChunk = plainFact.nextObject();
+ return result.with(new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log));
}
- if (dataChunk instanceof PGPSignatureList) {
- // skip
- dataChunk = plainFact.nextObject();
- }
-
- OpenPgpMetadata metadata;
-
- if (dataChunk instanceof PGPLiteralData) {
- log.add(LogType.MSG_DC_CLEAR_DATA, indent + 1);
- indent += 2;
- currentProgress += 4;
- updateProgress(R.string.progress_decrypting, currentProgress, 100);
-
- PGPLiteralData literalData = (PGPLiteralData) dataChunk;
-
- String originalFilename = literalData.getFileName();
- String mimeType = null;
- if (literalData.getFormat() == PGPLiteralData.TEXT
- || literalData.getFormat() == PGPLiteralData.UTF8) {
- mimeType = "text/plain";
- } else {
- // try to guess from file ending
- String extension = MimeTypeMap.getFileExtensionFromUrl(originalFilename);
- if (extension != null) {
- MimeTypeMap mime = MimeTypeMap.getSingleton();
- mimeType = mime.getMimeTypeFromExtension(extension);
- }
- if (mimeType == null) {
- mimeType = "application/octet-stream";
- }
- }
-
- if (!"".equals(originalFilename)) {
- log.add(LogType.MSG_DC_CLEAR_META_FILE, indent + 1, originalFilename);
- }
- log.add(LogType.MSG_DC_CLEAR_META_MIME, indent + 1,
- mimeType);
- log.add(LogType.MSG_DC_CLEAR_META_TIME, indent + 1,
- new Date(literalData.getModificationTime().getTime()).toString());
-
- // return here if we want to decrypt the metadata only
- if (input.isDecryptMetadataOnly()) {
-
- // this operation skips the entire stream to find the data length!
- Long originalSize = literalData.findDataLength();
-
- if (originalSize != null) {
- log.add(LogType.MSG_DC_CLEAR_META_SIZE, indent + 1,
- Long.toString(originalSize));
- } else {
- log.add(LogType.MSG_DC_CLEAR_META_SIZE_UNKNOWN, indent + 1);
- }
-
- metadata = new OpenPgpMetadata(
- originalFilename,
- mimeType,
- literalData.getModificationTime().getTime(),
- originalSize == null ? 0 : originalSize);
-
- log.add(LogType.MSG_DC_OK_META_ONLY, indent);
- DecryptVerifyResult result =
- new DecryptVerifyResult(DecryptVerifyResult.RESULT_OK, log);
- result.setCharset(charset);
- result.setDecryptionMetadata(metadata);
- return result;
- }
-
- int endProgress;
- if (signature != null) {
- endProgress = 90;
- } else if (encryptedData.isIntegrityProtected()) {
- endProgress = 95;
- } else {
- endProgress = 100;
- }
- ProgressScaler progressScaler =
- new ProgressScaler(mProgressable, currentProgress, endProgress, 100);
-
- InputStream dataIn = literalData.getInputStream();
-
- long alreadyWritten = 0;
- long wholeSize = 0; // TODO inputData.getSize() - inputData.getStreamPosition();
- int length;
- byte[] buffer = new byte[1 << 16];
- while ((length = dataIn.read(buffer)) > 0) {
- // Log.d(Constants.TAG, "read bytes: " + length);
- if (out != null) {
- out.write(buffer, 0, length);
- }
-
- // update signature buffer if signature is also present
- if (signature != null) {
- signature.update(buffer, 0, length);
- }
-
- alreadyWritten += length;
- if (wholeSize > 0) {
- long progress = 100 * alreadyWritten / wholeSize;
- // stop at 100% for wrong file sizes...
- if (progress > 100) {
- progress = 100;
- }
- progressScaler.setProgress((int) progress, 100);
- }
- // TODO: slow annealing to fake a progress?
- }
-
- metadata = new OpenPgpMetadata(
- originalFilename,
- mimeType,
- literalData.getModificationTime().getTime(),
- alreadyWritten);
-
- if (signature != null) {
- updateProgress(R.string.progress_verifying_signature, 90, 100);
- log.add(LogType.MSG_DC_CLEAR_SIGNATURE_CHECK, indent);
-
- PGPSignatureList signatureList = (PGPSignatureList) plainFact.nextObject();
- PGPSignature messageSignature = signatureList.get(signatureIndex);
-
- // TODO: what about binary signatures?
-
- // Verify signature
- boolean validSignature = signature.verify(messageSignature);
- if (validSignature) {
- log.add(LogType.MSG_DC_CLEAR_SIGNATURE_OK, indent + 1);
- } else {
- log.add(LogType.MSG_DC_CLEAR_SIGNATURE_BAD, indent + 1);
- }
-
- // check for insecure hash algorithms
- if (!PgpSecurityConstants.isSecureHashAlgorithm(signature.getHashAlgorithm())) {
- log.add(LogType.MSG_DC_INSECURE_HASH_ALGO, indent + 1);
- signatureResultBuilder.setInsecure(true);
- }
-
- signatureResultBuilder.setValidSignature(validSignature);
- }
-
- indent -= 1;
- } else {
- // If there is no literalData, we don't have any metadata
- metadata = null;
- }
-
- if (encryptedData.isIntegrityProtected()) {
- updateProgress(R.string.progress_verifying_integrity, 95, 100);
-
- if (encryptedData.verify()) {
- log.add(LogType.MSG_DC_INTEGRITY_CHECK_OK, indent);
- } else {
- log.add(LogType.MSG_DC_ERROR_INTEGRITY_CHECK, indent);
- return new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log);
- }
- } else {
- // If no valid signature is present:
- // Handle missing integrity protection like failed integrity protection!
- // The MDC packet can be stripped by an attacker!
- Log.d(Constants.TAG, "MDC fail");
- if (!signatureResultBuilder.isValidSignature()) {
- log.add(LogType.MSG_DC_INSECURE_MDC_MISSING, indent);
- decryptionResultBuilder.setInsecure(true);
- }
- }
-
- updateProgress(R.string.progress_done, 100, 100);
-
- log.add(LogType.MSG_DC_OK, indent);
-
- // Return a positive result, with metadata and verification info
- DecryptVerifyResult result = new DecryptVerifyResult(DecryptVerifyResult.RESULT_OK, log);
- result.setCachedCryptoInputParcel(cryptoInput);
- result.setSignatureResult(signatureResultBuilder.build());
- result.setCharset(charset);
- result.setDecryptionResult(decryptionResultBuilder.build());
- result.setDecryptionMetadata(metadata);
return result;
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpHelper.java
index 32718fb4e..e8d1d3111 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpHelper.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpHelper.java
@@ -21,6 +21,7 @@ package org.sufficientlysecure.keychain.pgp;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager.NameNotFoundException;
+import android.support.annotation.NonNull;
import android.text.TextUtils;
import org.sufficientlysecure.keychain.Constants;
@@ -116,32 +117,27 @@ public class PgpHelper {
}
}
- public static String getPgpContent(CharSequence input) {
- // only decrypt if clipboard content is available and a pgp message or cleartext signature
- if (!TextUtils.isEmpty(input)) {
- Log.dEscaped(Constants.TAG, "input: " + input);
+ public static String getPgpContent(@NonNull CharSequence input) {
+ Log.dEscaped(Constants.TAG, "input: " + input);
+
+ Matcher matcher = PgpHelper.PGP_MESSAGE.matcher(input);
+ if (matcher.matches()) {
+ String text = matcher.group(1);
+ text = fixPgpMessage(text);
- Matcher matcher = PgpHelper.PGP_MESSAGE.matcher(input);
+ Log.dEscaped(Constants.TAG, "input fixed: " + text);
+ return text;
+ } else {
+ matcher = PgpHelper.PGP_CLEARTEXT_SIGNATURE.matcher(input);
if (matcher.matches()) {
String text = matcher.group(1);
- text = fixPgpMessage(text);
+ text = fixPgpCleartextSignature(text);
Log.dEscaped(Constants.TAG, "input fixed: " + text);
return text;
} else {
- matcher = PgpHelper.PGP_CLEARTEXT_SIGNATURE.matcher(input);
- if (matcher.matches()) {
- String text = matcher.group(1);
- text = fixPgpCleartextSignature(text);
-
- Log.dEscaped(Constants.TAG, "input fixed: " + text);
- return text;
- } else {
- return null;
- }
+ return null;
}
- } else {
- return null;
}
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSecurityConstants.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSecurityConstants.java
index 3fa549946..cbd8ce47a 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSecurityConstants.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSecurityConstants.java
@@ -23,6 +23,7 @@ import org.spongycastle.bcpg.HashAlgorithmTags;
import org.spongycastle.bcpg.PublicKeyAlgorithmTags;
import org.spongycastle.bcpg.SymmetricKeyAlgorithmTags;
+import java.util.Arrays;
import java.util.HashSet;
/**
@@ -42,24 +43,23 @@ public class PgpSecurityConstants {
* Whitelist of accepted symmetric encryption algorithms
* all other algorithms are rejected with OpenPgpDecryptionResult.RESULT_INSECURE
*/
- private static HashSet<Integer> sSymmetricAlgorithmsWhitelist = new HashSet<>();
- static {
- // General remarks: We try to keep the whitelist short to reduce attack surface
- // TODO: block IDEA?: Bad key schedule (weak keys), implementation difficulties (easy to make errors)
- sSymmetricAlgorithmsWhitelist.add(SymmetricKeyAlgorithmTags.IDEA);
- sSymmetricAlgorithmsWhitelist.add(SymmetricKeyAlgorithmTags.TRIPLE_DES); // a MUST in RFC
- sSymmetricAlgorithmsWhitelist.add(SymmetricKeyAlgorithmTags.CAST5); // default in many gpg, pgp versions, 128 bit key
- // BLOWFISH: Twofish is the successor
- // SAFER: not used widely
- // DES: < 128 bit security
- sSymmetricAlgorithmsWhitelist.add(SymmetricKeyAlgorithmTags.AES_128);
- sSymmetricAlgorithmsWhitelist.add(SymmetricKeyAlgorithmTags.AES_192);
- sSymmetricAlgorithmsWhitelist.add(SymmetricKeyAlgorithmTags.AES_256);
- sSymmetricAlgorithmsWhitelist.add(SymmetricKeyAlgorithmTags.TWOFISH); // 128 bit
- // CAMELLIA_128: not used widely
- // CAMELLIA_192: not used widely
- // CAMELLIA_256: not used widely
- }
+ private static HashSet<Integer> sSymmetricAlgorithmsWhitelist = new HashSet<>(Arrays.asList(
+ // General remarks: We try to keep the whitelist short to reduce attack surface
+ // TODO: block IDEA?: Bad key schedule (weak keys), implementation difficulties (easy to make errors)
+ SymmetricKeyAlgorithmTags.IDEA,
+ SymmetricKeyAlgorithmTags.TRIPLE_DES, // a MUST in RFC
+ SymmetricKeyAlgorithmTags.CAST5, // default in many gpg, pgp versions, 128 bit key
+ // BLOWFISH: Twofish is the successor
+ // SAFER: not used widely
+ // DES: < 128 bit security
+ SymmetricKeyAlgorithmTags.AES_128,
+ SymmetricKeyAlgorithmTags.AES_192,
+ SymmetricKeyAlgorithmTags.AES_256,
+ SymmetricKeyAlgorithmTags.TWOFISH // 128 bit
+ // CAMELLIA_128: not used widely
+ // CAMELLIA_192: not used widely
+ // CAMELLIA_256: not used widely
+ ));
public static boolean isSecureSymmetricAlgorithm(int id) {
return sSymmetricAlgorithmsWhitelist.contains(id);
@@ -77,20 +77,19 @@ public class PgpSecurityConstants {
* ((collision resistance of 112-bits))
* Implementations SHOULD NOT sign SHA-256 hashes. They MUST NOT default to signing SHA-256 hashes.
*/
- private static HashSet<Integer> sHashAlgorithmsWhitelist = new HashSet<>();
- static {
- // MD5: broken
- // SHA1: broken
- // RIPEMD160: same security properties as SHA1
- // DOUBLE_SHA: not used widely
- // MD2: not used widely
- // TIGER_192: not used widely
- // HAVAL_5_160: not used widely
- sHashAlgorithmsWhitelist.add(HashAlgorithmTags.SHA256); // compatibility for old Mailvelope versions
- sHashAlgorithmsWhitelist.add(HashAlgorithmTags.SHA384);
- sHashAlgorithmsWhitelist.add(HashAlgorithmTags.SHA512);
- // SHA224: Not used widely, Yahoo argues against it
- }
+ private static HashSet<Integer> sHashAlgorithmsWhitelist = new HashSet<>(Arrays.asList(
+ // MD5: broken
+ // SHA1: broken
+ // RIPEMD160: same security properties as SHA1
+ // DOUBLE_SHA: not used widely
+ // MD2: not used widely
+ // TIGER_192: not used widely
+ // HAVAL_5_160: not used widely
+ HashAlgorithmTags.SHA256, // compatibility for old Mailvelope versions
+ HashAlgorithmTags.SHA384,
+ HashAlgorithmTags.SHA512
+ // SHA224: Not used widely, Yahoo argues against it
+ ));
public static boolean isSecureHashAlgorithm(int id) {
return sHashAlgorithmsWhitelist.contains(id);
@@ -106,12 +105,11 @@ public class PgpSecurityConstants {
* bitlength less than 1023 bits.
* Implementations MUST NOT accept any RSA keys with bitlength less than 2047 bits after January 1, 2016.
*/
- private static HashSet<String> sCurveWhitelist = new HashSet<>();
- static {
- sCurveWhitelist.add(NISTNamedCurves.getOID("P-256").getId());
- sCurveWhitelist.add(NISTNamedCurves.getOID("P-384").getId());
- sCurveWhitelist.add(NISTNamedCurves.getOID("P-521").getId());
- }
+ private static HashSet<String> sCurveWhitelist = new HashSet<>(Arrays.asList(
+ NISTNamedCurves.getOID("P-256").getId(),
+ NISTNamedCurves.getOID("P-384").getId(),
+ NISTNamedCurves.getOID("P-521").getId()
+ ));
public static boolean isSecureKey(CanonicalizedPublicKey key) {
switch (key.getAlgorithm()) {
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedKeyRing.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedKeyRing.java
index a7baddf8b..ca98882d8 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedKeyRing.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedKeyRing.java
@@ -216,17 +216,6 @@ public class UncachedKeyRing implements Serializable {
}
- public boolean containsSubkey(String expectedFingerprint) {
- Iterator<PGPPublicKey> it = mRing.getPublicKeys();
- while (it.hasNext()) {
- if (KeyFormattingUtils.convertFingerprintToHex(
- it.next().getFingerprint()).equalsIgnoreCase(expectedFingerprint)) {
- return true;
- }
- }
- return false;
- }
-
public interface IteratorWithIOThrow<E> {
public boolean hasNext() throws IOException;
public E next() throws IOException;
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedPublicKey.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedPublicKey.java
index 26f046372..013a6bf14 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedPublicKey.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedPublicKey.java
@@ -368,4 +368,5 @@ public class UncachedPublicKey {
return calendar.getTime();
}
+
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/WrappedUserAttribute.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/WrappedUserAttribute.java
index 2c7f0187a..535314607 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/WrappedUserAttribute.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/WrappedUserAttribute.java
@@ -1,4 +1,8 @@
/*
+<<<<<<< HEAD
+ * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
+=======
+>>>>>>> development
* Copyright (C) 2014 Vincent Breitmoser <v.breitmoser@mugenguild.com>
*
* This program is free software: you can redistribute it and/or modify
@@ -37,6 +41,7 @@ public class WrappedUserAttribute implements Serializable {
public static final int UAT_NONE = 0;
public static final int UAT_IMAGE = UserAttributeSubpacketTags.IMAGE_ATTRIBUTE;
+ public static final int UAT_URI_ATTRIBUTE = 101;
private PGPUserAttributeSubpacketVector mVector;
@@ -77,7 +82,7 @@ public class WrappedUserAttribute implements Serializable {
public static WrappedUserAttribute fromData (byte[] data) throws IOException {
UserAttributeSubpacketInputStream in =
new UserAttributeSubpacketInputStream(new ByteArrayInputStream(data));
- ArrayList<UserAttributeSubpacket> list = new ArrayList<UserAttributeSubpacket>();
+ ArrayList<UserAttributeSubpacket> list = new ArrayList<>();
while (in.available() > 0) {
list.add(in.readPacket());
}
@@ -121,6 +126,7 @@ public class WrappedUserAttribute implements Serializable {
private void readObjectNoData() throws ObjectStreamException {
}
+ @SuppressWarnings("SimplifiableIfStatement")
@Override
public boolean equals(Object o) {
if (!WrappedUserAttribute.class.isInstance(o)) {
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 0d9a4ac16..ee28b5f36 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainContract.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainContract.java
@@ -51,6 +51,11 @@ public class KeychainContract {
String EXPIRY = "expiry";
}
+ interface UpdatedKeysColumns {
+ String MASTER_KEY_ID = "master_key_id"; // not a database id
+ String LAST_UPDATED = "last_updated"; // time since epoch in seconds
+ }
+
interface UserPacketsColumns {
String MASTER_KEY_ID = "master_key_id"; // foreign key to key_rings._ID
String TYPE = "type"; // not a database id
@@ -97,6 +102,8 @@ public class KeychainContract {
public static final String BASE_KEY_RINGS = "key_rings";
+ public static final String BASE_UPDATED_KEYS = "updated_keys";
+
public static final String PATH_UNIFIED = "unified";
public static final String PATH_FIND = "find";
@@ -106,6 +113,7 @@ public class KeychainContract {
public static final String PATH_PUBLIC = "public";
public static final String PATH_SECRET = "secret";
public static final String PATH_USER_IDS = "user_ids";
+ public static final String PATH_LINKED_IDS = "linked_ids";
public static final String PATH_KEYS = "keys";
public static final String PATH_CERTS = "certs";
@@ -234,6 +242,16 @@ public class KeychainContract {
}
+ public static class UpdatedKeys implements UpdatedKeysColumns, BaseColumns {
+ public static final Uri CONTENT_URI = BASE_CONTENT_URI_INTERNAL.buildUpon()
+ .appendPath(BASE_UPDATED_KEYS).build();
+
+ public static final String CONTENT_TYPE
+ = "vnd.android.cursor.dir/vnd.org.sufficientlysecure.keychain.provider.updated_keys";
+ public static final String CONTENT_ITEM_TYPE
+ = "vnd.android.cursor.item/vnd.org.sufficientlysecure.keychain.provider.updated_keys";
+ }
+
public static class UserPackets implements UserPacketsColumns, BaseColumns {
public static final String VERIFIED = "verified";
public static final Uri CONTENT_URI = BASE_CONTENT_URI_INTERNAL.buildUpon()
@@ -262,6 +280,11 @@ public class KeychainContract {
public static Uri buildUserIdsUri(Uri uri) {
return CONTENT_URI.buildUpon().appendPath(uri.getPathSegments().get(1)).appendPath(PATH_USER_IDS).build();
}
+
+ public static Uri buildLinkedIdsUri(Uri uri) {
+ return CONTENT_URI.buildUpon().appendPath(uri.getPathSegments().get(1)).appendPath(PATH_LINKED_IDS).build();
+ }
+
}
public static class ApiApps implements ApiAppsColumns, BaseColumns {
@@ -350,7 +373,14 @@ public class KeychainContract {
}
public static Uri buildCertsUri(Uri uri) {
- return CONTENT_URI.buildUpon().appendPath(uri.getPathSegments().get(1)).appendPath(PATH_CERTS).build();
+ return CONTENT_URI.buildUpon().appendPath(uri.getPathSegments().get(1))
+ .appendPath(PATH_CERTS).build();
+ }
+
+ public static Uri buildLinkedIdCertsUri(Uri uri, int rank) {
+ return CONTENT_URI.buildUpon().appendPath(uri.getPathSegments().get(1))
+ .appendPath(PATH_LINKED_IDS).appendPath(Integer.toString(rank))
+ .appendPath(PATH_CERTS).build();
}
}
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 4d16d44c5..d7fb738fc 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainDatabase.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainDatabase.java
@@ -34,6 +34,7 @@ import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAppsColumns;
import org.sufficientlysecure.keychain.provider.KeychainContract.CertsColumns;
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRingsColumns;
import org.sufficientlysecure.keychain.provider.KeychainContract.KeysColumns;
+import org.sufficientlysecure.keychain.provider.KeychainContract.UpdatedKeysColumns;
import org.sufficientlysecure.keychain.provider.KeychainContract.UserPacketsColumns;
import org.sufficientlysecure.keychain.ui.ConsolidateDialogActivity;
import org.sufficientlysecure.keychain.util.Log;
@@ -53,7 +54,7 @@ import java.io.IOException;
*/
public class KeychainDatabase extends SQLiteOpenHelper {
private static final String DATABASE_NAME = "openkeychain.db";
- private static final int DATABASE_VERSION = 11;
+ private static final int DATABASE_VERSION = 12;
static Boolean apgHack = false;
private Context mContext;
@@ -61,6 +62,7 @@ public class KeychainDatabase extends SQLiteOpenHelper {
String KEY_RINGS_PUBLIC = "keyrings_public";
String KEY_RINGS_SECRET = "keyrings_secret";
String KEYS = "keys";
+ String UPDATED_KEYS = "updated_keys";
String USER_PACKETS = "user_packets";
String CERTS = "certs";
String API_APPS = "api_apps";
@@ -144,6 +146,14 @@ public class KeychainDatabase extends SQLiteOpenHelper {
+ Tables.USER_PACKETS + "(" + UserPacketsColumns.MASTER_KEY_ID + ", " + UserPacketsColumns.RANK + ") ON DELETE CASCADE"
+ ")";
+ private static final String CREATE_UPDATE_KEYS =
+ "CREATE TABLE IF NOT EXISTS " + Tables.UPDATED_KEYS + " ("
+ + UpdatedKeysColumns.MASTER_KEY_ID + " INTEGER PRIMARY KEY, "
+ + UpdatedKeysColumns.LAST_UPDATED + " INTEGER, "
+ + "FOREIGN KEY(" + UpdatedKeysColumns.MASTER_KEY_ID + ") REFERENCES "
+ + Tables.KEY_RINGS_PUBLIC + "(" + KeyRingsColumns.MASTER_KEY_ID + ") ON DELETE CASCADE"
+ + ")";
+
private static final String CREATE_API_APPS =
"CREATE TABLE IF NOT EXISTS " + Tables.API_APPS + " ("
+ BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
@@ -206,6 +216,7 @@ public class KeychainDatabase extends SQLiteOpenHelper {
db.execSQL(CREATE_KEYS);
db.execSQL(CREATE_USER_PACKETS);
db.execSQL(CREATE_CERTS);
+ db.execSQL(CREATE_UPDATE_KEYS);
db.execSQL(CREATE_API_APPS);
db.execSQL(CREATE_API_APPS_ACCOUNTS);
db.execSQL(CREATE_API_APPS_ALLOWED_KEYS);
@@ -278,8 +289,11 @@ public class KeychainDatabase extends SQLiteOpenHelper {
// fix problems in database, see #1402 for details
// https://github.com/open-keychain/open-keychain/issues/1402
db.execSQL("DELETE FROM api_accounts WHERE key_id BETWEEN 0 AND 3");
+ case 12:
+ db.execSQL(CREATE_UPDATE_KEYS);
if (oldVersion == 10) {
- // no consolidate if we are updating from 10, we're just here for the api_accounts fix
+ // no consolidate if we are updating from 10, we're just here for
+ // the api_accounts fix and the new update keys table
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 3ed9b1da9..d722fa9e7 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java
@@ -31,6 +31,7 @@ import android.net.Uri;
import android.text.TextUtils;
import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.pgp.WrappedUserAttribute;
import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAccounts;
import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAllowedKeys;
import org.sufficientlysecure.keychain.provider.KeychainContract.ApiApps;
@@ -38,6 +39,7 @@ import org.sufficientlysecure.keychain.provider.KeychainContract.Certs;
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRingData;
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
import org.sufficientlysecure.keychain.provider.KeychainContract.Keys;
+import org.sufficientlysecure.keychain.provider.KeychainContract.UpdatedKeys;
import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets;
import org.sufficientlysecure.keychain.provider.KeychainContract.UserPacketsColumns;
import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables;
@@ -62,6 +64,8 @@ public class KeychainProvider extends ContentProvider {
private static final int KEY_RING_SECRET = 204;
private static final int KEY_RING_CERTS = 205;
private static final int KEY_RING_CERTS_SPECIFIC = 206;
+ private static final int KEY_RING_LINKED_IDS = 207;
+ private static final int KEY_RING_LINKED_ID_CERTS = 208;
private static final int API_APPS = 301;
private static final int API_APPS_BY_PACKAGE_NAME = 302;
@@ -72,6 +76,9 @@ public class KeychainProvider extends ContentProvider {
private static final int KEY_RINGS_FIND_BY_EMAIL = 400;
private static final int KEY_RINGS_FIND_BY_SUBKEY = 401;
+ private static final int UPDATED_KEYS = 500;
+ private static final int UPDATED_KEYS_SPECIFIC = 501;
+
protected UriMatcher mUriMatcher;
/**
@@ -127,6 +134,9 @@ public class KeychainProvider extends ContentProvider {
* key_rings/_/unified
* key_rings/_/keys
* key_rings/_/user_ids
+ * key_rings/_/linked_ids
+ * key_rings/_/linked_ids/_
+ * key_rings/_/linked_ids/_/certs
* key_rings/_/public
* key_rings/_/secret
* key_rings/_/certs
@@ -143,6 +153,13 @@ public class KeychainProvider extends ContentProvider {
+ KeychainContract.PATH_USER_IDS,
KEY_RING_USER_IDS);
matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/*/"
+ + KeychainContract.PATH_LINKED_IDS,
+ KEY_RING_LINKED_IDS);
+ matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/*/"
+ + KeychainContract.PATH_LINKED_IDS + "/*/"
+ + KeychainContract.PATH_CERTS,
+ KEY_RING_LINKED_ID_CERTS);
+ matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/*/"
+ KeychainContract.PATH_PUBLIC,
KEY_RING_PUBLIC);
matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/*/"
@@ -179,6 +196,12 @@ public class KeychainProvider extends ContentProvider {
matcher.addURI(authority, KeychainContract.BASE_API_APPS + "/*/"
+ KeychainContract.PATH_ALLOWED_KEYS, API_ALLOWED_KEYS);
+ /**
+ * to access table containing last updated dates of keys
+ */
+ matcher.addURI(authority, KeychainContract.BASE_UPDATED_KEYS, UPDATED_KEYS);
+ matcher.addURI(authority, KeychainContract.BASE_UPDATED_KEYS + "/*", UPDATED_KEYS_SPECIFIC);
+
return matcher;
}
@@ -218,6 +241,11 @@ public class KeychainProvider extends ContentProvider {
case KEY_RING_SECRET:
return KeyRings.CONTENT_ITEM_TYPE;
+ case UPDATED_KEYS:
+ return UpdatedKeys.CONTENT_TYPE;
+ case UPDATED_KEYS_SPECIFIC:
+ return UpdatedKeys.CONTENT_ITEM_TYPE;
+
case API_APPS:
return ApiApps.CONTENT_TYPE;
@@ -477,7 +505,8 @@ public class KeychainProvider extends ContentProvider {
}
case KEY_RINGS_USER_IDS:
- case KEY_RING_USER_IDS: {
+ case KEY_RING_USER_IDS:
+ case KEY_RING_LINKED_IDS: {
HashMap<String, String> projectionMap = new HashMap<>();
projectionMap.put(UserPackets._ID, Tables.USER_PACKETS + ".oid AS _id");
projectionMap.put(UserPackets.MASTER_KEY_ID, Tables.USER_PACKETS + "." + UserPackets.MASTER_KEY_ID);
@@ -502,13 +531,15 @@ public class KeychainProvider extends ContentProvider {
groupBy = Tables.USER_PACKETS + "." + UserPackets.MASTER_KEY_ID
+ ", " + Tables.USER_PACKETS + "." + UserPackets.RANK;
- // for now, we only respect user ids here, so TYPE must be NULL
- // TODO expand with KEY_RING_USER_PACKETS query type which lifts this restriction
- qb.appendWhere(Tables.USER_PACKETS + "." + UserPackets.TYPE + " IS NULL");
+ if (match == KEY_RING_LINKED_IDS) {
+ qb.appendWhere(Tables.USER_PACKETS + "." + UserPackets.TYPE + " = "
+ + WrappedUserAttribute.UAT_URI_ATTRIBUTE);
+ } else {
+ qb.appendWhere(Tables.USER_PACKETS + "." + UserPackets.TYPE + " IS NULL");
+ }
// If we are searching for a particular keyring's ids, add where
- if (match == KEY_RING_USER_IDS) {
- // TODO remove with the thing above
+ if (match == KEY_RING_USER_IDS || match == KEY_RING_LINKED_IDS) {
qb.appendWhere(" AND ");
qb.appendWhere(Tables.USER_PACKETS + "." + UserPackets.MASTER_KEY_ID + " = ");
qb.appendWhereEscapeString(uri.getPathSegments().get(1));
@@ -559,7 +590,8 @@ public class KeychainProvider extends ContentProvider {
}
case KEY_RING_CERTS:
- case KEY_RING_CERTS_SPECIFIC: {
+ case KEY_RING_CERTS_SPECIFIC:
+ case KEY_RING_LINKED_ID_CERTS: {
HashMap<String, String> projectionMap = new HashMap<>();
projectionMap.put(Certs._ID, Tables.CERTS + ".oid AS " + Certs._ID);
projectionMap.put(Certs.MASTER_KEY_ID, Tables.CERTS + "." + Certs.MASTER_KEY_ID);
@@ -580,10 +612,6 @@ public class KeychainProvider extends ContentProvider {
+ " AND "
+ Tables.CERTS + "." + Certs.RANK + " = "
+ Tables.USER_PACKETS + "." + UserPackets.RANK
- // for now, we only return user ids here, so TYPE must be NULL
- // TODO at some point, we should lift this restriction
- + " AND "
- + Tables.USER_PACKETS + "." + UserPackets.TYPE + " IS NULL"
+ ") LEFT JOIN " + Tables.USER_PACKETS + " AS signer ON ("
+ Tables.CERTS + "." + Certs.KEY_ID_CERTIFIER + " = "
+ "signer." + UserPackets.MASTER_KEY_ID
@@ -603,6 +631,33 @@ public class KeychainProvider extends ContentProvider {
qb.appendWhereEscapeString(uri.getPathSegments().get(4));
}
+ if (match == KEY_RING_LINKED_ID_CERTS) {
+ qb.appendWhere(" AND " + Tables.USER_PACKETS + "."
+ + UserPackets.TYPE + " IS NOT NULL");
+
+ qb.appendWhere(" AND " + Tables.USER_PACKETS + "."
+ + UserPackets.RANK + " = ");
+ qb.appendWhereEscapeString(uri.getPathSegments().get(3));
+ } else {
+ qb.appendWhere(" AND " + Tables.USER_PACKETS + "." + UserPackets.TYPE + " IS NULL");
+ }
+
+ break;
+ }
+
+ case UPDATED_KEYS:
+ case UPDATED_KEYS_SPECIFIC: {
+ HashMap<String, String> 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);
+ qb.setProjectionMap(projectionMap);
+ if (match == UPDATED_KEYS_SPECIFIC) {
+ qb.appendWhere(UpdatedKeys.MASTER_KEY_ID + " = ");
+ qb.appendWhereEscapeString(uri.getPathSegments().get(1));
+ }
break;
}
@@ -726,6 +781,12 @@ public class KeychainProvider extends ContentProvider {
keyId = values.getAsLong(Certs.MASTER_KEY_ID);
break;
}
+ case UPDATED_KEYS: {
+ long updatedKeyId = db.replace(Tables.UPDATED_KEYS, null, values);
+ rowUri = UpdatedKeys.CONTENT_URI.buildUpon().appendPath("" + updatedKeyId)
+ .build();
+ break;
+ }
case API_APPS: {
db.insertOrThrow(Tables.API_APPS, null, values);
break;
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/ProviderHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/ProviderHelper.java
index 0c37bfc2a..a6823d3ac 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/ProviderHelper.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/ProviderHelper.java
@@ -32,10 +32,15 @@ import android.support.v4.util.LongSparseArray;
import org.spongycastle.bcpg.CompressionAlgorithmTags;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.operations.results.ImportKeyResult;
+import org.sufficientlysecure.keychain.pgp.WrappedUserAttribute;
+import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets;
+import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
+import org.sufficientlysecure.keychain.util.ParcelableFileCache.IteratorWithSize;
+import org.sufficientlysecure.keychain.util.Preferences;
import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing;
import org.sufficientlysecure.keychain.operations.ImportOperation;
import org.sufficientlysecure.keychain.operations.results.ConsolidateResult;
-import org.sufficientlysecure.keychain.operations.results.ImportKeyResult;
import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType;
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
import org.sufficientlysecure.keychain.operations.results.SaveKeyringResult;
@@ -50,7 +55,6 @@ import org.sufficientlysecure.keychain.pgp.Progressable;
import org.sufficientlysecure.keychain.pgp.UncachedKeyRing;
import org.sufficientlysecure.keychain.pgp.UncachedPublicKey;
import org.sufficientlysecure.keychain.pgp.WrappedSignature;
-import org.sufficientlysecure.keychain.pgp.WrappedUserAttribute;
import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAllowedKeys;
import org.sufficientlysecure.keychain.provider.KeychainContract.ApiApps;
@@ -59,14 +63,12 @@ import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRingData;
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
import org.sufficientlysecure.keychain.provider.KeychainContract.Keys;
import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets;
+import org.sufficientlysecure.keychain.provider.KeychainContract.UpdatedKeys;
import org.sufficientlysecure.keychain.remote.AccountSettings;
import org.sufficientlysecure.keychain.remote.AppSettings;
-import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
import org.sufficientlysecure.keychain.util.IterableIterator;
import org.sufficientlysecure.keychain.util.Log;
import org.sufficientlysecure.keychain.util.ParcelableFileCache;
-import org.sufficientlysecure.keychain.util.ParcelableFileCache.IteratorWithSize;
-import org.sufficientlysecure.keychain.util.Preferences;
import org.sufficientlysecure.keychain.util.ProgressFixedScaler;
import org.sufficientlysecure.keychain.util.ProgressScaler;
import org.sufficientlysecure.keychain.util.Utf8Util;
@@ -82,6 +84,7 @@ import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
+import java.util.concurrent.TimeUnit;
/**
* This class contains high level methods for database access. Despite its
@@ -685,6 +688,36 @@ public class ProviderHelper {
mIndent -= 1;
}
+ // before deleting key, retrieve it's last updated time
+ final int INDEX_MASTER_KEY_ID = 0;
+ final int INDEX_LAST_UPDATED = 1;
+ Cursor lastUpdatedCursor = mContentResolver.query(
+ UpdatedKeys.CONTENT_URI,
+ new String[]{
+ UpdatedKeys.MASTER_KEY_ID,
+ UpdatedKeys.LAST_UPDATED
+ },
+ UpdatedKeys.MASTER_KEY_ID + " = ?",
+ new String[]{"" + masterKeyId},
+ null
+ );
+ if (lastUpdatedCursor.moveToNext()) {
+ // there was an entry to re-insert
+ // this operation must happen after the new key is inserted
+ ContentValues lastUpdatedEntry = new ContentValues(2);
+ lastUpdatedEntry.put(UpdatedKeys.MASTER_KEY_ID,
+ lastUpdatedCursor.getLong(INDEX_MASTER_KEY_ID));
+ lastUpdatedEntry.put(UpdatedKeys.LAST_UPDATED,
+ lastUpdatedCursor.getLong(INDEX_LAST_UPDATED));
+ operations.add(
+ ContentProviderOperation
+ .newInsert(UpdatedKeys.CONTENT_URI)
+ .withValues(lastUpdatedEntry)
+ .build()
+ );
+ }
+ lastUpdatedCursor.close();
+
try {
// delete old version of this keyRing, which also deletes all keys and userIds on cascade
int deleted = mContentResolver.delete(
@@ -738,6 +771,11 @@ public class ProviderHelper {
if (type != o.type) {
return type == null ? -1 : 1;
}
+ // if one is *trusted* but the other isn't, that one comes first
+ // this overrides the primary attribute, even!
+ if ( (trustedCerts.size() == 0) != (o.trustedCerts.size() == 0) ) {
+ return trustedCerts.size() > o.trustedCerts.size() ? -1 : 1;
+ }
// if one key is primary but the other isn't, the primary one always comes first
if (isPrimary != o.isPrimary) {
return isPrimary ? -1 : 1;
@@ -845,7 +883,7 @@ public class ProviderHelper {
}
public SaveKeyringResult savePublicKeyRing(UncachedKeyRing keyRing) {
- return savePublicKeyRing(keyRing, new ProgressScaler());
+ return savePublicKeyRing(keyRing, new ProgressScaler(), null);
}
/**
@@ -854,7 +892,7 @@ public class ProviderHelper {
* This is a high level method, which takes care of merging all new information into the old and
* keep public and secret keyrings in sync.
*/
- public SaveKeyringResult savePublicKeyRing(UncachedKeyRing publicRing, Progressable progress) {
+ public SaveKeyringResult savePublicKeyRing(UncachedKeyRing publicRing, Progressable progress, String expectedFingerprint) {
try {
long masterKeyId = publicRing.getMasterKeyId();
@@ -927,6 +965,17 @@ public class ProviderHelper {
canSecretRing = null;
}
+
+ // If we have an expected fingerprint, make sure it matches
+ if (expectedFingerprint != null) {
+ if (!canPublicRing.containsSubkey(expectedFingerprint)) {
+ log(LogType.MSG_IP_FINGERPRINT_ERROR);
+ return new SaveKeyringResult(SaveKeyringResult.RESULT_ERROR, mLog, null);
+ } else {
+ log(LogType.MSG_IP_FINGERPRINT_OK);
+ }
+ }
+
int result = saveCanonicalizedPublicKeyRing(canPublicRing, progress, canSecretRing != null);
// Save the saved keyring (if any)
@@ -1239,6 +1288,28 @@ public class ProviderHelper {
}
// 2. wipe database (IT'S DANGEROUS)
+
+ // first, backup our list of updated key times
+ ArrayList<ContentValues> updatedKeysValues = new ArrayList<>();
+ final int INDEX_MASTER_KEY_ID = 0;
+ final int INDEX_LAST_UPDATED = 1;
+ Cursor lastUpdatedCursor = mContentResolver.query(
+ UpdatedKeys.CONTENT_URI,
+ new String[]{
+ UpdatedKeys.MASTER_KEY_ID,
+ UpdatedKeys.LAST_UPDATED
+ },
+ null, null, null);
+ while (lastUpdatedCursor.moveToNext()) {
+ ContentValues values = new ContentValues();
+ values.put(UpdatedKeys.MASTER_KEY_ID,
+ lastUpdatedCursor.getLong(INDEX_MASTER_KEY_ID));
+ values.put(UpdatedKeys.LAST_UPDATED,
+ lastUpdatedCursor.getLong(INDEX_LAST_UPDATED));
+ updatedKeysValues.add(values);
+ }
+ lastUpdatedCursor.close();
+
log.add(LogType.MSG_CON_DB_CLEAR, indent);
mContentResolver.delete(KeyRings.buildUnifiedKeyRingsUri(), null, null);
@@ -1288,6 +1359,10 @@ public class ProviderHelper {
new ProgressFixedScaler(progress, 25, 99, 100, R.string.progress_con_reimport))
.serialKeyRingImport(itPublics, numPublics, null, null);
log.add(result, indent);
+ // re-insert our backed up list of updated key times
+ // TODO: can this cause issues in case a public key re-import failed?
+ mContentResolver.bulkInsert(UpdatedKeys.CONTENT_URI,
+ updatedKeysValues.toArray(new ContentValues[updatedKeysValues.size()]));
} else {
log.add(LogType.MSG_CON_REIMPORT_PUBLIC_SKIP, indent);
}
@@ -1397,6 +1472,14 @@ public class ProviderHelper {
return getKeyRingAsArmoredString(data);
}
+ public Uri renewKeyLastUpdatedTime(long masterKeyId, long time, TimeUnit timeUnit) {
+ ContentValues values = new ContentValues();
+ values.put(UpdatedKeys.MASTER_KEY_ID, masterKeyId);
+ values.put(UpdatedKeys.LAST_UPDATED, timeUnit.toSeconds(time));
+
+ return mContentResolver.insert(UpdatedKeys.CONTENT_URI, values);
+ }
+
public ArrayList<String> getRegisteredApiApps() {
Cursor cursor = mContentResolver.query(ApiApps.CONTENT_URI, null, null, null, null);
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService.java
index ea016c657..57dd068ef 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService.java
@@ -25,6 +25,7 @@ import android.net.Uri;
import android.os.IBinder;
import android.os.ParcelFileDescriptor;
import android.os.Parcelable;
+import android.support.annotation.Nullable;
import android.text.TextUtils;
import org.openintents.openpgp.IOpenPgpService;
@@ -38,7 +39,7 @@ import org.sufficientlysecure.keychain.operations.results.DecryptVerifyResult;
import org.sufficientlysecure.keychain.operations.results.OperationResult.LogEntryParcel;
import org.sufficientlysecure.keychain.operations.results.PgpSignEncryptResult;
import org.sufficientlysecure.keychain.pgp.PgpSecurityConstants;
-import org.sufficientlysecure.keychain.pgp.PgpDecryptVerify;
+import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyOperation;
import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyInputParcel;
import org.sufficientlysecure.keychain.pgp.PgpSignEncryptInputParcel;
import org.sufficientlysecure.keychain.pgp.PgpSignEncryptOperation;
@@ -65,8 +66,10 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
+import java.util.List;
public class OpenPgpService extends RemoteService {
@@ -533,7 +536,7 @@ public class OpenPgpService extends RemoteService {
byte[] detachedSignature = data.getByteArrayExtra(OpenPgpApi.EXTRA_DETACHED_SIGNATURE);
- PgpDecryptVerify op = new PgpDecryptVerify(this, mProviderHelper, null);
+ PgpDecryptVerifyOperation op = new PgpDecryptVerifyOperation(this, mProviderHelper, null);
long inputLength = inputStream.available();
InputData inputData = new InputData(inputStream, inputLength);
@@ -715,28 +718,40 @@ public class OpenPgpService extends RemoteService {
}
private Intent getSignKeyIdImpl(Intent data) {
- String preferredUserId = data.getStringExtra(OpenPgpApi.EXTRA_USER_ID);
+ // if data already contains EXTRA_SIGN_KEY_ID, it has been executed again
+ // after user interaction. Then, we just need to return the long again!
+ if (data.hasExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID)) {
+ long signKeyId = data.getLongExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID,
+ Constants.key.none);
- Intent intent = new Intent(getBaseContext(), SelectSignKeyIdActivity.class);
- String currentPkg = getCurrentCallingPackage();
- intent.setData(KeychainContract.ApiApps.buildByPackageNameUri(currentPkg));
- intent.putExtra(SelectSignKeyIdActivity.EXTRA_USER_ID, preferredUserId);
- intent.putExtra(SelectSignKeyIdActivity.EXTRA_DATA, data);
+ Intent result = new Intent();
+ result.putExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, signKeyId);
+ result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS);
+ return result;
+ } else {
+ String preferredUserId = data.getStringExtra(OpenPgpApi.EXTRA_USER_ID);
- PendingIntent pi = PendingIntent.getActivity(getBaseContext(), 0,
- intent,
- PendingIntent.FLAG_CANCEL_CURRENT);
+ Intent intent = new Intent(getBaseContext(), SelectSignKeyIdActivity.class);
+ String currentPkg = getCurrentCallingPackage();
+ intent.setData(KeychainContract.ApiApps.buildByPackageNameUri(currentPkg));
+ intent.putExtra(SelectSignKeyIdActivity.EXTRA_USER_ID, preferredUserId);
+ intent.putExtra(SelectSignKeyIdActivity.EXTRA_DATA, data);
- // return PendingIntent to be executed by client
- Intent result = new Intent();
- result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED);
- result.putExtra(OpenPgpApi.RESULT_INTENT, pi);
+ PendingIntent pi = PendingIntent.getActivity(getBaseContext(), 0,
+ intent,
+ PendingIntent.FLAG_CANCEL_CURRENT);
+
+ // return PendingIntent to be executed by client
+ Intent result = new Intent();
+ result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED);
+ result.putExtra(OpenPgpApi.RESULT_INTENT, pi);
- return result;
+ return result;
+ }
}
private Intent getKeyIdsImpl(Intent data) {
- // if data already contains key ids extra GET_KEY_IDS has been executed again
+ // if data already contains EXTRA_KEY_IDS, it has been executed again
// after user interaction. Then, we just need to return the array again!
if (data.hasExtra(OpenPgpApi.EXTRA_KEY_IDS)) {
long[] keyIdsArray = data.getLongArrayExtra(OpenPgpApi.EXTRA_KEY_IDS);
@@ -800,19 +815,14 @@ public class OpenPgpService extends RemoteService {
}
// version code is required and needs to correspond to version code of service!
- // History of versions in org.openintents.openpgp.util.OpenPgpApi
- // we support 3, 4, 5, 6
- if (data.getIntExtra(OpenPgpApi.EXTRA_API_VERSION, -1) != 3
- && data.getIntExtra(OpenPgpApi.EXTRA_API_VERSION, -1) != 4
- && data.getIntExtra(OpenPgpApi.EXTRA_API_VERSION, -1) != 5
- && data.getIntExtra(OpenPgpApi.EXTRA_API_VERSION, -1) != 6
- && data.getIntExtra(OpenPgpApi.EXTRA_API_VERSION, -1) != 7
- && data.getIntExtra(OpenPgpApi.EXTRA_API_VERSION, -1) != 8) {
+ // History of versions in openpgp-api's CHANGELOG.md
+ List<Integer> supportedVersions = Arrays.asList(3, 4, 5, 6, 7, 8, 9);
+ if (!supportedVersions.contains(data.getIntExtra(OpenPgpApi.EXTRA_API_VERSION, -1))) {
Intent result = new Intent();
OpenPgpError error = new OpenPgpError
(OpenPgpError.INCOMPATIBLE_API_VERSIONS, "Incompatible API versions!\n"
+ "used API version: " + data.getIntExtra(OpenPgpApi.EXTRA_API_VERSION, -1) + "\n"
- + "supported API versions: 3-8");
+ + "supported API versions: " + supportedVersions.toString());
result.putExtra(OpenPgpApi.RESULT_ERROR, error);
result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR);
return result;
@@ -830,67 +840,8 @@ public class OpenPgpService extends RemoteService {
private final IOpenPgpService.Stub mBinder = new IOpenPgpService.Stub() {
@Override
public Intent execute(Intent data, ParcelFileDescriptor input, ParcelFileDescriptor output) {
- try {
- Intent errorResult = checkRequirements(data);
- if (errorResult != null) {
- return errorResult;
- }
-
- String action = data.getAction();
- switch (action) {
- case OpenPgpApi.ACTION_CLEARTEXT_SIGN: {
- return signImpl(data, input, output, true);
- }
- case OpenPgpApi.ACTION_SIGN: {
- // DEPRECATED: same as ACTION_CLEARTEXT_SIGN
- Log.w(Constants.TAG, "You are using a deprecated API call, please use ACTION_CLEARTEXT_SIGN instead of ACTION_SIGN!");
- return signImpl(data, input, output, true);
- }
- case OpenPgpApi.ACTION_DETACHED_SIGN: {
- return signImpl(data, input, output, false);
- }
- case OpenPgpApi.ACTION_ENCRYPT: {
- return encryptAndSignImpl(data, input, output, false);
- }
- case OpenPgpApi.ACTION_SIGN_AND_ENCRYPT: {
- return encryptAndSignImpl(data, input, output, true);
- }
- case OpenPgpApi.ACTION_DECRYPT_VERIFY: {
- return decryptAndVerifyImpl(data, input, output, false);
- }
- case OpenPgpApi.ACTION_DECRYPT_METADATA: {
- return decryptAndVerifyImpl(data, input, output, true);
- }
- case OpenPgpApi.ACTION_GET_SIGN_KEY_ID: {
- return getSignKeyIdImpl(data);
- }
- case OpenPgpApi.ACTION_GET_KEY_IDS: {
- return getKeyIdsImpl(data);
- }
- case OpenPgpApi.ACTION_GET_KEY: {
- return getKeyImpl(data);
- }
- default: {
- return null;
- }
- }
- } finally {
- // always close input and output file descriptors even in error cases
- if (input != null) {
- try {
- input.close();
- } catch (IOException e) {
- Log.e(Constants.TAG, "IOException when closing input ParcelFileDescriptor", e);
- }
- }
- if (output != null) {
- try {
- output.close();
- } catch (IOException e) {
- Log.e(Constants.TAG, "IOException when closing output ParcelFileDescriptor", e);
- }
- }
- }
+ Log.w(Constants.TAG, "You are using a deprecated service which may lead to truncated data on return, please use IOpenPgpService2!");
+ return executeInternal(data, input, output);
}
};
@@ -900,4 +851,68 @@ public class OpenPgpService extends RemoteService {
return mBinder;
}
+
+ protected Intent executeInternal(Intent data, ParcelFileDescriptor input, ParcelFileDescriptor output) {
+ try {
+ Intent errorResult = checkRequirements(data);
+ if (errorResult != null) {
+ return errorResult;
+ }
+
+ String action = data.getAction();
+ switch (action) {
+ case OpenPgpApi.ACTION_CLEARTEXT_SIGN: {
+ return signImpl(data, input, output, true);
+ }
+ case OpenPgpApi.ACTION_SIGN: {
+ // DEPRECATED: same as ACTION_CLEARTEXT_SIGN
+ Log.w(Constants.TAG, "You are using a deprecated API call, please use ACTION_CLEARTEXT_SIGN instead of ACTION_SIGN!");
+ return signImpl(data, input, output, true);
+ }
+ case OpenPgpApi.ACTION_DETACHED_SIGN: {
+ return signImpl(data, input, output, false);
+ }
+ case OpenPgpApi.ACTION_ENCRYPT: {
+ return encryptAndSignImpl(data, input, output, false);
+ }
+ case OpenPgpApi.ACTION_SIGN_AND_ENCRYPT: {
+ return encryptAndSignImpl(data, input, output, true);
+ }
+ case OpenPgpApi.ACTION_DECRYPT_VERIFY: {
+ return decryptAndVerifyImpl(data, input, output, false);
+ }
+ case OpenPgpApi.ACTION_DECRYPT_METADATA: {
+ return decryptAndVerifyImpl(data, input, output, true);
+ }
+ case OpenPgpApi.ACTION_GET_SIGN_KEY_ID: {
+ return getSignKeyIdImpl(data);
+ }
+ case OpenPgpApi.ACTION_GET_KEY_IDS: {
+ return getKeyIdsImpl(data);
+ }
+ case OpenPgpApi.ACTION_GET_KEY: {
+ return getKeyImpl(data);
+ }
+ default: {
+ return null;
+ }
+ }
+ } finally {
+ // always close input and output file descriptors even in error cases
+ if (input != null) {
+ try {
+ input.close();
+ } catch (IOException e) {
+ Log.e(Constants.TAG, "IOException when closing input ParcelFileDescriptor", e);
+ }
+ }
+ if (output != null) {
+ try {
+ output.close();
+ } catch (IOException e) {
+ Log.e(Constants.TAG, "IOException when closing output ParcelFileDescriptor", e);
+ }
+ }
+ }
+ }
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService2.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService2.java
new file mode 100644
index 000000000..110302e55
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService2.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2015 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.remote;
+
+import android.content.Intent;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.ParcelFileDescriptor;
+
+import org.openintents.openpgp.IOpenPgpService2;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.util.Log;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+public class OpenPgpService2 extends OpenPgpService {
+
+ private Map<Long, ParcelFileDescriptor> mOutputPipeMap = new HashMap<Long, ParcelFileDescriptor>();
+
+ private long createKey(int id) {
+ int callingPid = Binder.getCallingPid();
+ return ((long) callingPid << 32) | ((long) id & 0xFFFFFFFL);
+ }
+
+ private final IOpenPgpService2.Stub mBinder = new IOpenPgpService2.Stub() {
+
+ @Override
+ public ParcelFileDescriptor createOutputPipe(int outputPipeId) {
+ try {
+ ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
+ mOutputPipeMap.put(createKey(outputPipeId), pipe[1]);
+ return pipe[0];
+ } catch (IOException e) {
+ Log.e(Constants.TAG, "IOException in OpenPgpService2", e);
+ return null;
+ }
+
+ }
+
+ @Override
+ public Intent execute(Intent data, ParcelFileDescriptor input, int outputPipeId) {
+ long key = createKey(outputPipeId);
+ ParcelFileDescriptor output = mOutputPipeMap.get(key);
+ mOutputPipeMap.remove(key);
+ return executeInternal(data, input, output);
+ }
+
+ };
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mBinder;
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppsListFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppsListFragment.java
index 2deb33a67..a1451fb09 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppsListFragment.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppsListFragment.java
@@ -243,7 +243,7 @@ public class AppsListFragment extends ListFragment implements
null,
isInstalled(packageName),
1, // registered!
- R.drawable.ic_launcher // icon is retrieved later
+ R.mipmap.ic_launcher // icon is retrieved later
});
break;
}
@@ -265,7 +265,7 @@ public class AppsListFragment extends ListFragment implements
name,
isInstalled(packageName),
1, // registered!
- R.drawable.ic_launcher // icon is retrieved later
+ R.mipmap.ic_launcher // icon is retrieved later
});
break;
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/SelectSignKeyIdActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/SelectSignKeyIdActivity.java
index cb9f46f7f..bed49a6f6 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/SelectSignKeyIdActivity.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/SelectSignKeyIdActivity.java
@@ -40,14 +40,9 @@ public class SelectSignKeyIdActivity extends BaseActivity {
private static final int REQUEST_CODE_CREATE_KEY = 0x00008884;
- private Uri mAppUri;
private String mPreferredUserId;
private Intent mData;
- private SelectSignKeyIdListFragment mListFragment;
- private TextView mActionCreateKey;
- private TextView mNone;
-
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -62,15 +57,15 @@ public class SelectSignKeyIdActivity extends BaseActivity {
}
});
- mActionCreateKey = (TextView) findViewById(R.id.api_select_sign_key_create_key);
- mActionCreateKey.setOnClickListener(new View.OnClickListener() {
+ TextView createKeyButton = (TextView) findViewById(R.id.api_select_sign_key_create_key);
+ createKeyButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
createKey(mPreferredUserId);
}
});
- mNone = (TextView) findViewById(R.id.api_select_sign_key_none);
- mNone.setOnClickListener(new View.OnClickListener() {
+ TextView noneButton = (TextView) findViewById(R.id.api_select_sign_key_none);
+ noneButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 0 is "none"
@@ -82,16 +77,16 @@ public class SelectSignKeyIdActivity extends BaseActivity {
});
Intent intent = getIntent();
- mAppUri = intent.getData();
+ Uri appUri = intent.getData();
mPreferredUserId = intent.getStringExtra(EXTRA_USER_ID);
mData = intent.getParcelableExtra(EXTRA_DATA);
- if (mAppUri == null) {
+ if (appUri == null) {
Log.e(Constants.TAG, "Intent data missing. Should be Uri of app!");
finish();
return;
} else {
- Log.d(Constants.TAG, "uri: " + mAppUri);
- startListFragments(savedInstanceState, mAppUri, mData);
+ Log.d(Constants.TAG, "uri: " + appUri);
+ startListFragments(savedInstanceState, appUri, mData);
}
}
@@ -113,11 +108,11 @@ public class SelectSignKeyIdActivity extends BaseActivity {
}
// Create an instance of the fragments
- mListFragment = SelectSignKeyIdListFragment.newInstance(dataUri, data);
+ SelectSignKeyIdListFragment listFragment = SelectSignKeyIdListFragment.newInstance(dataUri, data);
// Add the fragment to the 'fragment_container' FrameLayout
// NOTE: We use commitAllowingStateLoss() to prevent weird crashes!
getSupportFragmentManager().beginTransaction()
- .replace(R.id.api_select_sign_key_list_fragment, mListFragment)
+ .replace(R.id.api_select_sign_key_list_fragment, listFragment)
.commitAllowingStateLoss();
// do it immediately!
getSupportFragmentManager().executePendingTransactions();
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/CertifyActionsParcel.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/CertifyActionsParcel.java
index e76dcfb49..3cdbca633 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/CertifyActionsParcel.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/CertifyActionsParcel.java
@@ -23,6 +23,9 @@ import android.os.Parcelable;
import java.io.Serializable;
import java.util.ArrayList;
+import java.util.List;
+import java.util.Date;
+import java.util.Map;
import org.sufficientlysecure.keychain.pgp.WrappedUserAttribute;
import org.sufficientlysecure.keychain.util.ParcelableProxy;
@@ -86,17 +89,10 @@ public class CertifyActionsParcel implements Parcelable {
final public ArrayList<String> mUserIds;
final public ArrayList<WrappedUserAttribute> mUserAttributes;
- public CertifyAction(long masterKeyId, ArrayList<String> userIds) {
+ public CertifyAction(long masterKeyId, List<String> userIds, List<WrappedUserAttribute> attributes) {
mMasterKeyId = masterKeyId;
- mUserIds = userIds;
- mUserAttributes = null;
- }
-
- public CertifyAction(long masterKeyId, ArrayList<String> userIds,
- ArrayList<WrappedUserAttribute> attributes) {
- mMasterKeyId = masterKeyId;
- mUserIds = userIds;
- mUserAttributes = attributes;
+ mUserIds = userIds == null ? null : new ArrayList<>(userIds);
+ mUserAttributes = attributes == null ? null : new ArrayList<>(attributes);
}
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ContactSyncAdapterService.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ContactSyncAdapterService.java
index 7688b9252..b36d23775 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ContactSyncAdapterService.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ContactSyncAdapterService.java
@@ -45,7 +45,7 @@ public class ContactSyncAdapterService extends Service {
@Override
public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider,
final SyncResult syncResult) {
- Log.d(Constants.TAG, "Performing a sync!");
+ Log.d(Constants.TAG, "Performing a contact sync!");
// TODO: Import is currently disabled for 2.8, until we implement proper origin management
// importDone.set(false);
// KeychainApplication.setupAccountAsNeeded(ContactSyncAdapterService.this);
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainService.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainService.java
index ce4381140..d2128cd77 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainService.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainService.java
@@ -41,7 +41,7 @@ import org.sufficientlysecure.keychain.operations.PromoteKeyOperation;
import org.sufficientlysecure.keychain.operations.RevokeOperation;
import org.sufficientlysecure.keychain.operations.SignEncryptOperation;
import org.sufficientlysecure.keychain.operations.results.OperationResult;
-import org.sufficientlysecure.keychain.pgp.PgpDecryptVerify;
+import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyOperation;
import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyInputParcel;
import org.sufficientlysecure.keychain.pgp.Progressable;
import org.sufficientlysecure.keychain.pgp.SignEncryptParcel;
@@ -112,7 +112,7 @@ public class KeychainService extends Service implements Progressable {
op = new SignEncryptOperation(outerThis, new ProviderHelper(outerThis),
outerThis, mActionCanceled);
} else if (inputParcel instanceof PgpDecryptVerifyInputParcel) {
- op = new PgpDecryptVerify(outerThis, new ProviderHelper(outerThis), outerThis);
+ op = new PgpDecryptVerifyOperation(outerThis, new ProviderHelper(outerThis), outerThis);
} else if (inputParcel instanceof SaveKeyringParcel) {
op = new EditKeyOperation(outerThis, new ProviderHelper(outerThis), outerThis,
mActionCanceled);
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeyserverSyncAdapterService.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeyserverSyncAdapterService.java
new file mode 100644
index 000000000..8aebae7aa
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeyserverSyncAdapterService.java
@@ -0,0 +1,516 @@
+package org.sufficientlysecure.keychain.service;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.app.AlarmManager;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.AbstractThreadedSyncAdapter;
+import android.content.ContentProviderClient;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SyncResult;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.PowerManager;
+import android.os.SystemClock;
+import android.support.v4.app.NotificationCompat;
+import android.widget.Toast;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing;
+import org.sufficientlysecure.keychain.operations.ImportOperation;
+import org.sufficientlysecure.keychain.operations.results.ImportKeyResult;
+import org.sufficientlysecure.keychain.operations.results.OperationResult;
+import org.sufficientlysecure.keychain.provider.KeychainContract;
+import org.sufficientlysecure.keychain.provider.ProviderHelper;
+import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
+import org.sufficientlysecure.keychain.ui.OrbotRequiredDialogActivity;
+import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
+import org.sufficientlysecure.keychain.util.Log;
+import org.sufficientlysecure.keychain.util.ParcelableProxy;
+import org.sufficientlysecure.keychain.util.Preferences;
+import org.sufficientlysecure.keychain.util.orbot.OrbotHelper;
+
+import java.util.ArrayList;
+import java.util.GregorianCalendar;
+import java.util.Random;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class KeyserverSyncAdapterService extends Service {
+
+ // how often a sync should be initiated, in s
+ public static final long SYNC_INTERVAL =
+ Constants.DEBUG_KEYSERVER_SYNC
+ ? TimeUnit.MINUTES.toSeconds(2) : TimeUnit.DAYS.toSeconds(3);
+ // time since last update after which a key should be updated again, in s
+ public static final long KEY_UPDATE_LIMIT =
+ Constants.DEBUG_KEYSERVER_SYNC ? 1 : TimeUnit.DAYS.toSeconds(7);
+ // time by which a sync is postponed in case of a
+ public static final long SYNC_POSTPONE_TIME =
+ Constants.DEBUG_KEYSERVER_SYNC ? 30 * 1000 : TimeUnit.MINUTES.toMillis(5);
+ // Time taken by Orbot before a new circuit is created
+ public static final int ORBOT_CIRCUIT_TIMEOUT = (int) TimeUnit.MINUTES.toMillis(10);
+
+
+ private static final String ACTION_IGNORE_TOR = "ignore_tor";
+ private static final String ACTION_UPDATE_ALL = "update_all";
+ private static final String ACTION_SYNC_NOW = "sync_now";
+ private static final String ACTION_DISMISS_NOTIFICATION = "cancel_sync";
+ private static final String ACTION_START_ORBOT = "start_orbot";
+ private static final String ACTION_CANCEL = "cancel";
+
+ private AtomicBoolean mCancelled = new AtomicBoolean(false);
+
+ @Override
+ public int onStartCommand(final Intent intent, int flags, final int startId) {
+ switch (intent.getAction()) {
+ case ACTION_CANCEL: {
+ mCancelled.set(true);
+ break;
+ }
+ // the reason for the separation betweyeen SYNC_NOW and UPDATE_ALL is so that starting
+ // the sync directly from the notification is possible while the screen is on with
+ // UPDATE_ALL, but a postponed sync is only started if screen is off
+ case ACTION_SYNC_NOW: {
+ // this checks for screen on/off before sync, and postpones the sync if on
+ ContentResolver.requestSync(
+ new Account(Constants.ACCOUNT_NAME, Constants.ACCOUNT_TYPE),
+ Constants.PROVIDER_AUTHORITY,
+ new Bundle()
+ );
+ break;
+ }
+ case ACTION_UPDATE_ALL: {
+ // does not check for screen on/off
+ asyncKeyUpdate(this, new CryptoInputParcel());
+ break;
+ }
+ case ACTION_IGNORE_TOR: {
+ NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
+ manager.cancel(Constants.Notification.KEYSERVER_SYNC_FAIL_ORBOT);
+ asyncKeyUpdate(this, new CryptoInputParcel(ParcelableProxy.getForNoProxy()));
+ break;
+ }
+ case ACTION_START_ORBOT: {
+ NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
+ manager.cancel(Constants.Notification.KEYSERVER_SYNC_FAIL_ORBOT);
+ Intent startOrbot = new Intent(this, OrbotRequiredDialogActivity.class);
+ startOrbot.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ startOrbot.putExtra(OrbotRequiredDialogActivity.EXTRA_START_ORBOT, true);
+ Messenger messenger = new Messenger(
+ new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case OrbotRequiredDialogActivity.MESSAGE_ORBOT_STARTED: {
+ asyncKeyUpdate(KeyserverSyncAdapterService.this,
+ new CryptoInputParcel());
+ break;
+ }
+ case OrbotRequiredDialogActivity.MESSAGE_ORBOT_IGNORE: {
+ asyncKeyUpdate(KeyserverSyncAdapterService.this,
+ new CryptoInputParcel(
+ ParcelableProxy.getForNoProxy()));
+ break;
+ }
+ case OrbotRequiredDialogActivity.MESSAGE_DIALOG_CANCEL: {
+ // just stop service
+ stopSelf();
+ break;
+ }
+ }
+ }
+ }
+ );
+ startOrbot.putExtra(OrbotRequiredDialogActivity.EXTRA_MESSENGER, messenger);
+ startActivity(startOrbot);
+ break;
+ }
+ case ACTION_DISMISS_NOTIFICATION: {
+ NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
+ manager.cancel(Constants.Notification.KEYSERVER_SYNC_FAIL_ORBOT);
+ stopSelf(startId);
+ break;
+ }
+ }
+ return START_NOT_STICKY;
+ }
+
+ private class KeyserverSyncAdapter extends AbstractThreadedSyncAdapter {
+
+ public KeyserverSyncAdapter() {
+ super(KeyserverSyncAdapterService.this, true);
+ }
+
+ @Override
+ public void onPerformSync(Account account, Bundle extras, String authority,
+ ContentProviderClient provider, SyncResult syncResult) {
+ Log.d(Constants.TAG, "Performing a keyserver sync!");
+
+ PowerManager pm = (PowerManager) KeyserverSyncAdapterService.this
+ .getSystemService(Context.POWER_SERVICE);
+ @SuppressWarnings("deprecation") // our min is API 15, deprecated only in 20
+ boolean isScreenOn = pm.isScreenOn();
+
+ if (!isScreenOn) {
+ Intent serviceIntent = new Intent(KeyserverSyncAdapterService.this,
+ KeyserverSyncAdapterService.class);
+ serviceIntent.setAction(ACTION_UPDATE_ALL);
+ startService(serviceIntent);
+ } else {
+ postponeSync();
+ }
+ }
+
+ @Override
+ public void onSyncCanceled() {
+ super.onSyncCanceled();
+ cancelUpdates(KeyserverSyncAdapterService.this);
+ }
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return new KeyserverSyncAdapter().getSyncAdapterBinder();
+ }
+
+ private void handleUpdateResult(ImportKeyResult result) {
+ if (result.isPending()) {
+ // result is pending due to Orbot not being started
+ // try to start it silently, if disabled show notifications
+ new OrbotHelper.SilentStartManager() {
+ @Override
+ protected void onOrbotStarted() {
+ // retry the update
+ asyncKeyUpdate(KeyserverSyncAdapterService.this,
+ new CryptoInputParcel());
+ }
+
+ @Override
+ protected void onSilentStartDisabled() {
+ // show notification
+ NotificationManager manager =
+ (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
+ manager.notify(Constants.Notification.KEYSERVER_SYNC_FAIL_ORBOT,
+ getOrbotNoification(KeyserverSyncAdapterService.this));
+ }
+ }.startOrbotAndListen(this, false);
+ } else if (isUpdateCancelled()) {
+ Log.d(Constants.TAG, "Keyserver sync cancelled, postponing by" + SYNC_POSTPONE_TIME
+ + "ms");
+ postponeSync();
+ } else {
+ Log.d(Constants.TAG, "Keyserver sync completed: Updated: " + result.mUpdatedKeys
+ + " Failed: " + result.mBadKeys);
+ stopSelf();
+ }
+ }
+
+ private void postponeSync() {
+ AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
+ Intent serviceIntent = new Intent(this, KeyserverSyncAdapterService.class);
+ serviceIntent.setAction(ACTION_SYNC_NOW);
+ PendingIntent pi = PendingIntent.getService(this, 0, serviceIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT);
+ alarmManager.set(
+ AlarmManager.ELAPSED_REALTIME_WAKEUP,
+ SystemClock.elapsedRealtime() + SYNC_POSTPONE_TIME,
+ pi
+ );
+ }
+
+ private void asyncKeyUpdate(final Context context,
+ final CryptoInputParcel cryptoInputParcel) {
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ ImportKeyResult result = updateKeysFromKeyserver(context, cryptoInputParcel);
+ handleUpdateResult(result);
+ }
+ }).start();
+ }
+
+ private synchronized ImportKeyResult updateKeysFromKeyserver(final Context context,
+ final CryptoInputParcel cryptoInputParcel) {
+ mCancelled.set(false);
+
+ ArrayList<ParcelableKeyRing> keyList = getKeysToUpdate(context);
+
+ if (isUpdateCancelled()) { // if we've already been cancelled
+ return new ImportKeyResult(OperationResult.RESULT_CANCELLED,
+ new OperationResult.OperationLog());
+ }
+
+ if (cryptoInputParcel.getParcelableProxy() == null) {
+ // no explicit proxy, retrieve from preferences. Check if we should do a staggered sync
+ if (Preferences.getPreferences(context).getProxyPrefs().torEnabled) {
+ return staggeredUpdate(context, keyList, cryptoInputParcel);
+ } else {
+ return directUpdate(context, keyList, cryptoInputParcel);
+ }
+ } else {
+ return directUpdate(context, keyList, cryptoInputParcel);
+ }
+ }
+
+ private ImportKeyResult directUpdate(Context context, ArrayList<ParcelableKeyRing> keyList,
+ CryptoInputParcel cryptoInputParcel) {
+ Log.d(Constants.TAG, "Starting normal update");
+ ImportOperation importOp = new ImportOperation(context, new ProviderHelper(context), null);
+ return importOp.execute(
+ new ImportKeyringParcel(keyList,
+ Preferences.getPreferences(context).getPreferredKeyserver()),
+ cryptoInputParcel
+ );
+ }
+
+
+ /**
+ * will perform a staggered update of user's keys using delays to ensure new Tor circuits, as
+ * performed by parcimonie. Relevant issue and method at:
+ * https://github.com/open-keychain/open-keychain/issues/1337
+ *
+ * @return result of the sync
+ */
+ private ImportKeyResult staggeredUpdate(Context context, ArrayList<ParcelableKeyRing> keyList,
+ CryptoInputParcel cryptoInputParcel) {
+ Log.d(Constants.TAG, "Starting staggered update");
+ // final int WEEK_IN_SECONDS = (int) TimeUnit.DAYS.toSeconds(7);
+ final int WEEK_IN_SECONDS = 0;
+ ImportOperation.KeyImportAccumulator accumulator
+ = new ImportOperation.KeyImportAccumulator(keyList.size(), null);
+ for (ParcelableKeyRing keyRing : keyList) {
+ int waitTime;
+ int staggeredTime = new Random().nextInt(1 + 2 * (WEEK_IN_SECONDS / keyList.size()));
+ if (staggeredTime >= ORBOT_CIRCUIT_TIMEOUT) {
+ waitTime = staggeredTime;
+ } else {
+ waitTime = ORBOT_CIRCUIT_TIMEOUT + new Random().nextInt(ORBOT_CIRCUIT_TIMEOUT);
+ }
+ Log.d(Constants.TAG, "Updating key with fingerprint " + keyRing.mExpectedFingerprint +
+ " with a wait time of " + waitTime + "s");
+ try {
+ Thread.sleep(waitTime * 1000);
+ } catch (InterruptedException e) {
+ Log.e(Constants.TAG, "Exception during sleep between key updates", e);
+ // skip this one
+ continue;
+ }
+ ArrayList<ParcelableKeyRing> keyWrapper = new ArrayList<>();
+ keyWrapper.add(keyRing);
+ if (isUpdateCancelled()) {
+ return new ImportKeyResult(ImportKeyResult.RESULT_CANCELLED,
+ new OperationResult.OperationLog());
+ }
+ ImportKeyResult result =
+ new ImportOperation(context, new ProviderHelper(context), null, mCancelled)
+ .execute(
+ new ImportKeyringParcel(
+ keyWrapper,
+ Preferences.getPreferences(context)
+ .getPreferredKeyserver()
+ ),
+ cryptoInputParcel
+ );
+ if (result.isPending()) {
+ return result;
+ }
+ accumulator.accumulateKeyImport(result);
+ }
+ return accumulator.getConsolidatedResult();
+ }
+
+ /**
+ * 1. Get keys which have been updated recently and therefore do not need to
+ * be updated now
+ * 2. Get list of all keys and filter out ones that don't need to be updated
+ * 3. Return keys to be updated
+ *
+ * @return list of keys that require update
+ */
+ private ArrayList<ParcelableKeyRing> getKeysToUpdate(Context context) {
+
+ // 1. Get keys which have been updated recently and don't need to updated now
+ final int INDEX_UPDATED_KEYS_MASTER_KEY_ID = 0;
+ final int INDEX_LAST_UPDATED = 1;
+
+ // all time in seconds not milliseconds
+ final long CURRENT_TIME = GregorianCalendar.getInstance().getTimeInMillis() / 1000;
+ Cursor updatedKeysCursor = context.getContentResolver().query(
+ KeychainContract.UpdatedKeys.CONTENT_URI,
+ new String[]{
+ KeychainContract.UpdatedKeys.MASTER_KEY_ID,
+ KeychainContract.UpdatedKeys.LAST_UPDATED
+ },
+ "? - " + KeychainContract.UpdatedKeys.LAST_UPDATED + " < " + KEY_UPDATE_LIMIT,
+ new String[]{"" + CURRENT_TIME},
+ null
+ );
+
+ ArrayList<Long> ignoreMasterKeyIds = new ArrayList<>();
+ while (updatedKeysCursor.moveToNext()) {
+ long masterKeyId = updatedKeysCursor.getLong(INDEX_UPDATED_KEYS_MASTER_KEY_ID);
+ Log.d(Constants.TAG, "Keyserver sync: Ignoring {" + masterKeyId + "} last updated at {"
+ + updatedKeysCursor.getLong(INDEX_LAST_UPDATED) + "}s");
+ ignoreMasterKeyIds.add(masterKeyId);
+ }
+ updatedKeysCursor.close();
+
+ // 2. Make a list of public keys which should be updated
+ final int INDEX_MASTER_KEY_ID = 0;
+ final int INDEX_FINGERPRINT = 1;
+ Cursor keyCursor = context.getContentResolver().query(
+ KeychainContract.KeyRings.buildUnifiedKeyRingsUri(),
+ new String[]{
+ KeychainContract.KeyRings.MASTER_KEY_ID,
+ KeychainContract.KeyRings.FINGERPRINT
+ },
+ null,
+ null,
+ null
+ );
+
+ if (keyCursor == null) {
+ return new ArrayList<>();
+ }
+
+ ArrayList<ParcelableKeyRing> keyList = new ArrayList<>();
+ while (keyCursor.moveToNext()) {
+ long keyId = keyCursor.getLong(INDEX_MASTER_KEY_ID);
+ if (ignoreMasterKeyIds.contains(keyId)) {
+ continue;
+ }
+ Log.d(Constants.TAG, "Keyserver sync: Updating {" + keyId + "}");
+ String fingerprint = KeyFormattingUtils
+ .convertFingerprintToHex(keyCursor.getBlob(INDEX_FINGERPRINT));
+ String hexKeyId = KeyFormattingUtils
+ .convertKeyIdToHex(keyId);
+ // we aren't updating from keybase as of now
+ keyList.add(new ParcelableKeyRing(fingerprint, hexKeyId, null));
+ }
+ keyCursor.close();
+
+ return keyList;
+ }
+
+ private boolean isUpdateCancelled() {
+ return mCancelled.get();
+ }
+
+ /**
+ * will cancel an update already in progress. We send an Intent to cancel it instead of simply
+ * modifying a static variable sync the service is running in a process that is different from
+ * the default application process where the UI code runs.
+ *
+ * @param context used to send an Intent to the service requesting cancellation.
+ */
+ public static void cancelUpdates(Context context) {
+ Intent intent = new Intent(context, KeyserverSyncAdapterService.class);
+ intent.setAction(ACTION_CANCEL);
+ context.startService(intent);
+ }
+
+ private Notification getOrbotNoification(Context context) {
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(context);
+ builder.setSmallIcon(R.drawable.ic_stat_notify_24dp)
+ .setLargeIcon(getBitmap(R.mipmap.ic_launcher, context))
+ .setContentTitle(context.getString(R.string.keyserver_sync_orbot_notif_title))
+ .setContentText(context.getString(R.string.keyserver_sync_orbot_notif_msg))
+ .setAutoCancel(true);
+
+ // In case the user decides to not use tor
+ Intent ignoreTorIntent = new Intent(context, KeyserverSyncAdapterService.class);
+ ignoreTorIntent.setAction(ACTION_IGNORE_TOR);
+ PendingIntent ignoreTorPi = PendingIntent.getService(
+ context,
+ 0, // security not issue since we're giving this pending intent to Notification Manager
+ ignoreTorIntent,
+ PendingIntent.FLAG_CANCEL_CURRENT
+ );
+
+ builder.addAction(R.drawable.ic_stat_tor_off,
+ context.getString(R.string.keyserver_sync_orbot_notif_ignore),
+ ignoreTorPi);
+
+ Intent startOrbotIntent = new Intent(context, KeyserverSyncAdapterService.class);
+ startOrbotIntent.setAction(ACTION_START_ORBOT);
+ PendingIntent startOrbotPi = PendingIntent.getService(
+ context,
+ 0, // security not issue since we're giving this pending intent to Notification Manager
+ startOrbotIntent,
+ PendingIntent.FLAG_CANCEL_CURRENT
+ );
+
+ builder.addAction(R.drawable.ic_stat_tor,
+ context.getString(R.string.keyserver_sync_orbot_notif_start),
+ startOrbotPi
+ );
+ builder.setContentIntent(startOrbotPi);
+
+ return builder.build();
+ }
+
+ public static void enableKeyserverSync(Context context) {
+ try {
+ AccountManager manager = AccountManager.get(context);
+ Account[] accounts = manager.getAccountsByType(Constants.ACCOUNT_TYPE);
+
+ Account account = new Account(Constants.ACCOUNT_NAME, Constants.ACCOUNT_TYPE);
+ if (accounts.length == 0) {
+ if (!manager.addAccountExplicitly(account, null, null)) {
+ Log.e(Constants.TAG, "Adding account failed!");
+ }
+ }
+ // for keyserver sync
+ ContentResolver.setIsSyncable(account, Constants.PROVIDER_AUTHORITY, 1);
+ ContentResolver.setSyncAutomatically(account, Constants.PROVIDER_AUTHORITY,
+ true);
+ ContentResolver.addPeriodicSync(
+ account,
+ Constants.PROVIDER_AUTHORITY,
+ new Bundle(),
+ SYNC_INTERVAL
+ );
+ } catch (SecurityException e) {
+ Log.e(Constants.TAG, "SecurityException when adding the account", e);
+ Toast.makeText(context, R.string.reinstall_openkeychain, Toast.LENGTH_LONG).show();
+ }
+ }
+
+ // from de.azapps.mirakel.helper.Helpers from https://github.com/MirakelX/mirakel-android
+ private Bitmap getBitmap(int resId, Context context) {
+ int mLargeIconWidth = (int) context.getResources().getDimension(
+ android.R.dimen.notification_large_icon_width);
+ int mLargeIconHeight = (int) context.getResources().getDimension(
+ android.R.dimen.notification_large_icon_height);
+ Drawable d;
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
+ // noinspection deprecation (can't help it at this api level)
+ d = context.getResources().getDrawable(resId);
+ } else {
+ d = context.getDrawable(resId);
+ }
+ if (d == null) {
+ return null;
+ }
+ Bitmap b = Bitmap.createBitmap(mLargeIconWidth, mLargeIconHeight, Bitmap.Config.ARGB_8888);
+ Canvas c = new Canvas(b);
+ d.setBounds(0, 0, mLargeIconWidth, mLargeIconHeight);
+ d.draw(c);
+ return b;
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/PassphraseCacheService.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/PassphraseCacheService.java
index 2a258b7e3..5d04317b3 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/PassphraseCacheService.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/PassphraseCacheService.java
@@ -101,8 +101,6 @@ public class PassphraseCacheService extends Service {
private static final long DEFAULT_TTL = 15;
- private static final int NOTIFICATION_ID = 1;
-
private static final int MSG_PASSPHRASE_CACHE_GET_OKAY = 1;
private static final int MSG_PASSPHRASE_CACHE_GET_KEY_NOT_FOUND = 2;
@@ -477,7 +475,7 @@ public class PassphraseCacheService extends Service {
private void updateService() {
if (mPassphraseCache.size() > 0) {
- startForeground(NOTIFICATION_ID, getNotification());
+ startForeground(Constants.Notification.PASSPHRASE_CACHE, getNotification());
} else {
// stop whole service if no cached passphrases remaining
Log.d(Constants.TAG, "PassphraseCacheService: No passphrases remaining in memory, stopping service!");
@@ -511,7 +509,7 @@ public class PassphraseCacheService extends Service {
private Notification getNotification() {
NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
builder.setSmallIcon(R.drawable.ic_stat_notify_24dp)
- .setLargeIcon(getBitmap(R.drawable.ic_launcher, getBaseContext()))
+ .setLargeIcon(getBitmap(R.mipmap.ic_launcher, getBaseContext()))
.setContentTitle(getResources().getQuantityString(R.plurals.passp_cache_notif_n_keys,
mPassphraseCache.size(), mPassphraseCache.size()))
.setContentText(getString(R.string.passp_cache_notif_click_to_clear));
@@ -603,4 +601,4 @@ public class PassphraseCacheService extends Service {
this.passphrase = passphrase;
}
}
-} \ No newline at end of file
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/BackupFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/BackupFragment.java
index 2d9ac6ee3..a3ea8ad9a 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/BackupFragment.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/BackupFragment.java
@@ -18,7 +18,11 @@
package org.sufficientlysecure.keychain.ui;
+import java.io.File;
+import java.text.SimpleDateFormat;
import java.util.ArrayList;
+import java.util.Date;
+import java.util.Locale;
import android.app.Activity;
import android.content.ContentResolver;
@@ -47,7 +51,20 @@ public class BackupFragment extends Fragment {
private int mIndex;
static final int REQUEST_REPEAT_PASSPHRASE = 1;
+ private ExportHelper mExportHelper;
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ // we won't get attached to a non-fragment activity, so the cast should be safe
+ mExportHelper = new ExportHelper((FragmentActivity) activity);
+ }
+
+ @Override
+ public void onDetach() {
+ super.onDetach();
+ mExportHelper = null;
+ }
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
@@ -80,8 +97,7 @@ public class BackupFragment extends Fragment {
}
if (!includeSecretKeys) {
- ExportHelper exportHelper = new ExportHelper(activity);
- exportHelper.showExportKeysDialog(null, Constants.Path.APP_DIR_FILE, false);
+ startBackup(false);
return;
}
@@ -136,8 +152,7 @@ public class BackupFragment extends Fragment {
return;
}
- ExportHelper exportHelper = new ExportHelper(activity);
- exportHelper.showExportKeysDialog(null, Constants.Path.APP_DIR_FILE, true);
+ startBackup(true);
}
}.execute(activity.getContentResolver());
@@ -167,8 +182,19 @@ public class BackupFragment extends Fragment {
return;
}
- ExportHelper exportHelper = new ExportHelper(getActivity());
- exportHelper.showExportKeysDialog(null, Constants.Path.APP_DIR_FILE, true);
+ startBackup(true);
}
}
+
+ private void startBackup(boolean exportSecret) {
+ File filename;
+ String date = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(new Date());
+ if (exportSecret) {
+ filename = new File(Constants.Path.APP_DIR, "keys_" + date + ".asc");
+ } else {
+ filename = new File(Constants.Path.APP_DIR, "keys_" + date + ".pub.asc");
+ }
+ mExportHelper.showExportKeysDialog(null, filename, exportSecret);
+ }
+
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyFingerprintActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyFingerprintActivity.java
index 016ab5f3c..c5528e40b 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyFingerprintActivity.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyFingerprintActivity.java
@@ -30,6 +30,8 @@ public class CertifyFingerprintActivity extends BaseActivity {
protected Uri mDataUri;
+ public static final String EXTRA_ENABLE_WORD_CONFIRM = "enable_word_confirm";
+
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -40,6 +42,7 @@ public class CertifyFingerprintActivity extends BaseActivity {
finish();
return;
}
+ boolean enableWordConfirm = getIntent().getBooleanExtra(EXTRA_ENABLE_WORD_CONFIRM, false);
setFullScreenDialogClose(new View.OnClickListener() {
@Override
@@ -50,7 +53,7 @@ public class CertifyFingerprintActivity extends BaseActivity {
Log.i(Constants.TAG, "mDataUri: " + mDataUri.toString());
- startFragment(savedInstanceState, mDataUri);
+ startFragment(savedInstanceState, mDataUri, enableWordConfirm);
}
@Override
@@ -58,7 +61,7 @@ public class CertifyFingerprintActivity extends BaseActivity {
setContentView(R.layout.certify_fingerprint_activity);
}
- private void startFragment(Bundle savedInstanceState, Uri dataUri) {
+ private void startFragment(Bundle savedInstanceState, Uri dataUri, boolean enableWordConfirm) {
// However, if we're being restored from a previous state,
// then we don't need to do anything and should return or else
// we could end up with overlapping fragments.
@@ -67,7 +70,7 @@ public class CertifyFingerprintActivity extends BaseActivity {
}
// Create an instance of the fragment
- CertifyFingerprintFragment frag = CertifyFingerprintFragment.newInstance(dataUri);
+ CertifyFingerprintFragment frag = CertifyFingerprintFragment.newInstance(dataUri, enableWordConfirm);
// Add the fragment to the 'fragment_container' FrameLayout
// NOTE: We use commitAllowingStateLoss() to prevent weird crashes!
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyFingerprintFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyFingerprintFragment.java
index a6b8a0e85..552fa34c0 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyFingerprintFragment.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyFingerprintFragment.java
@@ -19,6 +19,7 @@ package org.sufficientlysecure.keychain.ui;
import android.content.Intent;
import android.database.Cursor;
+import android.graphics.Typeface;
import android.net.Uri;
import android.os.Bundle;
import android.support.v4.app.LoaderManager;
@@ -34,6 +35,7 @@ import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException;
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
import org.sufficientlysecure.keychain.provider.ProviderHelper;
+import org.sufficientlysecure.keychain.ui.util.ExperimentalWordConfirm;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
import org.sufficientlysecure.keychain.util.Log;
@@ -44,23 +46,24 @@ public class CertifyFingerprintFragment extends LoaderFragment implements
static final int REQUEST_CERTIFY = 1;
public static final String ARG_DATA_URI = "uri";
+ public static final String ARG_ENABLE_WORD_CONFIRM = "enable_word_confirm";
private TextView mFingerprint;
+ private TextView mIntro;
private static final int LOADER_ID_UNIFIED = 0;
private Uri mDataUri;
-
- private View mActionNo;
- private View mActionYes;
+ private boolean mEnableWordConfirm;
/**
* Creates new instance of this fragment
*/
- public static CertifyFingerprintFragment newInstance(Uri dataUri) {
+ public static CertifyFingerprintFragment newInstance(Uri dataUri, boolean enableWordConfirm) {
CertifyFingerprintFragment frag = new CertifyFingerprintFragment();
Bundle args = new Bundle();
args.putParcelable(ARG_DATA_URI, dataUri);
+ args.putBoolean(ARG_ENABLE_WORD_CONFIRM, enableWordConfirm);
frag.setArguments(args);
@@ -72,18 +75,19 @@ public class CertifyFingerprintFragment extends LoaderFragment implements
View root = super.onCreateView(inflater, superContainer, savedInstanceState);
View view = inflater.inflate(R.layout.certify_fingerprint_fragment, getContainer());
- mActionNo = view.findViewById(R.id.certify_fingerprint_button_no);
- mActionYes = view.findViewById(R.id.certify_fingerprint_button_yes);
+ View actionNo = view.findViewById(R.id.certify_fingerprint_button_no);
+ View actionYes = view.findViewById(R.id.certify_fingerprint_button_yes);
mFingerprint = (TextView) view.findViewById(R.id.certify_fingerprint_fingerprint);
+ mIntro = (TextView) view.findViewById(R.id.certify_fingerprint_intro);
- mActionNo.setOnClickListener(new View.OnClickListener() {
+ actionNo.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
getActivity().finish();
}
});
- mActionYes.setOnClickListener(new View.OnClickListener() {
+ actionYes.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
certify(mDataUri);
@@ -103,6 +107,11 @@ public class CertifyFingerprintFragment extends LoaderFragment implements
getActivity().finish();
return;
}
+ mEnableWordConfirm = getArguments().getBoolean(ARG_ENABLE_WORD_CONFIRM);
+
+ if (mEnableWordConfirm) {
+ mIntro.setText(R.string.certify_fingerprint_text_words);
+ }
loadData(dataUri);
}
@@ -149,10 +158,13 @@ public class CertifyFingerprintFragment extends LoaderFragment implements
switch (loader.getId()) {
case LOADER_ID_UNIFIED: {
if (data.moveToFirst()) {
-
byte[] fingerprintBlob = data.getBlob(INDEX_UNIFIED_FINGERPRINT);
- String fingerprint = KeyFormattingUtils.convertFingerprintToHex(fingerprintBlob);
- mFingerprint.setText(KeyFormattingUtils.colorizeFingerprint(fingerprint));
+
+ if (mEnableWordConfirm) {
+ displayWordConfirm(fingerprintBlob);
+ } else {
+ displayHexConfirm(fingerprintBlob);
+ }
break;
}
@@ -162,6 +174,19 @@ public class CertifyFingerprintFragment extends LoaderFragment implements
setContentShown(true);
}
+ private void displayHexConfirm(byte[] fingerprintBlob) {
+ String fingerprint = KeyFormattingUtils.convertFingerprintToHex(fingerprintBlob);
+ mFingerprint.setText(KeyFormattingUtils.colorizeFingerprint(fingerprint));
+ }
+
+ private void displayWordConfirm(byte[] fingerprintBlob) {
+ String fingerprint = ExperimentalWordConfirm.getWords(getActivity(), fingerprintBlob);
+
+ mFingerprint.setTextSize(24);
+ mFingerprint.setTypeface(Typeface.DEFAULT, Typeface.BOLD);
+ mFingerprint.setText(fingerprint);
+ }
+
/**
* This is called when the last Cursor provided to onLoadFinished() above is about to be closed.
* We need to make sure we are no longer using it.
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyKeyFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyKeyFragment.java
index 5ca7c6bc7..357b445f0 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyKeyFragment.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyKeyFragment.java
@@ -83,7 +83,9 @@ public class CertifyKeyFragment
};
private static final int INDEX_MASTER_KEY_ID = 1;
private static final int INDEX_USER_ID = 2;
+ @SuppressWarnings("unused")
private static final int INDEX_IS_PRIMARY = 3;
+ @SuppressWarnings("unused")
private static final int INDEX_IS_REVOKED = 4;
private MultiUserIdsAdapter mUserIdsAdapter;
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptActivity.java
index e581e069b..881190ae2 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptActivity.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptActivity.java
@@ -29,13 +29,14 @@ import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
+import android.support.annotation.Nullable;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
-import android.view.View;
import android.widget.Toast;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.intents.OpenKeychainIntents;
+import org.sufficientlysecure.keychain.pgp.PgpHelper;
import org.sufficientlysecure.keychain.provider.TemporaryStorageProvider;
import org.sufficientlysecure.keychain.ui.base.BaseActivity;
@@ -93,7 +94,9 @@ public class DecryptActivity extends BaseActivity {
} else if (intent.hasExtra(Intent.EXTRA_TEXT)) {
String text = intent.getStringExtra(Intent.EXTRA_TEXT);
Uri uri = readToTempFile(text);
- uris.add(uri);
+ if (uri != null) {
+ uris.add(uri);
+ }
}
break;
@@ -105,7 +108,9 @@ public class DecryptActivity extends BaseActivity {
} else if (intent.hasExtra(Intent.EXTRA_TEXT)) {
for (String text : intent.getStringArrayListExtra(Intent.EXTRA_TEXT)) {
Uri uri = readToTempFile(text);
- uris.add(uri);
+ if (uri != null) {
+ uris.add(uri);
+ }
}
}
@@ -139,7 +144,9 @@ public class DecryptActivity extends BaseActivity {
String text = clip.getItemAt(0).coerceToText(this).toString();
uri = readToTempFile(text);
}
- uris.add(uri);
+ if (uri != null) {
+ uris.add(uri);
+ }
break;
}
@@ -170,9 +177,17 @@ public class DecryptActivity extends BaseActivity {
}
- public Uri readToTempFile(String text) throws IOException {
+ @Nullable public Uri readToTempFile(String text) throws IOException {
Uri tempFile = TemporaryStorageProvider.createFile(this);
OutputStream outStream = getContentResolver().openOutputStream(tempFile);
+
+ // clean up ascii armored message, fixing newlines and stuff
+ String cleanedText = PgpHelper.getPgpContent(text);
+ if (cleanedText == null) {
+ return null;
+ }
+
+ // if cleanup didn't work, just try the raw data
outStream.write(text.getBytes());
outStream.close();
return tempFile;
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptListFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptListFragment.java
index 640755ef3..3dda47ac5 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptListFragment.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptListFragment.java
@@ -35,7 +35,6 @@ import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.AsyncTask;
-import android.os.Build;
import android.os.Bundle;
import android.support.v7.widget.DefaultItemAnimator;
import android.support.v7.widget.LinearLayoutManager;
@@ -222,18 +221,6 @@ public class DecryptListFragment
}
}
- private void askForOutputFilename(Uri inputUri, String originalFilename, String mimeType) {
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
- File file = new File(inputUri.getPath());
- File parentDir = file.exists() ? file.getParentFile() : Constants.Path.APP_DIR;
- File targetFile = new File(parentDir, originalFilename);
- FileHelper.saveFile(this, getString(R.string.title_decrypt_to_file),
- getString(R.string.specify_file_to_decrypt_to), targetFile, REQUEST_CODE_OUTPUT);
- } else {
- FileHelper.saveDocument(this, mimeType, originalFilename, REQUEST_CODE_OUTPUT);
- }
- }
-
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) {
@@ -388,7 +375,7 @@ public class DecryptListFragment
onFileClick = new OnClickListener() {
@Override
public void onClick(View view) {
- displayWithViewIntent(uri);
+ displayWithViewIntent(uri, false);
}
};
}
@@ -413,7 +400,7 @@ public class DecryptListFragment
}
- public void displayWithViewIntent(final Uri uri) {
+ public void displayWithViewIntent(final Uri uri, boolean share) {
Activity activity = getActivity();
if (activity == null || mCurrentInputUri != null) {
return;
@@ -432,104 +419,47 @@ public class DecryptListFragment
// OpenKeychain's internal viewer
if ("text/plain".equals(metadata.getMimeType())) {
- parseMime(outputUri);
-
- // this is a significant i/o operation, use an asynctask
-// new AsyncTask<Void,Void,Intent>() {
-//
-// @Override
-// protected Intent doInBackground(Void... params) {
-//
-// Activity activity = getActivity();
-// if (activity == null) {
-// return null;
-// }
-//
-// Intent intent = new Intent(Intent.ACTION_VIEW);
-// intent.setDataAndType(outputUri, "text/plain");
-// intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
-// return intent;
-// }
-//
-// @Override
-// protected void onPostExecute(Intent intent) {
-// // for result so we can possibly get a snackbar error from internal viewer
-// Activity activity = getActivity();
-// if (intent == null || activity == null) {
-// return;
-// }
-//
-// LabeledIntent internalIntent = new LabeledIntent(
-// new Intent(intent)
-// .setClass(activity, DisplayTextActivity.class)
-// .putExtra(DisplayTextActivity.EXTRA_METADATA, result),
-// BuildConfig.APPLICATION_ID, R.string.view_internal, R.drawable.ic_launcher);
-//
-// Intent chooserIntent = Intent.createChooser(intent, getString(R.string.intent_show));
-// chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS,
-// new Parcelable[] { internalIntent });
-//
-// activity.startActivity(chooserIntent);
-// }
-//
-// }.execute();
+ if (share) {
+ try {
+ String plaintext = FileHelper.readTextFromUri(activity, outputUri, result.getCharset());
- } else {
- Intent intent = new Intent(Intent.ACTION_VIEW);
- intent.setDataAndType(outputUri, metadata.getMimeType());
- intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ Intent intent = new Intent(Intent.ACTION_SEND);
+ intent.setType(metadata.getMimeType());
+ intent.putExtra(Intent.EXTRA_TEXT, plaintext);
+ startActivity(intent);
- Intent chooserIntent = Intent.createChooser(intent, getString(R.string.intent_show));
- chooserIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
- activity.startActivity(chooserIntent);
- }
-
- }
-
- private void parseMime(final Uri inputUri) {
-
- CryptoOperationHelper.Callback<MimeParsingParcel, MimeParsingResult> callback
- = new CryptoOperationHelper.Callback<MimeParsingParcel, MimeParsingResult>() {
-
- @Override
- public MimeParsingParcel createOperationInput() {
- return new MimeParsingParcel(inputUri, null);
- }
+ } catch (IOException e) {
+ Notify.create(activity, R.string.error_preparing_data, Style.ERROR).show();
+ }
- @Override
- public void onCryptoOperationSuccess(MimeParsingResult result) {
- handleResult(result);
+ return;
}
- @Override
- public void onCryptoOperationCancelled() {
+ Intent intent = new Intent(activity, DisplayTextActivity.class);
+ intent.setAction(Intent.ACTION_VIEW);
+ intent.setDataAndType(outputUri, metadata.getMimeType());
+ intent.putExtra(DisplayTextActivity.EXTRA_METADATA, result);
+ activity.startActivity(intent);
- }
+ } else {
- @Override
- public void onCryptoOperationError(MimeParsingResult result) {
- handleResult(result);
+ Intent intent;
+ if (share) {
+ intent = new Intent(Intent.ACTION_SEND);
+ intent.setType(metadata.getMimeType());
+ intent.putExtra(Intent.EXTRA_STREAM, outputUri);
+ } else {
+ intent = new Intent(Intent.ACTION_VIEW);
+ intent.setDataAndType(outputUri, metadata.getMimeType());
}
- public void handleResult(MimeParsingResult result) {
- // TODO: merge with other log
-// saveKeyResult.getLog().add(result, 0);
-
- mOutputUris = new HashMap<>(result.getTemporaryUris().size());
- for (Uri tempUri : result.getTemporaryUris()) {
- // TODO: use same inputUri for all?
- mOutputUris.put(inputUri, tempUri);
- }
- }
+ intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
- @Override
- public boolean onCryptoSetProgress(String msg, int progress, int max) {
- return false;
- }
- };
+ Intent chooserIntent = Intent.createChooser(intent, getString(R.string.intent_show));
+ chooserIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ activity.startActivity(chooserIntent);
+ }
- CryptoOperationHelper mimeParsingHelper = new CryptoOperationHelper<>(3, this, callback, R.string.progress_uploading);
- mimeParsingHelper.cryptoOperation();
}
@Override
@@ -576,13 +506,17 @@ public class DecryptListFragment
intent.putExtra(LogDisplayFragment.EXTRA_RESULT, result);
activity.startActivity(intent);
return true;
+ case R.id.decrypt_share:
+ displayWithViewIntent(model.mInputUri, true);
+ return true;
case R.id.decrypt_save:
OpenPgpMetadata metadata = result.getDecryptionMetadata();
if (metadata == null) {
return true;
}
mCurrentInputUri = model.mInputUri;
- askForOutputFilename(model.mInputUri, metadata.getFilename(), metadata.getMimeType());
+ FileHelper.saveDocument(this, metadata.getFilename(), model.mInputUri, metadata.getMimeType(),
+ R.string.title_decrypt_to_file, R.string.specify_file_to_decrypt_to, REQUEST_CODE_OUTPUT);
return true;
case R.id.decrypt_delete:
deleteFile(activity, model.mInputUri);
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DeleteKeyDialogActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DeleteKeyDialogActivity.java
index b22053df1..478259133 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DeleteKeyDialogActivity.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DeleteKeyDialogActivity.java
@@ -48,7 +48,6 @@ import org.sufficientlysecure.keychain.service.RevokeKeyringParcel;
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper;
import org.sufficientlysecure.keychain.ui.dialog.CustomAlertDialogBuilder;
-import org.sufficientlysecure.keychain.ui.util.Notify;
import org.sufficientlysecure.keychain.ui.util.ThemeChanger;
import org.sufficientlysecure.keychain.util.Log;
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EditKeyFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EditKeyFragment.java
index 4769e68d8..07b0a12d3 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EditKeyFragment.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EditKeyFragment.java
@@ -280,7 +280,7 @@ public class EditKeyFragment extends QueueingCryptoOperationFragment<SaveKeyring
case LOADER_ID_USER_IDS: {
Uri baseUri = UserPackets.buildUserIdsUri(mDataUri);
return new CursorLoader(getActivity(), baseUri,
- UserIdsAdapter.USER_IDS_PROJECTION, null, null, null);
+ UserIdsAdapter.USER_PACKETS_PROJECTION, null, null, null);
}
case LOADER_ID_SUBKEYS: {
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptDecryptOverviewFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptDecryptOverviewFragment.java
index fc72a6c9f..84660ca7a 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptDecryptOverviewFragment.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptDecryptOverviewFragment.java
@@ -83,11 +83,7 @@ public class EncryptDecryptOverviewFragment extends Fragment {
mDecryptFile.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
- FileHelper.openDocument(EncryptDecryptOverviewFragment.this, "*/*", REQUEST_CODE_INPUT);
- } else {
- FileHelper.openFile(EncryptDecryptOverviewFragment.this, null, "*/*", REQUEST_CODE_INPUT);
- }
+ FileHelper.openDocument(EncryptDecryptOverviewFragment.this, null, "*/*", false, REQUEST_CODE_INPUT);
}
});
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptFilesFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptFilesFragment.java
index 63d37f296..8572a5712 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptFilesFragment.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptFilesFragment.java
@@ -18,7 +18,6 @@
package org.sufficientlysecure.keychain.ui;
-import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
@@ -196,13 +195,9 @@ public class EncryptFilesFragment
}
private void addInputUri() {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
- FileHelper.openDocument(EncryptFilesFragment.this, "*/*", true, REQUEST_CODE_INPUT);
- } else {
- FileHelper.openFile(EncryptFilesFragment.this, mFilesAdapter.getModelCount() == 0 ?
- null : mFilesAdapter.getModelItem(mFilesAdapter.getModelCount() - 1).inputUri,
- "*/*", REQUEST_CODE_INPUT);
- }
+ FileHelper.openDocument(EncryptFilesFragment.this, mFilesAdapter.getModelCount() == 0 ?
+ null : mFilesAdapter.getModelItem(mFilesAdapter.getModelCount() - 1).inputUri,
+ "*/*", true, REQUEST_CODE_INPUT);
}
private void addInputUri(Uri inputUri) {
@@ -230,19 +225,8 @@ public class EncryptFilesFragment
(mEncryptFilenames ? "1" : FileHelper.getFilename(getActivity(), model.inputUri))
+ (mUseArmor ? Constants.FILE_EXTENSION_ASC : Constants.FILE_EXTENSION_PGP_MAIN);
Uri inputUri = model.inputUri;
- saveDocumentIntent(targetName, inputUri);
- }
-
- private void saveDocumentIntent(String targetName, Uri inputUri) {
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
- File file = new File(inputUri.getPath());
- File parentDir = file.exists() ? file.getParentFile() : Constants.Path.APP_DIR;
- File targetFile = new File(parentDir, targetName);
- FileHelper.saveFile(this, getString(R.string.title_encrypt_to_file),
- getString(R.string.specify_file_to_encrypt_to), targetFile, REQUEST_CODE_OUTPUT);
- } else {
- FileHelper.saveDocument(this, "*/*", targetName, REQUEST_CODE_OUTPUT);
- }
+ FileHelper.saveDocument(this, targetName, inputUri,
+ R.string.title_encrypt_to_file, R.string.specify_file_to_encrypt_to, REQUEST_CODE_OUTPUT);
}
public void addFile(Intent data) {
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysFileFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysFileFragment.java
index 538fa16c7..746c75600 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysFileFragment.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysFileFragment.java
@@ -64,8 +64,8 @@ public class ImportKeysFileFragment extends Fragment {
// open .asc or .gpg files
// setting it to text/plain prevents Cyanogenmod's file manager from selecting asc
// or gpg types!
- FileHelper.openFile(ImportKeysFileFragment.this, Uri.fromFile(Constants.Path.APP_DIR),
- "*/*", REQUEST_CODE_FILE);
+ FileHelper.openDocument(ImportKeysFileFragment.this,
+ Uri.fromFile(Constants.Path.APP_DIR), "*/*", false, REQUEST_CODE_FILE);
}
});
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java
index 8c46876be..ce6994ba4 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java
@@ -18,11 +18,6 @@
package org.sufficientlysecure.keychain.ui;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.HashMap;
-
import android.animation.ObjectAnimator;
import android.annotation.TargetApi;
import android.app.Activity;
@@ -70,16 +65,19 @@ import org.sufficientlysecure.keychain.service.ConsolidateInputParcel;
import org.sufficientlysecure.keychain.service.ImportKeyringParcel;
import org.sufficientlysecure.keychain.ui.adapter.KeyAdapter;
import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper;
-import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
import org.sufficientlysecure.keychain.ui.util.FormattingUtils;
+import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
import org.sufficientlysecure.keychain.ui.util.Notify;
-import org.sufficientlysecure.keychain.ui.util.Notify.Style;
import org.sufficientlysecure.keychain.util.FabContainer;
import org.sufficientlysecure.keychain.util.Log;
import org.sufficientlysecure.keychain.util.Preferences;
import se.emilsjolander.stickylistheaders.StickyListHeadersAdapter;
import se.emilsjolander.stickylistheaders.StickyListHeadersListView;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+
/**
* Public key list with sticky list headers. It does _not_ extend ListFragment because it uses
* StickyListHeaders library which does not extend upon ListView.
@@ -536,7 +534,7 @@ public class KeyListFragment extends LoaderFragment
);
if (cursor == null) {
- Notify.create(activity, R.string.error_loading_keys, Style.ERROR);
+ Notify.create(activity, R.string.error_loading_keys, Notify.Style.ERROR);
return;
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/LogDisplayFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/LogDisplayFragment.java
index 1cd1a3099..43c8d2643 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/LogDisplayFragment.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/LogDisplayFragment.java
@@ -18,11 +18,12 @@
package org.sufficientlysecure.keychain.ui;
+import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
+import android.net.Uri;
import android.os.Bundle;
-import android.os.Parcel;
import android.support.v4.app.ListFragment;
import android.util.TypedValue;
import android.view.LayoutInflater;
@@ -37,19 +38,19 @@ import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.TextView;
-import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.operations.results.OperationResult;
import org.sufficientlysecure.keychain.operations.results.OperationResult.LogEntryParcel;
import org.sufficientlysecure.keychain.operations.results.OperationResult.LogLevel;
import org.sufficientlysecure.keychain.operations.results.OperationResult.SubLogEntryParcel;
+import org.sufficientlysecure.keychain.provider.TemporaryStorageProvider;
import org.sufficientlysecure.keychain.ui.util.FormattingUtils;
-import org.sufficientlysecure.keychain.util.FileHelper;
-import org.sufficientlysecure.keychain.util.Log;
+import org.sufficientlysecure.keychain.ui.util.Notify;
+import org.sufficientlysecure.keychain.ui.util.Notify.Style;
+
+import java.io.IOException;
+import java.io.OutputStream;
-import java.io.File;
-import java.io.FileNotFoundException;
-import java.io.PrintWriter;
public class LogDisplayFragment extends ListFragment implements OnItemClickListener {
@@ -60,6 +61,8 @@ public class LogDisplayFragment extends ListFragment implements OnItemClickListe
public static final String EXTRA_RESULT = "log";
protected int mTextColor;
+ private Uri mLogTempFile;
+
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -118,170 +121,40 @@ public class LogDisplayFragment extends ListFragment implements OnItemClickListe
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_log_display_export_log:
- exportLog();
+ shareLog();
break;
}
return super.onOptionsItemSelected(item);
}
- private void exportLog() {
- showExportLogDialog(new File(Constants.Path.APP_DIR, "export.log"));
- }
-
- private void writeToLogFile(final OperationResult.OperationLog operationLog, final File f) {
- OperationResult.OperationLog currLog = new OperationResult.OperationLog();
- currLog.add(OperationResult.LogType.MSG_EXPORT_LOG, 0);
-
- boolean error = false;
-
- PrintWriter pw = null;
- try {
- pw = new PrintWriter(f);
- pw.print(getPrintableOperationLog(operationLog, ""));
- if (pw.checkError()) {//IOException
- Log.e(Constants.TAG, "Log Export I/O Exception " + f.getAbsolutePath());
- currLog.add(OperationResult.LogType.MSG_EXPORT_LOG_EXPORT_ERROR_WRITING, 1);
- error = true;
- }
- } catch (FileNotFoundException e) {
- Log.e(Constants.TAG, "File not found for exporting log " + f.getAbsolutePath());
- currLog.add(OperationResult.LogType.MSG_EXPORT_LOG_EXPORT_ERROR_FOPEN, 1);
- error = true;
- }
- if (pw != null) {
- pw.close();
- if (!error && pw.checkError()) {//check if it is only pw.close() which generated error
- currLog.add(OperationResult.LogType.MSG_EXPORT_LOG_EXPORT_ERROR_WRITING, 1);
- error = true;
- }
- }
-
- if (!error) {
- currLog.add(OperationResult.LogType.MSG_EXPORT_LOG_EXPORT_SUCCESS, 1);
- }
-
- int opResultCode = error ? OperationResult.RESULT_ERROR : OperationResult.RESULT_OK;
- OperationResult opResult = new LogExportResult(opResultCode, currLog);
- opResult.createNotify(getActivity()).show();
- }
-
- /**
- * returns an indented String of an entire OperationLog
- *
- * @param opLog log to be converted to indented, printable format
- * @param basePadding padding to add at the start of all log entries, made for use with SubLogs
- * @return printable, indented version of passed operationLog
- */
- private String getPrintableOperationLog(OperationResult.OperationLog opLog, String basePadding) {
- String log = "";
- for (LogEntryParcel anOpLog : opLog) {
- log += getPrintableLogEntry(anOpLog, basePadding) + "\n";
- }
- log = log.substring(0, log.length() - 1);//gets rid of extra new line
- return log;
- }
-
- /**
- * returns an indented String of a LogEntryParcel including any sub-logs it may contain
- *
- * @param entryParcel log entryParcel whose String representation is to be obtained
- * @return indented version of passed log entryParcel in a readable format
- */
- private String getPrintableLogEntry(OperationResult.LogEntryParcel entryParcel,
- String basePadding) {
-
- final String indent = " ";//4 spaces = 1 Indent level
-
- String padding = basePadding;
- for (int i = 0; i < entryParcel.mIndent; i++) {
- padding += indent;
- }
- String logText = padding;
-
- switch (entryParcel.mType.mLevel) {
- case DEBUG:
- logText += "[DEBUG]";
- break;
- case INFO:
- logText += "[INFO]";
- break;
- case WARN:
- logText += "[WARN]";
- break;
- case ERROR:
- logText += "[ERROR]";
- break;
- case START:
- logText += "[START]";
- break;
- case OK:
- logText += "[OK]";
- break;
- case CANCELLED:
- logText += "[CANCELLED]";
- break;
- }
+ private void shareLog() {
- // special case: first parameter may be a quantity
- if (entryParcel.mParameters != null && entryParcel.mParameters.length > 0
- && entryParcel.mParameters[0] instanceof Integer) {
- logText += getResources().getQuantityString(entryParcel.mType.getMsgId(),
- (Integer) entryParcel.mParameters[0],
- entryParcel.mParameters);
- } else {
- logText += getResources().getString(entryParcel.mType.getMsgId(),
- entryParcel.mParameters);
+ Activity activity = getActivity();
+ if (activity == null) {
+ return;
}
- if (entryParcel instanceof SubLogEntryParcel) {
- OperationResult subResult = ((SubLogEntryParcel) entryParcel).getSubResult();
- LogEntryParcel subEntry = subResult.getLog().getLast();
- if (subEntry != null) {
- //the first line of log of subResult is same as entryParcel, so replace logText
- logText = getPrintableOperationLog(subResult.getLog(), padding);
+ String log = mResult.getLog().getPrintableOperationLog(getResources(), 0);
+
+ // if there is no log temp file yet, create one
+ if (mLogTempFile == null) {
+ mLogTempFile = TemporaryStorageProvider.createFile(getActivity(), "openkeychain_log.txt", "text/plain");
+ try {
+ OutputStream outputStream = activity.getContentResolver().openOutputStream(mLogTempFile);
+ outputStream.write(log.getBytes());
+ } catch (IOException e) {
+ Notify.create(activity, R.string.error_log_share_internal, Style.ERROR).show();
+ return;
}
}
- return logText;
- }
-
- private void showExportLogDialog(final File exportFile) {
-
- String title = this.getString(R.string.title_export_log);
-
- String message = this.getString(R.string.specify_file_to_export_log_to);
-
- FileHelper.saveFile(new FileHelper.FileDialogCallback() {
- @Override
- public void onFileSelected(File file, boolean checked) {
- writeToLogFile(mResult.getLog(), file);
- }
- }, this.getActivity().getSupportFragmentManager(), title, message, exportFile, null);
- }
-
- private static class LogExportResult extends OperationResult {
-
- public static Creator<LogExportResult> CREATOR = new Creator<LogExportResult>() {
- public LogExportResult createFromParcel(final Parcel source) {
- return new LogExportResult(source);
- }
-
- public LogExportResult[] newArray(final int size) {
- return new LogExportResult[size];
- }
- };
-
- public LogExportResult(int result, OperationLog log) {
- super(result, log);
- }
+ Intent intent = new Intent(Intent.ACTION_SEND);
+ intent.putExtra(Intent.EXTRA_STREAM, mLogTempFile);
+ intent.setType("text/plain");
+ intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ startActivity(intent);
- /**
- * trivial but necessary to implement the Parcelable protocol.
- */
- public LogExportResult(Parcel source) {
- super(source);
- }
}
@Override
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/NfcOperationActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/NfcOperationActivity.java
index e6591595e..b811b218e 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/NfcOperationActivity.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/NfcOperationActivity.java
@@ -21,7 +21,6 @@
package org.sufficientlysecure.keychain.ui;
import android.content.Intent;
-import android.content.pm.ActivityInfo;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.View;
@@ -50,15 +49,11 @@ import org.sufficientlysecure.keychain.util.Preferences;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Arrays;
-import java.util.Date;
/**
* This class provides a communication interface to OpenPGP applications on ISO SmartCard compliant
* NFC devices.
- * <p/>
* For the full specs, see http://g10code.com/docs/openpgp-card-2.0.pdf
- * NOTE: If no CryptoInputParcel is passed via EXTRA_CRYPTO_INPUT, the CryptoInputParcel is created
- * internally and is NOT meant to be used by signing operations before adding signature time
*/
public class NfcOperationActivity extends BaseNfcActivity {
@@ -84,8 +79,8 @@ public class NfcOperationActivity extends BaseNfcActivity {
@Override
protected void initTheme() {
mThemeChanger = new ThemeChanger(this);
- mThemeChanger.setThemes(R.style.Theme_Keychain_Light_Dialog_SecurityToken,
- R.style.Theme_Keychain_Dark_Dialog_SecurityToken);
+ mThemeChanger.setThemes(R.style.Theme_Keychain_Light_Dialog,
+ R.style.Theme_Keychain_Dark_Dialog);
mThemeChanger.changeTheme();
}
@@ -101,13 +96,6 @@ public class NfcOperationActivity extends BaseNfcActivity {
mInputParcel = getIntent().getParcelableExtra(EXTRA_CRYPTO_INPUT);
- if (mInputParcel == null) {
- // for compatibility when used from OpenPgpService
- // (or any place other than CryptoOperationHelper)
- // NOTE: This CryptoInputParcel cannot be used for signing without adding signature time
- mInputParcel = new CryptoInputParcel();
- }
-
setTitle(R.string.nfc_text);
vAnimator = (ViewAnimator) findViewById(R.id.view_animator);
@@ -163,9 +151,8 @@ public class NfcOperationActivity extends BaseNfcActivity {
break;
}
case NFC_SIGN: {
- if (mInputParcel.getSignatureTime() == null) {
- mInputParcel.addSignatureTime(new Date());
- }
+ mInputParcel.addSignatureTime(mRequiredInput.mSignatureTime);
+
for (int i = 0; i < mRequiredInput.mInputData.length; i++) {
byte[] hash = mRequiredInput.mInputData[i];
int algo = mRequiredInput.mSignAlgos[i];
@@ -240,7 +227,7 @@ public class NfcOperationActivity extends BaseNfcActivity {
throw new IOException("Inappropriate key flags for smart card key.");
}
- // TODO: Is this really needed?
+ // TODO: Is this really used anywhere?
mInputParcel.addCryptoData(subkeyBytes, cardSerialNumber);
}
@@ -261,15 +248,13 @@ public class NfcOperationActivity extends BaseNfcActivity {
protected void onNfcPostExecute() throws IOException {
if (mServiceIntent != null) {
// if we're triggered by OpenPgpService
+ // save updated cryptoInputParcel in cache
CryptoInputParcelCacheService.addCryptoInputParcel(this, mServiceIntent, mInputParcel);
- mServiceIntent.putExtra(EXTRA_CRYPTO_INPUT,
- getIntent().getParcelableExtra(EXTRA_CRYPTO_INPUT));
setResult(RESULT_OK, mServiceIntent);
} else {
Intent result = new Intent();
+ // send back the CryptoInputParcel we received
result.putExtra(RESULT_CRYPTO_INPUT, mInputParcel);
- // send back the CryptoInputParcel we receive, to conform with the pattern in
- // CryptoOperationHelper
setResult(RESULT_OK, result);
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/OrbotRequiredDialogActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/OrbotRequiredDialogActivity.java
index d1c247a76..0e70cda14 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/OrbotRequiredDialogActivity.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/OrbotRequiredDialogActivity.java
@@ -18,12 +18,22 @@
package org.sufficientlysecure.keychain.ui;
+import android.app.ProgressDialog;
import android.content.Intent;
import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.RemoteException;
import android.support.v4.app.FragmentActivity;
+import android.view.ContextThemeWrapper;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.compatibility.DialogFragmentWorkaround;
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
+import org.sufficientlysecure.keychain.ui.util.ThemeChanger;
+import org.sufficientlysecure.keychain.util.Log;
import org.sufficientlysecure.keychain.util.ParcelableProxy;
import org.sufficientlysecure.keychain.util.orbot.OrbotHelper;
@@ -34,8 +44,14 @@ import org.sufficientlysecure.keychain.util.orbot.OrbotHelper;
public class OrbotRequiredDialogActivity extends FragmentActivity
implements OrbotHelper.DialogActions {
+ public static final int MESSAGE_ORBOT_STARTED = 1;
+ public static final int MESSAGE_ORBOT_IGNORE = 2;
+ public static final int MESSAGE_DIALOG_CANCEL = 3;
+
// if suppplied and true will start Orbot directly without showing dialog
public static final String EXTRA_START_ORBOT = "start_orbot";
+ // used for communicating results when triggered from a service
+ public static final String EXTRA_MESSENGER = "messenger";
// to provide any previous crypto input into which proxy preference is merged
public static final String EXTRA_CRYPTO_INPUT = "extra_crypto_input";
@@ -43,6 +59,9 @@ public class OrbotRequiredDialogActivity extends FragmentActivity
public static final String RESULT_CRYPTO_INPUT = "result_crypto_input";
private CryptoInputParcel mCryptoInputParcel;
+ private Messenger mMessenger;
+
+ private ProgressDialog mShowOrbotProgressDialog;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -54,9 +73,16 @@ public class OrbotRequiredDialogActivity extends FragmentActivity
mCryptoInputParcel = new CryptoInputParcel();
}
+ mMessenger = getIntent().getParcelableExtra(EXTRA_MESSENGER);
+
boolean startOrbotDirect = getIntent().getBooleanExtra(EXTRA_START_ORBOT, false);
if (startOrbotDirect) {
- OrbotHelper.bestPossibleOrbotStart(this, this);
+ ContextThemeWrapper theme = ThemeChanger.getDialogThemeWrapper(this);
+ mShowOrbotProgressDialog = new ProgressDialog(theme);
+ mShowOrbotProgressDialog.setTitle(R.string.progress_starting_orbot);
+ mShowOrbotProgressDialog.setCancelable(false);
+ mShowOrbotProgressDialog.show();
+ OrbotHelper.bestPossibleOrbotStart(this, this, false);
} else {
showDialog();
}
@@ -84,13 +110,32 @@ public class OrbotRequiredDialogActivity extends FragmentActivity
super.onActivityResult(requestCode, resultCode, data);
switch (requestCode) {
case OrbotHelper.START_TOR_RESULT: {
- onOrbotStarted(); // assumption that orbot was started, no way to tell for sure
+ dismissOrbotProgressDialog();
+ // unfortunately, this result is returned immediately and not when Orbot is started
+ // 10s is approximately the longest time Orbot has taken to start
+ new Handler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ onOrbotStarted(); // assumption that orbot was started
+ }
+ }, 10000);
}
}
}
+ /**
+ * for when Orbot is started without showing the dialog by the EXTRA_START_ORBOT intent extra
+ */
+ private void dismissOrbotProgressDialog() {
+ if (mShowOrbotProgressDialog != null) {
+ mShowOrbotProgressDialog.dismiss();
+ }
+ }
+
@Override
public void onOrbotStarted() {
+ dismissOrbotProgressDialog();
+ sendMessage(MESSAGE_ORBOT_STARTED);
Intent intent = new Intent();
// send back unmodified CryptoInputParcel for a retry
intent.putExtra(RESULT_CRYPTO_INPUT, mCryptoInputParcel);
@@ -100,6 +145,7 @@ public class OrbotRequiredDialogActivity extends FragmentActivity
@Override
public void onNeutralButton() {
+ sendMessage(MESSAGE_ORBOT_IGNORE);
Intent intent = new Intent();
mCryptoInputParcel.addParcelableProxy(ParcelableProxy.getForNoProxy());
intent.putExtra(RESULT_CRYPTO_INPUT, mCryptoInputParcel);
@@ -109,6 +155,19 @@ public class OrbotRequiredDialogActivity extends FragmentActivity
@Override
public void onCancel() {
+ sendMessage(MESSAGE_DIALOG_CANCEL);
finish();
}
+
+ private void sendMessage(int what) {
+ if (mMessenger != null) {
+ Message msg = Message.obtain();
+ msg.what = what;
+ try {
+ mMessenger.send(msg);
+ } catch (RemoteException e) {
+ Log.e(Constants.TAG, "Could not deliver message", e);
+ }
+ }
+ }
} \ No newline at end of file
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PassphraseDialogActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PassphraseDialogActivity.java
index d7224bd04..e71349880 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PassphraseDialogActivity.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PassphraseDialogActivity.java
@@ -71,6 +71,7 @@ import org.sufficientlysecure.keychain.util.Preferences;
* internally and is NOT meant to be used by signing operations before adding a signature time
*/
public class PassphraseDialogActivity extends FragmentActivity {
+
public static final String RESULT_CRYPTO_INPUT = "result_data";
public static final String EXTRA_REQUIRED_INPUT = "required_input";
@@ -261,6 +262,9 @@ public class PassphraseDialogActivity extends FragmentActivity {
case DIVERT_TO_CARD:
message = getString(R.string.yubikey_pin_for, userId);
break;
+ // special case: empty passphrase just returns the empty passphrase
+ case PASSPHRASE_EMPTY:
+ finishCaching(new Passphrase(""));
default:
throw new AssertionError("Unhandled SecretKeyType (should not happen)");
}
@@ -280,51 +284,42 @@ public class PassphraseDialogActivity extends FragmentActivity {
mPassphraseText.setText(message);
- if (keyType == CanonicalizedSecretKey.SecretKeyType.PATTERN) {
- // start pattern dialog and show progress circle here...
-// Intent patternActivity = new Intent(getActivity(), LockPatternActivity.class);
-// patternActivity.putExtra(LockPatternActivity.EXTRA_PATTERN, "123");
-// startActivityForResult(patternActivity, REQUEST_CODE_ENTER_PATTERN);
- mInput.setVisibility(View.INVISIBLE);
- mProgress.setVisibility(View.VISIBLE);
- } else {
- // Hack to open keyboard.
- // This is the only method that I found to work across all Android versions
- // http://turbomanage.wordpress.com/2012/05/02/show-soft-keyboard-automatically-when-edittext-receives-focus/
- // Notes: * onCreateView can't be used because we want to add buttons to the dialog
- // * opening in onActivityCreated does not work on Android 4.4
- mPassphraseEditText.setOnFocusChangeListener(new View.OnFocusChangeListener() {
- @Override
- public void onFocusChange(View v, boolean hasFocus) {
- mPassphraseEditText.post(new Runnable() {
- @Override
- public void run() {
- if (getActivity() == null || mPassphraseEditText == null) {
- return;
- }
- InputMethodManager imm = (InputMethodManager) getActivity()
- .getSystemService(Context.INPUT_METHOD_SERVICE);
- imm.showSoftInput(mPassphraseEditText, InputMethodManager.SHOW_IMPLICIT);
+ // Hack to open keyboard.
+ // This is the only method that I found to work across all Android versions
+ // http://turbomanage.wordpress.com/2012/05/02/show-soft-keyboard-automatically-when-edittext-receives-focus/
+ // Notes: * onCreateView can't be used because we want to add buttons to the dialog
+ // * opening in onActivityCreated does not work on Android 4.4
+ mPassphraseEditText.setOnFocusChangeListener(new View.OnFocusChangeListener() {
+ @Override
+ public void onFocusChange(View v, boolean hasFocus) {
+ mPassphraseEditText.post(new Runnable() {
+ @Override
+ public void run() {
+ if (getActivity() == null || mPassphraseEditText == null) {
+ return;
}
- });
- }
- });
- mPassphraseEditText.requestFocus();
-
- mPassphraseEditText.setImeActionLabel(getString(android.R.string.ok), EditorInfo.IME_ACTION_DONE);
- mPassphraseEditText.setOnEditorActionListener(this);
-
- if ((keyType == CanonicalizedSecretKey.SecretKeyType.DIVERT_TO_CARD && Preferences.getPreferences(activity).useNumKeypadForYubiKeyPin())
- || keyType == CanonicalizedSecretKey.SecretKeyType.PIN) {
- mPassphraseEditText.setInputType(InputType.TYPE_CLASS_NUMBER);
- mPassphraseEditText.setTransformationMethod(PasswordTransformationMethod.getInstance());
- } else {
- mPassphraseEditText.setRawInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
+ InputMethodManager imm = (InputMethodManager) getActivity()
+ .getSystemService(Context.INPUT_METHOD_SERVICE);
+ imm.showSoftInput(mPassphraseEditText, InputMethodManager.SHOW_IMPLICIT);
+ }
+ });
}
+ });
+ mPassphraseEditText.requestFocus();
+ mPassphraseEditText.setImeActionLabel(getString(android.R.string.ok), EditorInfo.IME_ACTION_DONE);
+ mPassphraseEditText.setOnEditorActionListener(this);
+
+ if ((keyType == CanonicalizedSecretKey.SecretKeyType.DIVERT_TO_CARD && Preferences.getPreferences(activity).useNumKeypadForYubiKeyPin())
+ || keyType == CanonicalizedSecretKey.SecretKeyType.PIN) {
+ mPassphraseEditText.setInputType(InputType.TYPE_CLASS_NUMBER);
mPassphraseEditText.setTransformationMethod(PasswordTransformationMethod.getInstance());
+ } else {
+ mPassphraseEditText.setRawInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
}
+ mPassphraseEditText.setTransformationMethod(PasswordTransformationMethod.getInstance());
+
AlertDialog dialog = alert.create();
dialog.setButton(DialogInterface.BUTTON_POSITIVE,
activity.getString(R.string.btn_unlock), (DialogInterface.OnClickListener) null);
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PassphraseWizardActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PassphraseWizardActivity.java
deleted file mode 100644
index e55494145..000000000
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PassphraseWizardActivity.java
+++ /dev/null
@@ -1,577 +0,0 @@
-/*
- * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
-
-package org.sufficientlysecure.keychain.ui;
-
-import android.annotation.TargetApi;
-import android.support.v7.app.AlertDialog;
-import android.app.PendingIntent;
-import android.content.DialogInterface;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.nfc.FormatException;
-import android.nfc.NdefMessage;
-import android.nfc.NdefRecord;
-import android.nfc.NfcAdapter;
-import android.nfc.Tag;
-import android.nfc.tech.Ndef;
-import android.os.Build;
-import android.os.Bundle;
-import android.provider.Settings;
-import android.support.v4.app.Fragment;
-import android.support.v4.app.FragmentActivity;
-import android.support.v4.app.FragmentTransaction;
-import android.text.TextUtils;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.EditText;
-import android.widget.TextView;
-import android.widget.Toast;
-
-import org.sufficientlysecure.keychain.Constants;
-import org.sufficientlysecure.keychain.R;
-import org.sufficientlysecure.keychain.util.Log;
-
-import java.io.IOException;
-import java.io.UnsupportedEncodingException;
-import java.security.SecureRandom;
-import java.util.Arrays;
-import java.util.List;
-
-@TargetApi(Build.VERSION_CODES.HONEYCOMB)
-public class PassphraseWizardActivity extends FragmentActivity {
-//public class PassphraseWizardActivity extends FragmentActivity implements LockPatternView.OnPatternListener {
- //create or authenticate
- public String selectedAction;
- //for lockpattern
- public static char[] pattern;
- private static String passphrase = "";
- //nfc string
- private static byte[] output = new byte[8];
-
- public static final String CREATE_METHOD = "create";
- public static final String AUTHENTICATION = "authenticate";
-
- NfcAdapter adapter;
- PendingIntent pendingIntent;
- IntentFilter writeTagFilters[];
- boolean writeMode;
- Tag myTag;
- boolean writeNFC = false;
- boolean readNFC = false;
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- if (getActionBar() != null) {
- getActionBar().setTitle(R.string.unlock_method);
- }
-
- selectedAction = getIntent().getAction();
- if (savedInstanceState == null) {
- SelectMethods selectMethods = new SelectMethods();
- FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
- transaction.add(R.id.fragmentContainer, selectMethods).commit();
- }
- setContentView(R.layout.passphrase_wizard);
-
- adapter = NfcAdapter.getDefaultAdapter(this);
- if (adapter != null) {
- pendingIntent = PendingIntent.getActivity(this, 0, new Intent(this, PassphraseWizardActivity.class).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), 0);
- IntentFilter tagDetected = new IntentFilter(NfcAdapter.ACTION_TAG_DISCOVERED);
- tagDetected.addCategory(Intent.CATEGORY_DEFAULT);
- writeTagFilters = new IntentFilter[]{tagDetected};
- }
- }
-
- public void noPassphrase(View view) {
- passphrase = "";
- Toast.makeText(this, R.string.no_passphrase_set, Toast.LENGTH_SHORT).show();
- this.finish();
- }
-
- public void passphrase(View view) {
- Passphrase passphrase = new Passphrase();
- FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
- transaction.replace(R.id.fragmentContainer, passphrase).addToBackStack(null).commit();
- }
-
- public void startLockpattern(View view) {
- if (getActionBar() != null) {
- getActionBar().setTitle(R.string.draw_lockpattern);
- }
-// LockPatternFragmentOld lpf = LockPatternFragmentOld.newInstance(selectedAction);
-// LockPatternFragment lpf = LockPatternFragment.newInstance("asd");
-
-// FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
-// transaction.replace(R.id.fragmentContainer, lpf).addToBackStack(null).commit();
- }
-
- public void cancel(View view) {
- this.finish();
- }
-
- public void savePassphrase(View view) {
- EditText passphrase = (EditText) findViewById(R.id.passphrase);
- passphrase.setError(null);
- String pw = passphrase.getText().toString();
- //check and save passphrase
- if (selectedAction.equals(CREATE_METHOD)) {
- EditText passphraseAgain = (EditText) findViewById(R.id.passphraseAgain);
- passphraseAgain.setError(null);
- String pwAgain = passphraseAgain.getText().toString();
-
- if (!TextUtils.isEmpty(pw)) {
- if (!TextUtils.isEmpty(pwAgain)) {
- if (pw.equals(pwAgain)) {
- PassphraseWizardActivity.passphrase = pw;
- Toast.makeText(this, getString(R.string.passphrase_saved), Toast.LENGTH_SHORT).show();
- this.finish();
- } else {
- passphrase.setError(getString(R.string.passphrase_invalid));
- passphrase.requestFocus();
- }
- } else {
- passphraseAgain.setError(getString(R.string.missing_passphrase));
- passphraseAgain.requestFocus();
- }
- } else {
- passphrase.setError(getString(R.string.missing_passphrase));
- passphrase.requestFocus();
- }
- }
- //check for right passphrase
- if (selectedAction.equals(AUTHENTICATION)) {
- if (pw.equals(PassphraseWizardActivity.passphrase)) {
- Toast.makeText(this, getString(R.string.unlocked), Toast.LENGTH_SHORT).show();
- this.finish();
- } else {
- passphrase.setError(getString(R.string.passphrase_invalid));
- passphrase.requestFocus();
- }
- }
- }
-
- public void NFC(View view) {
- if (adapter != null) {
- if (getActionBar() != null) {
- getActionBar().setTitle(R.string.nfc_title);
- }
- NFCFragment nfc = new NFCFragment();
- FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
- transaction.replace(R.id.fragmentContainer, nfc).addToBackStack(null).commit();
-
- //if you want to create a new method or just authenticate
- if (CREATE_METHOD.equals(selectedAction)) {
- writeNFC = true;
- } else if (AUTHENTICATION.equals(selectedAction)) {
- readNFC = true;
- }
-
- if (!adapter.isEnabled()) {
- showAlertDialog(getString(R.string.enable_nfc), true);
- }
- } else {
- showAlertDialog(getString(R.string.no_nfc_support), false);
- }
- }
-
- @Override
- protected void onNewIntent(Intent intent) {
- if (NfcAdapter.ACTION_TAG_DISCOVERED.equals(intent.getAction())) {
- myTag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
-
- if (writeNFC && CREATE_METHOD.equals(selectedAction)) {
- //write new password on NFC tag
- try {
- if (myTag != null) {
- write(myTag);
- writeNFC = false; //just write once
- Toast.makeText(this, R.string.nfc_write_succesful, Toast.LENGTH_SHORT).show();
- //advance to lockpattern
-// LockPatternFragmentOld lpf = LockPatternFragmentOld.newInstance(selectedAction);
-// FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
-// transaction.replace(R.id.fragmentContainer, lpf).addToBackStack(null).commit();
- }
- } catch (IOException | FormatException e) {
- Log.e(Constants.TAG, "Failed to write on NFC tag", e);
- }
-
- } else if (readNFC && AUTHENTICATION.equals(selectedAction)) {
- //read pw from NFC tag
- try {
- if (myTag != null) {
- //if tag detected, read tag
- String pwtag = read(myTag);
- if (output != null && pwtag.equals(output.toString())) {
-
- //passwort matches, go to next view
- Toast.makeText(this, R.string.passphrases_match + "!", Toast.LENGTH_SHORT).show();
-
-// LockPatternFragmentOld lpf = LockPatternFragmentOld.newInstance(selectedAction);
-// FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
-// transaction.replace(R.id.fragmentContainer, lpf).addToBackStack(null).commit();
- readNFC = false; //just once
- } else {
- //passwort doesnt match
- TextView nfc = (TextView) findViewById(R.id.nfcText);
- nfc.setText(R.string.nfc_wrong_tag);
- }
- }
- } catch (IOException | FormatException e) {
- Log.e(Constants.TAG, "Failed to read NFC tag", e);
- }
- }
- }
- }
-
- private void write(Tag tag) throws IOException, FormatException {
- //generate new random key and write them on the tag
- SecureRandom sr = new SecureRandom();
- sr.nextBytes(output);
- NdefRecord[] records = {createRecord(output.toString())};
- NdefMessage message = new NdefMessage(records);
- Ndef ndef = Ndef.get(tag);
- ndef.connect();
- ndef.writeNdefMessage(message);
- ndef.close();
- }
-
- private String read(Tag tag) throws IOException, FormatException {
- //read string from tag
- String password = null;
- Ndef ndef = Ndef.get(tag);
- ndef.connect();
- NdefMessage ndefMessage = ndef.getCachedNdefMessage();
-
- NdefRecord[] records = ndefMessage.getRecords();
- for (NdefRecord ndefRecord : records) {
- if (ndefRecord.getTnf() == NdefRecord.TNF_WELL_KNOWN && Arrays.equals(ndefRecord.getType(), NdefRecord.RTD_TEXT)) {
- try {
- password = readText(ndefRecord);
- } catch (UnsupportedEncodingException e) {
- Log.e(Constants.TAG, "Failed to read password from tag", e);
- }
- }
- }
- ndef.close();
- return password;
- }
-
- private String readText(NdefRecord record) throws UnsupportedEncodingException {
- //low-level method for reading nfc
- byte[] payload = record.getPayload();
- String textEncoding = ((payload[0] & 128) == 0) ? "UTF-8" : "UTF-16";
- int languageCodeLength = payload[0] & 0063;
- return new String(payload, languageCodeLength + 1, payload.length - languageCodeLength - 1, textEncoding);
- }
-
- private NdefRecord createRecord(String text) throws UnsupportedEncodingException {
- //low-level method for writing nfc
- String lang = "en";
- byte[] textBytes = text.getBytes();
- byte[] langBytes = lang.getBytes("US-ASCII");
- int langLength = langBytes.length;
- int textLength = textBytes.length;
- byte[] payload = new byte[1 + langLength + textLength];
-
- // set status byte (see NDEF spec for actual bits)
- payload[0] = (byte) langLength;
- // copy langbytes and textbytes into payload
- System.arraycopy(langBytes, 0, payload, 1, langLength);
- System.arraycopy(textBytes, 0, payload, 1 + langLength, textLength);
- return new NdefRecord(NdefRecord.TNF_WELL_KNOWN, NdefRecord.RTD_TEXT, new byte[0], payload);
- }
-
- public void showAlertDialog(String message, boolean nfc) {
- //This method shows an AlertDialog
- AlertDialog.Builder alert = new AlertDialog.Builder(this);
- alert.setTitle("Information").setMessage(message).setPositiveButton("Ok",
- new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialogInterface, int i) {
- }
- }
- );
- if (nfc) {
-
- alert.setNeutralButton(R.string.nfc_settings,
- new DialogInterface.OnClickListener() {
- public void onClick(DialogInterface dialogInterface, int i) {
- startActivity(new Intent(Settings.ACTION_NFC_SETTINGS));
- }
- }
- );
- }
- alert.show();
- }
-
- @Override
- public void onPause() {
- //pause this app and free nfc intent
- super.onPause();
- if (adapter != null) {
- WriteModeOff();
- }
- }
-
- @Override
- public void onResume() {
- //resume this app and get nfc intent
- super.onResume();
- if (adapter != null) {
- WriteModeOn();
- }
- }
-
- private void WriteModeOn() {
- //enable nfc for this view
- writeMode = true;
- adapter.enableForegroundDispatch(this, pendingIntent, writeTagFilters, null);
- }
-
- private void WriteModeOff() {
- //disable nfc for this view
- writeMode = false;
- adapter.disableForegroundDispatch(this);
- }
-
- public static class SelectMethods extends Fragment {
-// private OnFragmentInteractionListener mListener;
-
- /**
- * Use this factory method to create a new instance of
- * this fragment using the provided parameters.
- */
- public SelectMethods() {
-
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- }
-
- @Override
- public void onResume() {
- super.onResume();
- if (getActivity().getActionBar() != null) {
- getActivity().getActionBar().setTitle(R.string.unlock_method);
- }
- }
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
- // Inflate the layout for this fragment
- return inflater.inflate(R.layout.passphrase_wizard_fragment_select_methods, container, false);
- }
-
-// @Override
-// public void onAttach(Activity activity) {
-// super.onAttach(activity);
-// try {
-// mListener = (OnFragmentInteractionListener) activity;
-// } catch (ClassCastException e) {
-// throw new ClassCastException(activity.toString()
-// + " must implement OnFragmentInteractionListener");
-// }
-// }
-//
-// @Override
-// public void onDetach() {
-// super.onDetach();
-// mListener = null;
-// }
-
- /**
- * This interface must be implemented by activities that contain this
- * fragment to allow an interaction in this fragment to be communicated
- * to the activity and potentially other fragments contained in that
- * activity.
- * <p/>
- * See the Android Training lesson <a href=
- * "http://developer.android.com/training/basics/fragments/communicating.html"
- * >Communicating with Other Fragments</a> for more information.
- */
-// public static interface OnFragmentInteractionListener {
-// public void onFragmentInteraction(Uri uri);
-// }
-
- }
-
-
- // /**
-// * A simple {@link android.support.v4.app.Fragment} subclass.
-// * Activities that contain this fragment must implement the
-// * {@link com.haibison.android.lockpattern.Passphrase.OnFragmentInteractionListener} interface
-// * to handle interaction events.
-// */
- public static class Passphrase extends Fragment {
-
-// private OnFragmentInteractionListener mListener;
-
- public Passphrase() {
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- }
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
- // Inflate the layout for this fragment
- View view = inflater.inflate(R.layout.passphrase_wizard_fragment_passphrase, container, false);
- EditText passphraseAgain = (EditText) view.findViewById(R.id.passphraseAgain);
- TextView passphraseText = (TextView) view.findViewById(R.id.passphraseText);
- TextView passphraseTextAgain = (TextView) view.findViewById(R.id.passphraseTextAgain);
- String selectedAction = getActivity().getIntent().getAction();
- if (selectedAction.equals(AUTHENTICATION)) {
- passphraseAgain.setVisibility(View.GONE);
- passphraseTextAgain.setVisibility(View.GONE);
- passphraseText.setText(R.string.enter_passphrase);
-// getActivity().getActionBar().setTitle(R.string.enter_passphrase);
- } else if (selectedAction.equals(CREATE_METHOD)) {
- passphraseAgain.setVisibility(View.VISIBLE);
- passphraseTextAgain.setVisibility(View.VISIBLE);
- passphraseText.setText(R.string.passphrase);
-// getActivity().getActionBar().setTitle(R.string.set_passphrase);
- }
- return view;
- }
-
-// @Override
-// public void onAttach(Activity activity) {
-// super.onAttach(activity);
-// try {
-// mListener = (OnFragmentInteractionListener) activity;
-// } catch (ClassCastException e) {
-// throw new ClassCastException(activity.toString()
-// + " must implement OnFragmentInteractionListener");
-// }
-// }
-//
-// @Override
-// public void onDetach() {
-// super.onDetach();
-// mListener = null;
-// }
-
-// /**
-// * This interface must be implemented by activities that contain this
-// * fragment to allow an interaction in this fragment to be communicated
-// * to the activity and potentially other fragments contained in that
-// * activity.
-// * <p/>
-// * See the Android Training lesson <a href=
-// * "http://developer.android.com/training/basics/fragments/communicating.html"
-// * >Communicating with Other Fragments</a> for more information.
-// */
-// public interface OnFragmentInteractionListener {
-// public void onFragmentInteraction(Uri uri);
-// }
- }
-
-
- /**
- * A simple {@link android.support.v4.app.Fragment} subclass.
- * Activities that contain this fragment must implement the
- * interface
- * to handle interaction events.
- * Use the method to
- * create an instance of this fragment.
- */
- public static class NFCFragment extends Fragment {
- // TODO: Rename parameter arguments, choose names that match
- // the fragment initialization parameters, e.g. ARG_ITEM_NUMBER
- private static final String ARG_PARAM1 = "param1";
- private static final String ARG_PARAM2 = "param2";
-
- // TODO: Rename and change types of parameters
- private String mParam1;
- private String mParam2;
-
-// private OnFragmentInteractionListener mListener;
-
- /**
- * Use this factory method to create a new instance of
- * this fragment using the provided parameters.
- *
- * @param param1 Parameter 1.
- * @param param2 Parameter 2.
- * @return A new instance of fragment SelectMethods.
- */
- // TODO: Rename and change types and number of parameters
- public static NFCFragment newInstance(String param1, String param2) {
- NFCFragment fragment = new NFCFragment();
- Bundle args = new Bundle();
- args.putString(ARG_PARAM1, param1);
- args.putString(ARG_PARAM2, param2);
- fragment.setArguments(args);
- return fragment;
- }
-
- public NFCFragment() {
- // Required empty public constructor
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- if (getArguments() != null) {
- mParam1 = getArguments().getString(ARG_PARAM1);
- mParam2 = getArguments().getString(ARG_PARAM2);
- }
- }
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
- // Inflate the layout for this fragment
- return inflater.inflate(R.layout.passphrase_wizard_fragment_nfc, container, false);
- }
-
-// // TODO: Rename method, update argument and hook method into UI event
-// public void onButtonPressed(Uri uri) {
-// if (mListener != null) {
-// mListener.onFragmentInteraction(uri);
-// }
-// }
-
-// @Override
-// public void onAttach(Activity activity) {
-// super.onAttach(activity);
-// try {
-// mListener = (OnFragmentInteractionListener) activity;
-// } catch (ClassCastException e) {
-// throw new ClassCastException(activity.toString()
-// + " must implement OnFragmentInteractionListener");
-// }
-// }
-
-
-// @Override
-// public void onDetach() {
-// super.onDetach();
-// mListener = null;
-// }
- }
-
-}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsActivity.java
index c18156428..eb9ee05af 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsActivity.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsActivity.java
@@ -18,11 +18,12 @@
package org.sufficientlysecure.keychain.ui;
-import android.annotation.TargetApi;
+import android.accounts.Account;
+import android.accounts.AccountManager;
import android.app.Activity;
+import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
-import android.os.Build;
import android.os.Bundle;
import android.preference.CheckBoxPreference;
import android.preference.EditTextPreference;
@@ -31,6 +32,8 @@ import android.preference.Preference;
import android.preference.PreferenceActivity;
import android.preference.PreferenceFragment;
import android.preference.PreferenceScreen;
+import android.preference.SwitchPreference;
+import android.provider.ContactsContract;
import android.support.v7.widget.Toolbar;
import android.text.TextUtils;
import android.view.View;
@@ -74,7 +77,7 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
String action = getIntent().getAction();
- if (action != null && action.equals(ACTION_PREFS_CLOUD)) {
+ if (ACTION_PREFS_CLOUD.equals(action)) {
addPreferencesFromResource(R.xml.cloud_search_prefs);
mKeyServerPreference = (PreferenceScreen) findPreference(Constants.Pref.KEY_SERVERS);
@@ -91,14 +94,14 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
}
});
initializeSearchKeyserver(
- (CheckBoxPreference) findPreference(Constants.Pref.SEARCH_KEYSERVER)
+ (SwitchPreference) findPreference(Constants.Pref.SEARCH_KEYSERVER)
);
initializeSearchKeybase(
- (CheckBoxPreference) findPreference(Constants.Pref.SEARCH_KEYBASE)
+ (SwitchPreference) findPreference(Constants.Pref.SEARCH_KEYBASE)
);
- } else if (action != null && action.equals(ACTION_PREFS_ADV)) {
- addPreferencesFromResource(R.xml.adv_preferences);
+ } else if (ACTION_PREFS_ADV.equals(action)) {
+ addPreferencesFromResource(R.xml.passphrase_preferences);
initializePassphraseCacheSubs(
(CheckBoxPreference) findPreference(Constants.Pref.PASSPHRASE_CACHE_SUBS));
@@ -112,7 +115,7 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
initializeUseNumKeypadForYubiKeyPin(
(CheckBoxPreference) findPreference(Constants.Pref.USE_NUMKEYPAD_FOR_YUBIKEY_PIN));
- } else if (action != null && action.equals(ACTION_PREFS_GUI)) {
+ } else if (ACTION_PREFS_GUI.equals(action)) {
addPreferencesFromResource(R.xml.gui_preferences);
initializeTheme((ListPreference) findPreference(Constants.Pref.THEME));
@@ -192,10 +195,10 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
}
});
initializeSearchKeyserver(
- (CheckBoxPreference) findPreference(Constants.Pref.SEARCH_KEYSERVER)
+ (SwitchPreference) findPreference(Constants.Pref.SEARCH_KEYSERVER)
);
initializeSearchKeybase(
- (CheckBoxPreference) findPreference(Constants.Pref.SEARCH_KEYBASE)
+ (SwitchPreference) findPreference(Constants.Pref.SEARCH_KEYBASE)
);
}
@@ -219,14 +222,14 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
/**
* This fragment shows the PIN/password preferences
*/
- public static class AdvancedPrefsFragment extends PreferenceFragment {
+ public static class PassphrasePrefsFragment extends PreferenceFragment {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Load the preferences from an XML resource
- addPreferencesFromResource(R.xml.adv_preferences);
+ addPreferencesFromResource(R.xml.passphrase_preferences);
initializePassphraseCacheSubs(
(CheckBoxPreference) findPreference(Constants.Pref.PASSPHRASE_CACHE_SUBS));
@@ -253,8 +256,8 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
}
public static class Initializer {
- private CheckBoxPreference mUseTor;
- private CheckBoxPreference mUseNormalProxy;
+ private SwitchPreference mUseTor;
+ private SwitchPreference mUseNormalProxy;
private EditTextPreference mProxyHost;
private EditTextPreference mProxyPort;
private ListPreference mProxyType;
@@ -290,8 +293,8 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
mActivity.addPreferencesFromResource(R.xml.proxy_prefs);
}
- mUseTor = (CheckBoxPreference) automaticallyFindPreference(Constants.Pref.USE_TOR_PROXY);
- mUseNormalProxy = (CheckBoxPreference) automaticallyFindPreference(Constants.Pref.USE_NORMAL_PROXY);
+ mUseTor = (SwitchPreference) automaticallyFindPreference(Constants.Pref.USE_TOR_PROXY);
+ mUseNormalProxy = (SwitchPreference) automaticallyFindPreference(Constants.Pref.USE_NORMAL_PROXY);
mProxyHost = (EditTextPreference) automaticallyFindPreference(Constants.Pref.PROXY_HOST);
mProxyPort = (EditTextPreference) automaticallyFindPreference(Constants.Pref.PROXY_PORT);
mProxyType = (ListPreference) automaticallyFindPreference(Constants.Pref.PROXY_TYPE);
@@ -467,11 +470,121 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
}
}
+ /**
+ * This fragment shows the keyserver/contacts sync preferences
+ */
+ public static class SyncPrefsFragment extends PreferenceFragment {
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // Load the preferences from an XML resource
+ addPreferencesFromResource(R.xml.sync_preferences);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ // this needs to be done in onResume since the user can change sync values from Android
+ // settings and we need to reflect that change when the user navigates back
+ AccountManager manager = AccountManager.get(getActivity());
+ final Account account = manager.getAccountsByType(Constants.ACCOUNT_TYPE)[0];
+ // for keyserver sync
+ initializeSyncCheckBox(
+ (SwitchPreference) findPreference(Constants.Pref.SYNC_KEYSERVER),
+ account,
+ Constants.PROVIDER_AUTHORITY
+ );
+ // for contacts sync
+ initializeSyncCheckBox(
+ (SwitchPreference) findPreference(Constants.Pref.SYNC_CONTACTS),
+ account,
+ ContactsContract.AUTHORITY
+ );
+ }
+
+ private void initializeSyncCheckBox(final SwitchPreference syncCheckBox,
+ final Account account,
+ final String authority) {
+ boolean syncEnabled = ContentResolver.getSyncAutomatically(account, authority);
+ syncCheckBox.setChecked(syncEnabled);
+ setSummary(syncCheckBox, authority, syncEnabled);
+
+ syncCheckBox.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ boolean syncEnabled = (Boolean) newValue;
+ if (syncEnabled) {
+ ContentResolver.setSyncAutomatically(account, authority, true);
+ } else {
+ // disable syncs
+ ContentResolver.setSyncAutomatically(account, authority, false);
+ // cancel any ongoing/pending syncs
+ ContentResolver.cancelSync(account, authority);
+ }
+ setSummary(syncCheckBox, authority, syncEnabled);
+ return true;
+ }
+ });
+ }
+
+ private void setSummary(SwitchPreference syncCheckBox, String authority,
+ boolean checked) {
+ switch (authority) {
+ case Constants.PROVIDER_AUTHORITY: {
+ if (checked) {
+ syncCheckBox.setSummary(R.string.label_sync_settings_keyserver_summary_on);
+ } else {
+ syncCheckBox.setSummary(R.string.label_sync_settings_keyserver_summary_off);
+ }
+ break;
+ }
+ case ContactsContract.AUTHORITY: {
+ if (checked) {
+ syncCheckBox.setSummary(R.string.label_sync_settings_contacts_summary_on);
+ } else {
+ syncCheckBox.setSummary(R.string.label_sync_settings_contacts_summary_off);
+ }
+ break;
+ }
+ }
+ }
+ }
+
+ /**
+ * This fragment shows experimental features
+ */
+ public static class ExperimentalPrefsFragment extends PreferenceFragment {
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // Load the preferences from an XML resource
+ addPreferencesFromResource(R.xml.experimental_preferences);
+
+ initializeExperimentalEnableWordConfirm(
+ (SwitchPreference) findPreference(Constants.Pref.EXPERIMENTAL_ENABLE_WORD_CONFIRM));
+
+ initializeExperimentalEnableLinkedIdentities(
+ (SwitchPreference) findPreference(Constants.Pref.EXPERIMENTAL_ENABLE_LINKED_IDENTITIES));
+
+ initializeExperimentalEnableKeybase(
+ (SwitchPreference) findPreference(Constants.Pref.EXPERIMENTAL_ENABLE_KEYBASE));
+
+ initializeTheme((ListPreference) findPreference(Constants.Pref.THEME));
+
+ }
+ }
+
protected boolean isValidFragment(String fragmentName) {
- return AdvancedPrefsFragment.class.getName().equals(fragmentName)
+ return PassphrasePrefsFragment.class.getName().equals(fragmentName)
|| CloudSearchPrefsFragment.class.getName().equals(fragmentName)
|| ProxyPrefsFragment.class.getName().equals(fragmentName)
|| GuiPrefsFragment.class.getName().equals(fragmentName)
+ || SyncPrefsFragment.class.getName().equals(fragmentName)
+ || ExperimentalPrefsFragment.class.getName().equals(fragmentName)
|| super.isValidFragment(fragmentName);
}
@@ -502,11 +615,13 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
private static void initializeTheme(final ListPreference mTheme) {
mTheme.setValue(sPreferences.getTheme());
- mTheme.setSummary(mTheme.getEntry());
+ mTheme.setSummary(mTheme.getEntry() + "\n"
+ + mTheme.getContext().getString(R.string.label_experimental_settings_theme_summary));
mTheme.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
public boolean onPreferenceChange(Preference preference, Object newValue) {
mTheme.setValue((String) newValue);
- mTheme.setSummary(mTheme.getEntry());
+ mTheme.setSummary(mTheme.getEntry() + "\n"
+ + mTheme.getContext().getString(R.string.label_experimental_settings_theme_summary));
sPreferences.setTheme((String) newValue);
((SettingsActivity) mTheme.getContext()).recreate();
@@ -516,7 +631,7 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
});
}
- private static void initializeSearchKeyserver(final CheckBoxPreference mSearchKeyserver) {
+ private static void initializeSearchKeyserver(final SwitchPreference mSearchKeyserver) {
Preferences.CloudSearchPrefs prefs = sPreferences.getCloudSearchPrefs();
mSearchKeyserver.setChecked(prefs.searchKeyserver);
mSearchKeyserver.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@@ -529,7 +644,7 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
});
}
- private static void initializeSearchKeybase(final CheckBoxPreference mSearchKeybase) {
+ private static void initializeSearchKeybase(final SwitchPreference mSearchKeybase) {
Preferences.CloudSearchPrefs prefs = sPreferences.getCloudSearchPrefs();
mSearchKeybase.setChecked(prefs.searchKeybase);
mSearchKeybase.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@@ -571,4 +686,37 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
}
});
}
+
+ private static void initializeExperimentalEnableWordConfirm(final SwitchPreference mExperimentalEnableWordConfirm) {
+ mExperimentalEnableWordConfirm.setChecked(sPreferences.getExperimentalEnableWordConfirm());
+ mExperimentalEnableWordConfirm.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ mExperimentalEnableWordConfirm.setChecked((Boolean) newValue);
+ sPreferences.setExperimentalEnableWordConfirm((Boolean) newValue);
+ return false;
+ }
+ });
+ }
+
+ private static void initializeExperimentalEnableLinkedIdentities(final SwitchPreference mExperimentalEnableLinkedIdentities) {
+ mExperimentalEnableLinkedIdentities.setChecked(sPreferences.getExperimentalEnableLinkedIdentities());
+ mExperimentalEnableLinkedIdentities.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ mExperimentalEnableLinkedIdentities.setChecked((Boolean) newValue);
+ sPreferences.setExperimentalEnableLinkedIdentities((Boolean) newValue);
+ return false;
+ }
+ });
+ }
+
+ private static void initializeExperimentalEnableKeybase(final SwitchPreference mExperimentalKeybase) {
+ mExperimentalKeybase.setChecked(sPreferences.getExperimentalEnableKeybase());
+ mExperimentalKeybase.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ mExperimentalKeybase.setChecked((Boolean) newValue);
+ sPreferences.setExperimentalEnableKeybase((Boolean) newValue);
+ return false;
+ }
+ });
+ }
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsKeyServerActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsKeyServerActivity.java
index f61ada84f..7dd92c45f 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsKeyServerActivity.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsKeyServerActivity.java
@@ -19,6 +19,7 @@ package org.sufficientlysecure.keychain.ui;
import android.content.Intent;
import android.os.Bundle;
+import android.view.MenuItem;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.ui.base.BaseActivity;
@@ -34,6 +35,19 @@ public class SettingsKeyServerActivity extends BaseActivity {
Intent intent = getIntent();
String servers[] = intent.getStringArrayExtra(EXTRA_KEY_SERVERS);
loadFragment(savedInstanceState, servers);
+
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ finish();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
}
@Override
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java
index 1d0e085da..930c1fc26 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java
@@ -18,6 +18,11 @@
package org.sufficientlysecure.keychain.ui;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+
import android.animation.ArgbEvaluator;
import android.animation.ObjectAnimator;
import android.annotation.SuppressLint;
@@ -32,6 +37,9 @@ import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.provider.ContactsContract;
+import android.support.design.widget.AppBarLayout;
+import android.support.design.widget.CollapsingToolbarLayout;
+import android.support.design.widget.FloatingActionButton;
import android.support.v4.app.ActivityCompat;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.LoaderManager;
@@ -45,14 +53,13 @@ import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.view.animation.Animation.AnimationListener;
import android.view.animation.AnimationUtils;
+import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;
-import com.getbase.floatingactionbutton.FloatingActionButton;
-
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing;
@@ -65,8 +72,10 @@ import org.sufficientlysecure.keychain.provider.KeychainContract;
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
import org.sufficientlysecure.keychain.provider.ProviderHelper;
import org.sufficientlysecure.keychain.service.ImportKeyringParcel;
+import org.sufficientlysecure.keychain.ui.ViewKeyFragment.PostponeType;
import org.sufficientlysecure.keychain.ui.base.BaseNfcActivity;
import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper;
+import org.sufficientlysecure.keychain.ui.linked.LinkedIdWizard;
import org.sufficientlysecure.keychain.ui.util.FormattingUtils;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils.State;
@@ -80,8 +89,6 @@ import org.sufficientlysecure.keychain.util.Log;
import org.sufficientlysecure.keychain.util.NfcHelper;
import org.sufficientlysecure.keychain.util.Preferences;
-import java.io.IOException;
-import java.util.ArrayList;
public class ViewKeyActivity extends BaseNfcActivity implements
LoaderManager.LoaderCallbacks<Cursor>,
@@ -97,6 +104,7 @@ public class ViewKeyActivity extends BaseNfcActivity implements
static final int REQUEST_DELETE = 4;
public static final String EXTRA_DISPLAY_RESULT = "display_result";
+ public static final String EXTRA_LINKED_TRANSITION = "linked_transition";
ProviderHelper mProviderHelper;
@@ -107,16 +115,17 @@ public class ViewKeyActivity extends BaseNfcActivity implements
private ArrayList<ParcelableKeyRing> mKeyList;
private CryptoOperationHelper<ImportKeyringParcel, ImportKeyResult> mOperationHelper;
- private TextView mName;
private TextView mStatusText;
private ImageView mStatusImage;
- private RelativeLayout mBigToolbar;
+ private AppBarLayout mAppBarLayout;
+ private CollapsingToolbarLayout mCollapsingToolbarLayout;
private ImageButton mActionEncryptFile;
private ImageButton mActionEncryptText;
private ImageButton mActionNfc;
private FloatingActionButton mFab;
private ImageView mPhoto;
+ private FrameLayout mPhotoLayout;
private ImageView mQrCode;
private CardView mQrCodeLayout;
@@ -156,16 +165,17 @@ public class ViewKeyActivity extends BaseNfcActivity implements
setTitle(null);
- mName = (TextView) findViewById(R.id.view_key_name);
mStatusText = (TextView) findViewById(R.id.view_key_status);
mStatusImage = (ImageView) findViewById(R.id.view_key_status_image);
- mBigToolbar = (RelativeLayout) findViewById(R.id.toolbar_big);
+ mAppBarLayout = (AppBarLayout) findViewById(R.id.app_bar_layout);
+ mCollapsingToolbarLayout = (CollapsingToolbarLayout) findViewById(R.id.collapsing_toolbar);
mActionEncryptFile = (ImageButton) findViewById(R.id.view_key_action_encrypt_files);
mActionEncryptText = (ImageButton) findViewById(R.id.view_key_action_encrypt_text);
mActionNfc = (ImageButton) findViewById(R.id.view_key_action_nfc);
mFab = (FloatingActionButton) findViewById(R.id.fab);
mPhoto = (ImageView) findViewById(R.id.view_key_photo);
+ mPhotoLayout = (FrameLayout) findViewById(R.id.view_key_photo_layout);
mQrCode = (ImageView) findViewById(R.id.view_key_qr_code);
mQrCodeLayout = (CardView) findViewById(R.id.view_key_qr_code_layout);
@@ -287,13 +297,26 @@ public class ViewKeyActivity extends BaseNfcActivity implements
return;
}
+ boolean linkedTransition = getIntent().getBooleanExtra(EXTRA_LINKED_TRANSITION, false);
+ if (linkedTransition && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ postponeEnterTransition();
+ }
+
FragmentManager manager = getSupportFragmentManager();
// Create an instance of the fragment
- final ViewKeyFragment frag = ViewKeyFragment.newInstance(mDataUri);
+ final ViewKeyFragment frag = ViewKeyFragment.newInstance(mDataUri,
+ linkedTransition ? PostponeType.LINKED : PostponeType.NONE);
manager.beginTransaction()
.replace(R.id.view_key_fragment, frag)
.commit();
+ if (Preferences.getPreferences(this).getExperimentalEnableKeybase()) {
+ final ViewKeyKeybaseFragment keybaseFrag = ViewKeyKeybaseFragment.newInstance(mDataUri);
+ manager.beginTransaction()
+ .replace(R.id.view_key_keybase_fragment, keybaseFrag)
+ .commit();
+ }
+
// need to postpone loading of the yubikey fragment until after mMasterKeyId
// is available, but we mark here that this should be done
mShowYubikeyAfterCreation = true;
@@ -344,12 +367,23 @@ public class ViewKeyActivity extends BaseNfcActivity implements
}
return true;
}
+ case R.id.menu_key_view_add_linked_identity: {
+ Intent intent = new Intent(this, LinkedIdWizard.class);
+ intent.setData(mDataUri);
+ startActivity(intent);
+ finish();
+ return true;
+ }
case R.id.menu_key_view_edit: {
editKey(mDataUri);
return true;
}
case R.id.menu_key_view_certify_fingerprint: {
- certifyFingeprint(mDataUri);
+ certifyFingeprint(mDataUri, false);
+ return true;
+ }
+ case R.id.menu_key_view_certify_fingerprint_word: {
+ certifyFingeprint(mDataUri, true);
return true;
}
}
@@ -360,10 +394,19 @@ public class ViewKeyActivity extends BaseNfcActivity implements
public boolean onPrepareOptionsMenu(Menu menu) {
MenuItem editKey = menu.findItem(R.id.menu_key_view_edit);
editKey.setVisible(mIsSecret);
+
MenuItem exportKey = menu.findItem(R.id.menu_key_view_export_file);
exportKey.setVisible(mIsSecret);
+
+ MenuItem addLinked = menu.findItem(R.id.menu_key_view_add_linked_identity);
+ addLinked.setVisible(mIsSecret
+ && Preferences.getPreferences(this).getExperimentalEnableLinkedIdentities());
+
MenuItem certifyFingerprint = menu.findItem(R.id.menu_key_view_certify_fingerprint);
certifyFingerprint.setVisible(!mIsSecret && !mIsVerified && !mIsExpired && !mIsRevoked);
+ MenuItem certifyFingerprintWord = menu.findItem(R.id.menu_key_view_certify_fingerprint_word);
+ certifyFingerprintWord.setVisible(!mIsSecret && !mIsVerified && !mIsExpired && !mIsRevoked
+ && Preferences.getPreferences(this).getExperimentalEnableWordConfirm());
return true;
}
@@ -375,16 +418,17 @@ public class ViewKeyActivity extends BaseNfcActivity implements
startActivityForResult(scanQrCode, REQUEST_QR_FINGERPRINT);
}
- private void certifyFingeprint(Uri dataUri) {
+ private void certifyFingeprint(Uri dataUri, boolean enableWordConfirm) {
Intent intent = new Intent(this, CertifyFingerprintActivity.class);
intent.setData(dataUri);
+ intent.putExtra(CertifyFingerprintActivity.EXTRA_ENABLE_WORD_CONFIRM, enableWordConfirm);
startActivityForResult(intent, REQUEST_CERTIFY);
}
private void certifyImmediate() {
Intent intent = new Intent(this, CertifyKeyActivity.class);
- intent.putExtra(CertifyKeyActivity.EXTRA_KEY_IDS, new long[]{mMasterKeyId});
+ intent.putExtra(CertifyKeyActivity.EXTRA_KEY_IDS, new long[] { mMasterKeyId });
startActivityForResult(intent, REQUEST_CERTIFY);
}
@@ -413,7 +457,8 @@ public class ViewKeyActivity extends BaseNfcActivity implements
private void backupToFile() {
new ExportHelper(this).showExportKeysDialog(
- mMasterKeyId, Constants.Path.APP_DIR_FILE, true);
+ mMasterKeyId, new File(Constants.Path.APP_DIR,
+ KeyFormattingUtils.convertKeyIdToHex(mMasterKeyId) + ".sec.asc"), true);
}
private void deleteKey() {
@@ -437,13 +482,13 @@ public class ViewKeyActivity extends BaseNfcActivity implements
return;
}
- if (resultCode != Activity.RESULT_OK) {
- return;
- }
-
switch (requestCode) {
case REQUEST_QR_FINGERPRINT: {
+ if (resultCode != Activity.RESULT_OK) {
+ return;
+ }
+
// If there is an EXTRA_RESULT, that's an error. Just show it.
if (data.hasExtra(OperationResult.EXTRA_RESULT)) {
OperationResult result = data.getParcelableExtra(OperationResult.EXTRA_RESULT);
@@ -465,11 +510,19 @@ public class ViewKeyActivity extends BaseNfcActivity implements
}
case REQUEST_BACKUP: {
+ if (resultCode != Activity.RESULT_OK) {
+ return;
+ }
+
backupToFile();
return;
}
case REQUEST_CERTIFY: {
+ if (resultCode != Activity.RESULT_OK) {
+ return;
+ }
+
if (data.hasExtra(OperationResult.EXTRA_RESULT)) {
OperationResult result = data.getParcelableExtra(OperationResult.EXTRA_RESULT);
result.createNotify(this).show();
@@ -478,6 +531,10 @@ public class ViewKeyActivity extends BaseNfcActivity implements
}
case REQUEST_DELETE: {
+ if (resultCode != Activity.RESULT_OK) {
+ return;
+ }
+
setResult(RESULT_OK, data);
finish();
return;
@@ -530,7 +587,6 @@ public class ViewKeyActivity extends BaseNfcActivity implements
finish();
}
}, R.string.snack_yubikey_view).show();
-
// and if it's not found, offer import
} catch (PgpKeyNotFoundException e) {
Notify.create(this, R.string.snack_yubi_other, Notify.LENGTH_LONG,
@@ -728,9 +784,9 @@ public class ViewKeyActivity extends BaseNfcActivity implements
// get name, email, and comment from USER_ID
KeyRing.UserId mainUserId = KeyRing.splitUserId(data.getString(INDEX_USER_ID));
if (mainUserId.name != null) {
- mName.setText(mainUserId.name);
+ mCollapsingToolbarLayout.setTitle(mainUserId.name);
} else {
- mName.setText(R.string.user_id_no_name);
+ mCollapsingToolbarLayout.setTitle(getString(R.string.user_id_no_name));
}
mMasterKeyId = data.getLong(INDEX_MASTER_KEY_ID);
@@ -767,8 +823,12 @@ public class ViewKeyActivity extends BaseNfcActivity implements
}
protected void onPostExecute(Bitmap photo) {
+ if (photo == null) {
+ return;
+ }
+
mPhoto.setImageBitmap(photo);
- mPhoto.setVisibility(View.VISIBLE);
+ mPhotoLayout.setVisibility(View.VISIBLE);
}
};
@@ -781,9 +841,9 @@ public class ViewKeyActivity extends BaseNfcActivity implements
State.REVOKED, R.color.icons, true);
color = getResources().getColor(R.color.key_flag_red);
- mActionEncryptFile.setVisibility(View.GONE);
- mActionEncryptText.setVisibility(View.GONE);
- mActionNfc.setVisibility(View.GONE);
+ mActionEncryptFile.setVisibility(View.INVISIBLE);
+ mActionEncryptText.setVisibility(View.INVISIBLE);
+ mActionNfc.setVisibility(View.INVISIBLE);
mFab.setVisibility(View.GONE);
mQrCodeLayout.setVisibility(View.GONE);
} else if (mIsExpired) {
@@ -797,9 +857,9 @@ public class ViewKeyActivity extends BaseNfcActivity implements
State.EXPIRED, R.color.icons, true);
color = getResources().getColor(R.color.key_flag_red);
- mActionEncryptFile.setVisibility(View.GONE);
- mActionEncryptText.setVisibility(View.GONE);
- mActionNfc.setVisibility(View.GONE);
+ mActionEncryptFile.setVisibility(View.INVISIBLE);
+ mActionEncryptText.setVisibility(View.INVISIBLE);
+ mActionNfc.setVisibility(View.INVISIBLE);
mFab.setVisibility(View.GONE);
mQrCodeLayout.setVisibility(View.GONE);
} else if (mIsSecret) {
@@ -814,15 +874,15 @@ public class ViewKeyActivity extends BaseNfcActivity implements
mQrCodeLayout.setVisibility(View.VISIBLE);
// and place leftOf qr code
- RelativeLayout.LayoutParams nameParams = (RelativeLayout.LayoutParams)
- mName.getLayoutParams();
- // remove right margin
- nameParams.setMargins(FormattingUtils.dpToPx(this, 48), 0, 0, 0);
- if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
- nameParams.setMarginEnd(0);
- }
- nameParams.addRule(RelativeLayout.LEFT_OF, R.id.view_key_qr_code_layout);
- mName.setLayoutParams(nameParams);
+// RelativeLayout.LayoutParams nameParams = (RelativeLayout.LayoutParams)
+// mName.getLayoutParams();
+// // remove right margin
+// nameParams.setMargins(FormattingUtils.dpToPx(this, 48), 0, 0, 0);
+// if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
+// nameParams.setMarginEnd(0);
+// }
+// nameParams.addRule(RelativeLayout.LEFT_OF, R.id.view_key_qr_code_layout);
+// mName.setLayoutParams(nameParams);
RelativeLayout.LayoutParams statusParams = (RelativeLayout.LayoutParams)
mStatusText.getLayoutParams();
@@ -844,7 +904,7 @@ public class ViewKeyActivity extends BaseNfcActivity implements
}
mFab.setVisibility(View.VISIBLE);
// noinspection deprecation (no getDrawable with theme at current minApi level 15!)
- mFab.setIconDrawable(getResources().getDrawable(R.drawable.ic_repeat_white_24dp));
+ mFab.setImageDrawable(getResources().getDrawable(R.drawable.ic_repeat_white_24dp));
} else {
mActionEncryptFile.setVisibility(View.VISIBLE);
mActionEncryptText.setVisibility(View.VISIBLE);
@@ -872,22 +932,19 @@ public class ViewKeyActivity extends BaseNfcActivity implements
}
if (mPreviousColor == 0 || mPreviousColor == color) {
- mStatusBar.setBackgroundColor(getStatusBarBackgroundColor(color));
- mBigToolbar.setBackgroundColor(color);
+ mAppBarLayout.setBackgroundColor(color);
+ mCollapsingToolbarLayout.setContentScrimColor(color);
+ mCollapsingToolbarLayout.setStatusBarScrimColor(getStatusBarBackgroundColor(color));
mPreviousColor = color;
} else {
- ObjectAnimator colorFade1 =
- ObjectAnimator.ofObject(mStatusBar, "backgroundColor",
- new ArgbEvaluator(), mPreviousColor,
- getStatusBarBackgroundColor(color));
- ObjectAnimator colorFade2 =
- ObjectAnimator.ofObject(mBigToolbar, "backgroundColor",
+ ObjectAnimator colorFade =
+ ObjectAnimator.ofObject(mAppBarLayout, "backgroundColor",
new ArgbEvaluator(), mPreviousColor, color);
+ mCollapsingToolbarLayout.setContentScrimColor(color);
+ mCollapsingToolbarLayout.setStatusBarScrimColor(getStatusBarBackgroundColor(color));
- colorFade1.setDuration(1200);
- colorFade2.setDuration(1200);
- colorFade1.start();
- colorFade2.start();
+ colorFade.setDuration(1200);
+ colorFade.start();
mPreviousColor = color;
}
@@ -963,4 +1020,6 @@ public class ViewKeyActivity extends BaseNfcActivity implements
public boolean onCryptoSetProgress(String msg, int progress, int max) {
return true;
}
+
}
+
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvActivity.java
index 673092e61..edd9feec9 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvActivity.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvActivity.java
@@ -57,7 +57,6 @@ public class ViewKeyAdvActivity extends BaseActivity implements
public static final int TAB_IDENTITIES = 1;
public static final int TAB_SUBKEYS = 2;
public static final int TAB_CERTS = 3;
- public static final int TAB_KEYBASE = 4;
// view
private ViewPager mViewPager;
@@ -140,11 +139,6 @@ public class ViewKeyAdvActivity extends BaseActivity implements
adapter.addTab(ViewKeyAdvCertsFragment.class,
certsBundle, getString(R.string.key_view_tab_certs));
- Bundle trustBundle = new Bundle();
- trustBundle.putParcelable(ViewKeyTrustFragment.ARG_DATA_URI, dataUri);
- adapter.addTab(ViewKeyTrustFragment.class,
- trustBundle, getString(R.string.key_view_tab_keybase));
-
// update layout after operations
mSlidingTabLayout.setViewPager(mViewPager);
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvUserIdsFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvUserIdsFragment.java
index 7bfebaf62..ad437f924 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvUserIdsFragment.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvUserIdsFragment.java
@@ -133,7 +133,7 @@ public class ViewKeyAdvUserIdsFragment extends LoaderFragment implements
case LOADER_ID_USER_IDS: {
Uri baseUri = UserPackets.buildUserIdsUri(mDataUri);
return new CursorLoader(getActivity(), baseUri,
- UserIdsAdapter.USER_IDS_PROJECTION, null, null, null);
+ UserIdsAdapter.USER_PACKETS_PROJECTION, null, null, null);
}
default:
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyFragment.java
index a929d52f0..7be695de0 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyFragment.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyFragment.java
@@ -18,52 +18,73 @@
package org.sufficientlysecure.keychain.ui;
+
+import java.io.IOException;
+import java.util.List;
+
+import android.annotation.TargetApi;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.net.Uri;
+import android.os.Build;
+import android.os.Build.VERSION_CODES;
import android.os.Bundle;
+import android.os.Handler;
import android.provider.ContactsContract;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.support.v7.widget.CardView;
+import android.transition.Fade;
+import android.transition.Transition;
+import android.transition.TransitionInflater;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
-import android.widget.*;
+import android.view.ViewTreeObserver;
+import android.view.ViewTreeObserver.OnPreDrawListener;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.ListView;
+import android.widget.TextView;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.compatibility.DialogFragmentWorkaround;
import org.sufficientlysecure.keychain.provider.KeychainContract;
+import org.sufficientlysecure.keychain.ui.adapter.LinkedIdsAdapter;
import org.sufficientlysecure.keychain.ui.adapter.UserIdsAdapter;
import org.sufficientlysecure.keychain.ui.dialog.UserIdInfoDialogFragment;
+import org.sufficientlysecure.keychain.ui.linked.LinkedIdViewFragment;
+import org.sufficientlysecure.keychain.ui.linked.LinkedIdViewFragment.OnIdentityLoadedListener;
import org.sufficientlysecure.keychain.util.ContactHelper;
import org.sufficientlysecure.keychain.util.Log;
-
-import java.util.List;
+import org.sufficientlysecure.keychain.util.Preferences;
public class ViewKeyFragment extends LoaderFragment implements
LoaderManager.LoaderCallbacks<Cursor> {
public static final String ARG_DATA_URI = "uri";
+ public static final String ARG_POSTPONE_TYPE = "postpone_type";
private ListView mUserIds;
//private ListView mLinkedSystemContact;
- boolean mIsSecret = false;
+ enum PostponeType {
+ NONE, LINKED;
+ }
- CardView mSystemContactCard;
- LinearLayout mSystemContactLayout;
- ImageView mSystemContactPicture;
- TextView mSystemContactName;
+ boolean mIsSecret = false;
private static final int LOADER_ID_UNIFIED = 0;
private static final int LOADER_ID_USER_IDS = 1;
private static final int LOADER_ID_LINKED_CONTACT = 2;
+ private static final int LOADER_ID_LINKED_IDS = 3;
private static final String LOADER_EXTRA_LINKED_CONTACT_MASTER_KEY_ID
= "loader_linked_contact_master_key_id";
@@ -71,16 +92,29 @@ public class ViewKeyFragment extends LoaderFragment implements
= "loader_linked_contact_is_secret";
private UserIdsAdapter mUserIdsAdapter;
+ private LinkedIdsAdapter mLinkedIdsAdapter;
private Uri mDataUri;
+ private PostponeType mPostponeType;
+
+ private CardView mSystemContactCard;
+ private LinearLayout mSystemContactLayout;
+ private ImageView mSystemContactPicture;
+ private TextView mSystemContactName;
+
+ private ListView mLinkedIds;
+ private CardView mLinkedIdsCard;
+ private byte[] mFingerprint;
+ private TextView mLinkedIdsExpander;
/**
* Creates new instance of this fragment
*/
- public static ViewKeyFragment newInstance(Uri dataUri) {
+ public static ViewKeyFragment newInstance(Uri dataUri, PostponeType postponeType) {
ViewKeyFragment frag = new ViewKeyFragment();
Bundle args = new Bundle();
args.putParcelable(ARG_DATA_URI, dataUri);
+ args.putInt(ARG_POSTPONE_TYPE, postponeType.ordinal());
frag.setArguments(args);
@@ -93,6 +127,11 @@ public class ViewKeyFragment extends LoaderFragment implements
View view = inflater.inflate(R.layout.view_key_fragment, getContainer());
mUserIds = (ListView) view.findViewById(R.id.view_key_user_ids);
+ mLinkedIdsCard = (CardView) view.findViewById(R.id.card_linked_ids);
+
+ mLinkedIds = (ListView) view.findViewById(R.id.view_key_linked_ids);
+
+ mLinkedIdsExpander = (TextView) view.findViewById(R.id.view_key_linked_ids_expander);
mUserIds.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
@@ -100,6 +139,12 @@ public class ViewKeyFragment extends LoaderFragment implements
showUserIdInfo(position);
}
});
+ mLinkedIds.setOnItemClickListener(new OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ showLinkedId(position);
+ }
+ });
mSystemContactCard = (CardView) view.findViewById(R.id.linked_system_contact_card);
mSystemContactLayout = (LinearLayout) view.findViewById(R.id.system_contact_layout);
@@ -109,6 +154,47 @@ public class ViewKeyFragment extends LoaderFragment implements
return root;
}
+ private void showLinkedId(final int position) {
+ final LinkedIdViewFragment frag;
+ try {
+ frag = mLinkedIdsAdapter.getLinkedIdFragment(mDataUri, position, mFingerprint);
+ } catch (IOException e) {
+ e.printStackTrace();
+ return;
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ Transition trans = TransitionInflater.from(getActivity())
+ .inflateTransition(R.transition.linked_id_card_trans);
+ // setSharedElementReturnTransition(trans);
+ setExitTransition(new Fade());
+ frag.setSharedElementEnterTransition(trans);
+ }
+
+ getFragmentManager().beginTransaction()
+ .add(R.id.view_key_fragment, frag)
+ .hide(frag)
+ .commit();
+
+ frag.setOnIdentityLoadedListener(new OnIdentityLoadedListener() {
+ @Override
+ public void onIdentityLoaded() {
+ new Handler().post(new Runnable() {
+ @Override
+ public void run() {
+ getFragmentManager().beginTransaction()
+ .show(frag)
+ .addSharedElement(mLinkedIdsCard, "card_linked_ids")
+ .remove(ViewKeyFragment.this)
+ .addToBackStack("linked_id")
+ .commit();
+ }
+ });
+ }
+ });
+
+ }
+
private void showUserIdInfo(final int position) {
if (!mIsSecret) {
final boolean isRevoked = mUserIdsAdapter.getIsRevoked(position);
@@ -129,8 +215,6 @@ public class ViewKeyFragment extends LoaderFragment implements
* Hides card if no linked system contact exists. Sets name, picture
* and onClickListener for the linked system contact's layout.
* In the case of a secret key, "me" (own profile) contact details are loaded.
- *
- * @param contactId
*/
private void loadLinkedSystemContact(final long contactId) {
// contact doesn't exist, stop
@@ -188,7 +272,6 @@ public class ViewKeyFragment extends LoaderFragment implements
* ContactsContract.Contact table)
*
* @param contactId _ID for row in ContactsContract.Contacts table
- * @param context
*/
private void launchContactActivity(final long contactId, Context context) {
Intent intent = new Intent(Intent.ACTION_VIEW);
@@ -202,6 +285,7 @@ public class ViewKeyFragment extends LoaderFragment implements
super.onActivityCreated(savedInstanceState);
Uri dataUri = getArguments().getParcelable(ARG_DATA_URI);
+ mPostponeType = PostponeType.values()[getArguments().getInt(ARG_POSTPONE_TYPE, 0)];
if (dataUri == null) {
Log.e(Constants.TAG, "Data missing. Should be Uri of key!");
getActivity().finish();
@@ -225,12 +309,17 @@ public class ViewKeyFragment extends LoaderFragment implements
};
static final int INDEX_MASTER_KEY_ID = 1;
+ @SuppressWarnings("unused")
static final int INDEX_USER_ID = 2;
+ @SuppressWarnings("unused")
static final int INDEX_IS_REVOKED = 3;
+ @SuppressWarnings("unused")
static final int INDEX_IS_EXPIRED = 4;
+ @SuppressWarnings("unused")
static final int INDEX_VERIFIED = 5;
static final int INDEX_HAS_ANY_SECRET = 6;
static final int INDEX_FINGERPRINT = 7;
+ @SuppressWarnings("unused")
static final int INDEX_HAS_ENCRYPT = 8;
private static final String[] RAWCONTACT_PROJECTION = {
@@ -246,21 +335,26 @@ public class ViewKeyFragment extends LoaderFragment implements
// Prepare the loaders. Either re-connect with an existing ones,
// or start new ones.
- // TODO Is this loader the same as the one in the activity?
getLoaderManager().initLoader(LOADER_ID_UNIFIED, null, this);
}
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
- setContentShown(false);
switch (id) {
case LOADER_ID_UNIFIED: {
+ setContentShown(false, false);
Uri baseUri = KeychainContract.KeyRings.buildUnifiedKeyRingUri(mDataUri);
return new CursorLoader(getActivity(), baseUri, UNIFIED_PROJECTION, null, null, null);
}
- case LOADER_ID_USER_IDS:
+
+ case LOADER_ID_USER_IDS: {
return UserIdsAdapter.createLoader(getActivity(), mDataUri);
+ }
+
+ case LOADER_ID_LINKED_IDS: {
+ return LinkedIdsAdapter.createLoader(getActivity(), mDataUri);
+ }
//we need a separate loader for linked contact to ensure refreshing on verification
case LOADER_ID_LINKED_CONTACT: {
@@ -310,19 +404,21 @@ public class ViewKeyFragment extends LoaderFragment implements
if (data.moveToFirst()) {
mIsSecret = data.getInt(INDEX_HAS_ANY_SECRET) != 0;
+ mFingerprint = data.getBlob(INDEX_FINGERPRINT);
+ long masterKeyId = data.getLong(INDEX_MASTER_KEY_ID);
// load user ids after we know if it's a secret key
mUserIdsAdapter = new UserIdsAdapter(getActivity(), null, 0, !mIsSecret, null);
mUserIds.setAdapter(mUserIdsAdapter);
getLoaderManager().initLoader(LOADER_ID_USER_IDS, null, this);
- long masterKeyId = data.getLong(INDEX_MASTER_KEY_ID);
- // we need to load linked contact here to prevent lag introduced by loader
- // for the linked contact
- long contactId = ContactHelper.findContactId(
- getActivity().getContentResolver(),
- masterKeyId);
- loadLinkedSystemContact(contactId);
+ if (Preferences.getPreferences(getActivity()).getExperimentalEnableLinkedIdentities()) {
+ mLinkedIdsAdapter =
+ new LinkedIdsAdapter(getActivity(), null, 0, mIsSecret, mLinkedIdsExpander);
+ mLinkedIds.setAdapter(mLinkedIdsAdapter);
+ getLoaderManager().initLoader(LOADER_ID_LINKED_IDS, null, this);
+ }
+
Bundle linkedContactData = new Bundle();
linkedContactData.putLong(LOADER_EXTRA_LINKED_CONTACT_MASTER_KEY_ID, masterKeyId);
@@ -336,10 +432,28 @@ public class ViewKeyFragment extends LoaderFragment implements
}
case LOADER_ID_USER_IDS: {
+ setContentShown(true, false);
mUserIdsAdapter.swapCursor(data);
break;
}
+ case LOADER_ID_LINKED_IDS: {
+ mLinkedIdsAdapter.swapCursor(data);
+ mLinkedIdsCard.setVisibility(mLinkedIdsAdapter.getCount() > 0 ? View.VISIBLE : View.GONE);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && mPostponeType == PostponeType.LINKED) {
+ mLinkedIdsCard.getViewTreeObserver().addOnPreDrawListener(new OnPreDrawListener() {
+ @TargetApi(VERSION_CODES.LOLLIPOP)
+ @Override
+ public boolean onPreDraw() {
+ mLinkedIdsCard.getViewTreeObserver().removeOnPreDrawListener(this);
+ getActivity().startPostponedEnterTransition();
+ return true;
+ }
+ });
+ }
+ break;
+ }
+
case LOADER_ID_LINKED_CONTACT: {
if (data.moveToFirst()) {// if we have a linked contact
long contactId = data.getLong(INDEX_CONTACT_ID);
@@ -349,7 +463,6 @@ public class ViewKeyFragment extends LoaderFragment implements
}
}
- setContentShown(true);
}
/**
@@ -363,6 +476,11 @@ public class ViewKeyFragment extends LoaderFragment implements
mUserIdsAdapter.swapCursor(null);
break;
}
+ case LOADER_ID_LINKED_IDS: {
+ mLinkedIdsCard.setVisibility(View.GONE);
+ mLinkedIdsAdapter.swapCursor(null);
+ break;
+ }
}
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyTrustFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyKeybaseFragment.java
index 150acdc90..266633061 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyTrustFragment.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyKeybaseFragment.java
@@ -59,14 +59,12 @@ import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
-public class ViewKeyTrustFragment extends LoaderFragment implements
+public class ViewKeyKeybaseFragment extends LoaderFragment implements
LoaderManager.LoaderCallbacks<Cursor>,
CryptoOperationHelper.Callback<KeybaseVerificationParcel, KeybaseVerificationResult> {
public static final String ARG_DATA_URI = "uri";
- private View mStartSearch;
- private TextView mTrustReadout;
private TextView mReportHeader;
private TableLayout mProofListing;
private LayoutInflater mInflater;
@@ -86,15 +84,25 @@ public class ViewKeyTrustFragment extends LoaderFragment implements
private CryptoOperationHelper<KeybaseVerificationParcel, KeybaseVerificationResult>
mKeybaseOpHelper;
+ /**
+ * Creates new instance of this fragment
+ */
+ public static ViewKeyKeybaseFragment newInstance(Uri dataUri) {
+ ViewKeyKeybaseFragment frag = new ViewKeyKeybaseFragment();
+ Bundle args = new Bundle();
+ args.putParcelable(ARG_DATA_URI, dataUri);
+
+ frag.setArguments(args);
+
+ return frag;
+ }
+
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup superContainer, Bundle savedInstanceState) {
View root = super.onCreateView(inflater, superContainer, savedInstanceState);
View view = inflater.inflate(R.layout.view_key_adv_keybase_fragment, getContainer());
mInflater = inflater;
- mTrustReadout = (TextView) view.findViewById(R.id.view_key_trust_readout);
- mStartSearch = view.findViewById(R.id.view_key_trust_search_cloud);
- mStartSearch.setEnabled(false);
mReportHeader = (TextView) view.findViewById(R.id.view_key_trust_cloud_narrative);
mProofListing = (TableLayout) view.findViewById(R.id.view_key_proof_list);
mProofVerifyHeader = view.findViewById(R.id.view_key_proof_verify_header);
@@ -157,83 +165,45 @@ public class ViewKeyTrustFragment extends LoaderFragment implements
}
boolean nothingSpecial = true;
- StringBuilder message = new StringBuilder();
// Swap the new cursor in. (The framework will take care of closing the
// old cursor once we return.)
if (data.moveToFirst()) {
- if (data.getInt(INDEX_UNIFIED_HAS_ANY_SECRET) != 0) {
- message.append(getString(R.string.key_trust_it_is_yours)).append("\n");
- nothingSpecial = false;
- } else if (data.getInt(INDEX_VERIFIED) != 0) {
- message.append(getString(R.string.key_trust_already_verified)).append("\n");
- nothingSpecial = false;
- }
+ final byte[] fp = data.getBlob(INDEX_TRUST_FINGERPRINT);
+ final String fingerprint = KeyFormattingUtils.convertFingerprintToHex(fp);
- // If this key is revoked, don’t trust it!
- if (data.getInt(INDEX_TRUST_IS_REVOKED) != 0) {
- message.append(getString(R.string.key_trust_revoked)).
- append(getString(R.string.key_trust_old_keys));
+ startSearch(fingerprint);
+ }
- nothingSpecial = false;
- } else {
- if (data.getInt(INDEX_TRUST_IS_EXPIRED) != 0) {
+ setContentShown(true);
+ }
- // if expired, don’t trust it!
- message.append(getString(R.string.key_trust_expired)).
- append(getString(R.string.key_trust_old_keys));
+ private void startSearch(final String fingerprint) {
+ final Preferences.ProxyPrefs proxyPrefs =
+ Preferences.getPreferences(getActivity()).getProxyPrefs();
- nothingSpecial = false;
- }
+ OrbotHelper.DialogActions dialogActions = new OrbotHelper.DialogActions() {
+ @Override
+ public void onOrbotStarted() {
+ new DescribeKey(proxyPrefs.parcelableProxy).execute(fingerprint);
}
- if (nothingSpecial) {
- message.append(getString(R.string.key_trust_maybe));
+ @Override
+ public void onNeutralButton() {
+ new DescribeKey(ParcelableProxy.getForNoProxy())
+ .execute(fingerprint);
}
- final byte[] fp = data.getBlob(INDEX_TRUST_FINGERPRINT);
- final String fingerprint = KeyFormattingUtils.convertFingerprintToHex(fp);
- if (fingerprint != null) {
+ @Override
+ public void onCancel() {
- mStartSearch.setEnabled(true);
- mStartSearch.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- final Preferences.ProxyPrefs proxyPrefs =
- Preferences.getPreferences(getActivity()).getProxyPrefs();
-
- OrbotHelper.DialogActions dialogActions = new OrbotHelper.DialogActions() {
- @Override
- public void onOrbotStarted() {
- mStartSearch.setEnabled(false);
- new DescribeKey(proxyPrefs.parcelableProxy).execute(fingerprint);
- }
-
- @Override
- public void onNeutralButton() {
- mStartSearch.setEnabled(false);
- new DescribeKey(ParcelableProxy.getForNoProxy())
- .execute(fingerprint);
- }
-
- @Override
- public void onCancel() {
-
- }
- };
-
- if (OrbotHelper.putOrbotInRequiredState(dialogActions, getActivity())) {
- mStartSearch.setEnabled(false);
- new DescribeKey(proxyPrefs.parcelableProxy).execute(fingerprint);
- }
- }
- });
}
- }
+ };
- mTrustReadout.setText(message);
- setContentShown(true);
+ if (OrbotHelper.putOrbotInRequiredState(dialogActions, getActivity())) {
+ new DescribeKey(proxyPrefs.parcelableProxy).execute(fingerprint);
+ }
}
/**
@@ -299,17 +269,16 @@ public class ViewKeyTrustFragment extends LoaderFragment implements
return new ResultPage(getString(R.string.key_trust_results_prefix), proofList);
}
- private SpannableStringBuilder formatSpannableString(SpannableStringBuilder proofLinks,String proofType){
+ private SpannableStringBuilder formatSpannableString(SpannableStringBuilder proofLinks, String proofType) {
//Formatting SpannableStringBuilder with String.format() causes the links to stop working.
//This method is to insert the links while reserving the links
SpannableStringBuilder ssb = new SpannableStringBuilder();
ssb.append(proofType);
- if(proofType.contains("%s")){
+ if (proofType.contains("%s")) {
int i = proofType.indexOf("%s");
- ssb.replace(i,i+2,proofLinks);
- }
- else ssb.append(proofLinks);
+ ssb.replace(i, i + 2, proofLinks);
+ } else ssb.append(proofLinks);
return ssb;
}
@@ -343,7 +312,6 @@ public class ViewKeyTrustFragment extends LoaderFragment implements
result.mHeader = getActivity().getString(R.string.key_trust_no_cloud_evidence);
}
- mStartSearch.setVisibility(View.GONE);
mReportHeader.setVisibility(View.VISIBLE);
mProofListing.setVisibility(View.VISIBLE);
mReportHeader.setText(result.mHeader);
@@ -358,22 +326,35 @@ public class ViewKeyTrustFragment extends LoaderFragment implements
text.setMovementMethod(LinkMovementMethod.getInstance());
mProofListing.addView(row);
}
-
- // mSearchReport.loadDataWithBaseURL("file:///android_res/drawable/", s, "text/html", "UTF-8", null);
}
}
private String getProofNarrative(int proofType) {
int stringIndex;
switch (proofType) {
- case Proof.PROOF_TYPE_TWITTER: stringIndex = R.string.keybase_narrative_twitter; break;
- case Proof.PROOF_TYPE_GITHUB: stringIndex = R.string.keybase_narrative_github; break;
- case Proof.PROOF_TYPE_DNS: stringIndex = R.string.keybase_narrative_dns; break;
- case Proof.PROOF_TYPE_WEB_SITE: stringIndex = R.string.keybase_narrative_web_site; break;
- case Proof.PROOF_TYPE_HACKERNEWS: stringIndex = R.string.keybase_narrative_hackernews; break;
- case Proof.PROOF_TYPE_COINBASE: stringIndex = R.string.keybase_narrative_coinbase; break;
- case Proof.PROOF_TYPE_REDDIT: stringIndex = R.string.keybase_narrative_reddit; break;
- default: stringIndex = R.string.keybase_narrative_unknown;
+ case Proof.PROOF_TYPE_TWITTER:
+ stringIndex = R.string.keybase_narrative_twitter;
+ break;
+ case Proof.PROOF_TYPE_GITHUB:
+ stringIndex = R.string.keybase_narrative_github;
+ break;
+ case Proof.PROOF_TYPE_DNS:
+ stringIndex = R.string.keybase_narrative_dns;
+ break;
+ case Proof.PROOF_TYPE_WEB_SITE:
+ stringIndex = R.string.keybase_narrative_web_site;
+ break;
+ case Proof.PROOF_TYPE_HACKERNEWS:
+ stringIndex = R.string.keybase_narrative_hackernews;
+ break;
+ case Proof.PROOF_TYPE_COINBASE:
+ stringIndex = R.string.keybase_narrative_coinbase;
+ break;
+ case Proof.PROOF_TYPE_REDDIT:
+ stringIndex = R.string.keybase_narrative_reddit;
+ break;
+ default:
+ stringIndex = R.string.keybase_narrative_unknown;
}
return getActivity().getString(stringIndex);
}
@@ -390,14 +371,22 @@ public class ViewKeyTrustFragment extends LoaderFragment implements
// which proofs do we have working verifiers for?
private boolean haveProofFor(int proofType) {
switch (proofType) {
- case Proof.PROOF_TYPE_TWITTER: return true;
- case Proof.PROOF_TYPE_GITHUB: return true;
- case Proof.PROOF_TYPE_DNS: return true;
- case Proof.PROOF_TYPE_WEB_SITE: return true;
- case Proof.PROOF_TYPE_HACKERNEWS: return true;
- case Proof.PROOF_TYPE_COINBASE: return true;
- case Proof.PROOF_TYPE_REDDIT: return true;
- default: return false;
+ case Proof.PROOF_TYPE_TWITTER:
+ return true;
+ case Proof.PROOF_TYPE_GITHUB:
+ return true;
+ case Proof.PROOF_TYPE_DNS:
+ return true;
+ case Proof.PROOF_TYPE_WEB_SITE:
+ return true;
+ case Proof.PROOF_TYPE_HACKERNEWS:
+ return true;
+ case Proof.PROOF_TYPE_COINBASE:
+ return true;
+ case Proof.PROOF_TYPE_REDDIT:
+ return true;
+ default:
+ return false;
}
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeyAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeyAdapter.java
index 59d772d63..56d273c7c 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeyAdapter.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeyAdapter.java
@@ -23,6 +23,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
+import java.util.Date;
import android.content.Context;
import android.database.Cursor;
@@ -193,9 +194,9 @@ public class KeyAdapter extends CursorAdapter {
String dateTime = DateUtils.formatDateTime(context,
item.mCreation.getTime(),
DateUtils.FORMAT_SHOW_DATE
+ | DateUtils.FORMAT_SHOW_TIME
| DateUtils.FORMAT_SHOW_YEAR
| DateUtils.FORMAT_ABBREV_MONTH);
-
mCreationDate.setText(context.getString(R.string.label_key_created,
dateTime));
mCreationDate.setTextColor(textColor);
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/LinkedIdsAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/LinkedIdsAdapter.java
new file mode 100644
index 000000000..5cf0e6e08
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/LinkedIdsAdapter.java
@@ -0,0 +1,238 @@
+/*
+ * Copyright (C) 2014-2015 Dominik Schürmann <dominik@dominikschuermann.de>
+ * Copyright (C) 2015 Vincent Breitmoser <v.breitmoser@mugenguild.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui.adapter;
+
+import android.animation.ObjectAnimator;
+import android.app.Activity;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Build;
+import android.support.v4.content.CursorLoader;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.linked.LinkedAttribute;
+import org.sufficientlysecure.keychain.linked.UriAttribute;
+import org.sufficientlysecure.keychain.provider.KeychainContract.Certs;
+import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets;
+import org.sufficientlysecure.keychain.ui.linked.LinkedIdViewFragment;
+import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
+import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils.State;
+import org.sufficientlysecure.keychain.ui.util.SubtleAttentionSeeker;
+import org.sufficientlysecure.keychain.util.FilterCursorWrapper;
+
+import java.io.IOException;
+import java.util.WeakHashMap;
+
+public class LinkedIdsAdapter extends UserAttributesAdapter {
+ private final boolean mIsSecret;
+ protected LayoutInflater mInflater;
+ WeakHashMap<Integer,UriAttribute> mLinkedIdentityCache = new WeakHashMap<>();
+
+ private Cursor mUnfilteredCursor;
+
+ private TextView mExpander;
+
+ public LinkedIdsAdapter(Context context, Cursor c, int flags,
+ boolean isSecret, TextView expander) {
+ super(context, c, flags);
+ mInflater = LayoutInflater.from(context);
+ mIsSecret = isSecret;
+
+ if (expander != null) {
+ expander.setVisibility(View.GONE);
+ /* don't show an expander (maybe in some sort of advanced view?)
+ mExpander = expander;
+ mExpander.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ showUnfiltered();
+ }
+ });
+ */
+ }
+ }
+
+ @Override
+ public Cursor swapCursor(Cursor cursor) {
+ if (cursor == null) {
+ mUnfilteredCursor = null;
+ return super.swapCursor(null);
+ }
+ mUnfilteredCursor = cursor;
+ FilterCursorWrapper filteredCursor = new FilterCursorWrapper(cursor) {
+ @Override
+ public boolean isVisible(Cursor cursor) {
+ UriAttribute id = getItemAtPosition(cursor);
+ return id instanceof LinkedAttribute;
+ }
+ };
+
+ if (mExpander != null) {
+ int hidden = filteredCursor.getHiddenCount();
+ if (hidden == 0) {
+ mExpander.setVisibility(View.GONE);
+ } else {
+ mExpander.setVisibility(View.VISIBLE);
+ mExpander.setText(mContext.getResources().getQuantityString(
+ R.plurals.linked_id_expand, hidden));
+ }
+ }
+
+ return super.swapCursor(filteredCursor);
+ }
+
+ private void showUnfiltered() {
+ mExpander.setVisibility(View.GONE);
+ super.swapCursor(mUnfilteredCursor);
+ }
+
+ @Override
+ public void bindView(View view, Context context, Cursor cursor) {
+
+ ViewHolder holder = (ViewHolder) view.getTag();
+
+ if (!mIsSecret) {
+ int isVerified = cursor.getInt(INDEX_VERIFIED);
+ switch (isVerified) {
+ case Certs.VERIFIED_SECRET:
+ KeyFormattingUtils.setStatusImage(mContext, holder.vVerified,
+ null, State.VERIFIED, KeyFormattingUtils.DEFAULT_COLOR);
+ break;
+ case Certs.VERIFIED_SELF:
+ KeyFormattingUtils.setStatusImage(mContext, holder.vVerified,
+ null, State.UNVERIFIED, KeyFormattingUtils.DEFAULT_COLOR);
+ break;
+ default:
+ KeyFormattingUtils.setStatusImage(mContext, holder.vVerified,
+ null, State.INVALID, KeyFormattingUtils.DEFAULT_COLOR);
+ break;
+ }
+ }
+
+ UriAttribute id = getItemAtPosition(cursor);
+ holder.setData(mContext, id);
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ view.setTransitionName(id.mUri.toString());
+ }
+
+ }
+
+ public UriAttribute getItemAtPosition(Cursor cursor) {
+ int rank = cursor.getInt(INDEX_RANK);
+ Log.d(Constants.TAG, "requested rank: " + rank);
+
+ UriAttribute ret = mLinkedIdentityCache.get(rank);
+ if (ret != null) {
+ Log.d(Constants.TAG, "cached!");
+ return ret;
+ }
+ Log.d(Constants.TAG, "not cached!");
+
+ try {
+ byte[] data = cursor.getBlob(INDEX_ATTRIBUTE_DATA);
+ ret = LinkedAttribute.fromAttributeData(data);
+ mLinkedIdentityCache.put(rank, ret);
+ return ret;
+ } catch (IOException e) {
+ Log.e(Constants.TAG, "could not read linked identity subpacket data", e);
+ return null;
+ }
+ }
+
+ @Override
+ public UriAttribute getItem(int position) {
+ Cursor cursor = getCursor();
+ cursor.moveToPosition(position);
+ return getItemAtPosition(cursor);
+ }
+
+ @Override
+ public View newView(Context context, Cursor cursor, ViewGroup parent) {
+ View v = mInflater.inflate(R.layout.linked_id_item, null);
+ ViewHolder holder = new ViewHolder(v);
+ v.setTag(holder);
+ return v;
+ }
+
+ // don't show revoked user ids, irrelevant for average users
+ public static final String LINKED_IDS_WHERE = UserPackets.IS_REVOKED + " = 0";
+
+ public static CursorLoader createLoader(Activity activity, Uri dataUri) {
+ Uri baseUri = UserPackets.buildLinkedIdsUri(dataUri);
+ return new CursorLoader(activity, baseUri,
+ UserIdsAdapter.USER_PACKETS_PROJECTION, LINKED_IDS_WHERE, null, null);
+ }
+
+ public LinkedIdViewFragment getLinkedIdFragment(Uri baseUri,
+ int position, byte[] fingerprint) throws IOException {
+ Cursor c = getCursor();
+ c.moveToPosition(position);
+ int rank = c.getInt(UserIdsAdapter.INDEX_RANK);
+
+ Uri dataUri = UserPackets.buildLinkedIdsUri(baseUri);
+ return LinkedIdViewFragment.newInstance(dataUri, rank, mIsSecret, fingerprint);
+ }
+
+ public static class ViewHolder {
+ final public ImageView vVerified;
+ final public ImageView vIcon;
+ final public TextView vTitle;
+ final public TextView vComment;
+
+ public ViewHolder(View view) {
+ vVerified = (ImageView) view.findViewById(R.id.linked_id_certified_icon);
+ vIcon = (ImageView) view.findViewById(R.id.linked_id_type_icon);
+ vTitle = (TextView) view.findViewById(R.id.linked_id_title);
+ vComment = (TextView) view.findViewById(R.id.linked_id_comment);
+ }
+
+ public void setData(Context context, UriAttribute id) {
+
+ vTitle.setText(id.getDisplayTitle(context));
+
+ String comment = id.getDisplayComment(context);
+ if (comment != null) {
+ vComment.setVisibility(View.VISIBLE);
+ vComment.setText(comment);
+ } else {
+ vComment.setVisibility(View.GONE);
+ }
+
+ vIcon.setImageResource(id.getDisplayIcon());
+
+ }
+
+ public void seekAttention() {
+ ObjectAnimator anim = SubtleAttentionSeeker.tintText(vComment, 1000);
+ anim.setStartDelay(200);
+ anim.start();
+ }
+
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/LinkedIdsCertAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/LinkedIdsCertAdapter.java
new file mode 100644
index 000000000..5ecd9f408
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/LinkedIdsCertAdapter.java
@@ -0,0 +1,57 @@
+package org.sufficientlysecure.keychain.ui.adapter;
+
+
+import android.app.Activity;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.support.v4.content.CursorLoader;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.CursorAdapter;
+
+import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets;
+
+
+public class LinkedIdsCertAdapter extends CursorAdapter {
+
+ public static final String[] USER_CERTS_PROJECTION = new String[]{
+ UserPackets._ID,
+ UserPackets.TYPE,
+ UserPackets.USER_ID,
+ UserPackets.ATTRIBUTE_DATA,
+ UserPackets.RANK,
+ UserPackets.VERIFIED,
+ UserPackets.IS_PRIMARY,
+ UserPackets.IS_REVOKED
+ };
+ protected static final int INDEX_ID = 0;
+ protected static final int INDEX_TYPE = 1;
+ protected static final int INDEX_USER_ID = 2;
+ protected static final int INDEX_ATTRIBUTE_DATA = 3;
+ protected static final int INDEX_RANK = 4;
+ protected static final int INDEX_VERIFIED = 5;
+ protected static final int INDEX_IS_PRIMARY = 6;
+ protected static final int INDEX_IS_REVOKED = 7;
+
+ public LinkedIdsCertAdapter(Context context, Cursor c, int flags) {
+ super(context, c, flags);
+ }
+
+ @Override
+ public void bindView(View view, Context context, Cursor cursor) {
+
+ }
+
+ @Override
+ public View newView(Context context, Cursor cursor, ViewGroup parent) {
+ return null;
+ }
+
+ public static CursorLoader createLoader(Activity activity, Uri dataUri) {
+ Uri baseUri = UserPackets.buildLinkedIdsUri(dataUri);
+ return new CursorLoader(activity, baseUri,
+ UserIdsAdapter.USER_PACKETS_PROJECTION, null, null, null);
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/MultiUserIdsAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/MultiUserIdsAdapter.java
index 6b16e8445..b91abf076 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/MultiUserIdsAdapter.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/MultiUserIdsAdapter.java
@@ -171,7 +171,7 @@ public class MultiUserIdsAdapter extends CursorAdapter {
CertifyAction action = actions.get(keyId);
if (actions.get(keyId) == null) {
- actions.put(keyId, new CertifyAction(keyId, uids));
+ actions.put(keyId, new CertifyAction(keyId, uids, null));
} else {
action.mUserIds.addAll(uids);
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/SelectKeyCursorAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/SelectKeyCursorAdapter.java
index 4ea651bb5..f01f25200 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/SelectKeyCursorAdapter.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/SelectKeyCursorAdapter.java
@@ -137,9 +137,9 @@ abstract public class SelectKeyCursorAdapter extends CursorAdapter {
String dateTime = DateUtils.formatDateTime(context,
cursor.getLong(mIndexCreation) * 1000,
DateUtils.FORMAT_SHOW_DATE
+ | DateUtils.FORMAT_SHOW_TIME
| DateUtils.FORMAT_SHOW_YEAR
| DateUtils.FORMAT_ABBREV_MONTH);
-
h.creation.setText(context.getString(R.string.label_key_created, dateTime));
h.creation.setVisibility(View.VISIBLE);
} else {
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/UserAttributesAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/UserAttributesAdapter.java
index 457083770..e0abaf4b0 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/UserAttributesAdapter.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/UserAttributesAdapter.java
@@ -8,22 +8,24 @@ import android.view.View;
import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets;
public abstract class UserAttributesAdapter extends CursorAdapter {
- public static final String[] USER_IDS_PROJECTION = new String[]{
+ public static final String[] USER_PACKETS_PROJECTION = new String[]{
UserPackets._ID,
UserPackets.TYPE,
UserPackets.USER_ID,
+ UserPackets.ATTRIBUTE_DATA,
UserPackets.RANK,
UserPackets.VERIFIED,
UserPackets.IS_PRIMARY,
UserPackets.IS_REVOKED
};
- protected static final int INDEX_ID = 0;
- protected static final int INDEX_TYPE = 1;
- protected static final int INDEX_USER_ID = 2;
- protected static final int INDEX_RANK = 3;
- protected static final int INDEX_VERIFIED = 4;
- protected static final int INDEX_IS_PRIMARY = 5;
- protected static final int INDEX_IS_REVOKED = 6;
+ public static final int INDEX_ID = 0;
+ public static final int INDEX_TYPE = 1;
+ public static final int INDEX_USER_ID = 2;
+ public static final int INDEX_ATTRIBUTE_DATA = 3;
+ public static final int INDEX_RANK = 4;
+ public static final int INDEX_VERIFIED = 5;
+ public static final int INDEX_IS_PRIMARY = 6;
+ public static final int INDEX_IS_REVOKED = 7;
public UserAttributesAdapter(Context context, Cursor c, int flags) {
super(context, c, flags);
@@ -46,4 +48,5 @@ public abstract class UserAttributesAdapter extends CursorAdapter {
mCursor.moveToPosition(position);
return mCursor.getInt(INDEX_VERIFIED);
}
+
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/UserIdsAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/UserIdsAdapter.java
index e2c6b0928..0f4312dad 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/UserIdsAdapter.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/UserIdsAdapter.java
@@ -188,7 +188,7 @@ public class UserIdsAdapter extends UserAttributesAdapter {
public static CursorLoader createLoader(Activity activity, Uri dataUri) {
Uri baseUri = UserPackets.buildUserIdsUri(dataUri);
return new CursorLoader(activity, baseUri,
- UserIdsAdapter.USER_IDS_PROJECTION, USER_IDS_WHERE, null, null);
+ UserIdsAdapter.USER_PACKETS_PROJECTION, USER_IDS_WHERE, null, null);
}
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseActivity.java
index fcf5dc11e..aa4e7d840 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseActivity.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseActivity.java
@@ -30,6 +30,7 @@ import android.view.ViewGroup;
import android.widget.TextView;
import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.service.KeyserverSyncAdapterService;
import org.sufficientlysecure.keychain.ui.util.ThemeChanger;
/**
@@ -51,6 +52,7 @@ public abstract class BaseActivity extends AppCompatActivity {
@Override
protected void onResume() {
super.onResume();
+ KeyserverSyncAdapterService.cancelUpdates(this);
if (mThemeChanger.changeTheme()) {
Intent intent = getIntent();
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/CryptoOperationFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/CryptoOperationFragment.java
index 52507f3e9..de90d48fd 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/CryptoOperationFragment.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/CryptoOperationFragment.java
@@ -18,9 +18,9 @@
package org.sufficientlysecure.keychain.ui.base;
+
import android.content.Context;
import android.content.Intent;
-import android.os.Bundle;
import android.os.Parcelable;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
@@ -50,17 +50,21 @@ import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
* @see KeychainService
*
*/
-abstract class CryptoOperationFragment<T extends Parcelable, S extends OperationResult>
+public abstract class CryptoOperationFragment<T extends Parcelable, S extends OperationResult>
extends Fragment implements CryptoOperationHelper.Callback<T, S> {
final private CryptoOperationHelper<T, S> mOperationHelper;
+ public CryptoOperationFragment() {
+ mOperationHelper = new CryptoOperationHelper<>(1, this, this, R.string.progress_processing);
+ }
+
public CryptoOperationFragment(Integer initialProgressMsg) {
mOperationHelper = new CryptoOperationHelper<>(1, this, this, initialProgressMsg);
}
- public CryptoOperationFragment() {
- mOperationHelper = new CryptoOperationHelper<>(1, this, this, R.string.progress_processing);
+ public CryptoOperationFragment(int id, Integer initialProgressMsg) {
+ mOperationHelper = new CryptoOperationHelper<>(id, this, this, initialProgressMsg);
}
@Override
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/CryptoOperationHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/CryptoOperationHelper.java
index b33128978..52c6797d5 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/CryptoOperationHelper.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/CryptoOperationHelper.java
@@ -19,6 +19,8 @@
package org.sufficientlysecure.keychain.ui.base;
+import java.util.Date;
+
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Intent;
@@ -70,9 +72,11 @@ public class CryptoOperationHelper<T extends Parcelable, S extends OperationResu
// particular helper. a request code looks as follows:
// (id << 9) + (1<<8) + REQUEST_CODE_X
// that is, starting from LSB, there are 8 bits request code, 1
- // fixed bit set, then 7 bit operator-id code. the first two
- // summands are stored in the mId for easy operation.
- private final int mId;
+ // fixed bit set, then 7 bit helper-id code. the first two
+ // summands are stored in the mHelperId for easy operation.
+ private final int mHelperId;
+ // bitmask for helperId is everything except the least 8 bits
+ public static final int HELPER_ID_BITMASK = ~0xff;
public static final int REQUEST_CODE_PASSPHRASE = 1;
public static final int REQUEST_CODE_NFC = 2;
@@ -92,7 +96,7 @@ public class CryptoOperationHelper<T extends Parcelable, S extends OperationResu
*/
public CryptoOperationHelper(int id, FragmentActivity activity, Callback<T, S> callback,
Integer progressMessageString) {
- mId = (id << 9) + (1<<8);
+ mHelperId = (id << 9) + (1<<8);
mActivity = activity;
mUseFragment = false;
mCallback = callback;
@@ -103,7 +107,7 @@ public class CryptoOperationHelper<T extends Parcelable, S extends OperationResu
* if OperationHelper is being integrated into a fragment
*/
public CryptoOperationHelper(int id, Fragment fragment, Callback<T, S> callback, Integer progressMessageString) {
- mId = (id << 9) + (1<<8);
+ mHelperId = (id << 9) + (1<<8);
mFragment = fragment;
mUseFragment = true;
mProgressMessageResource = progressMessageString;
@@ -162,9 +166,9 @@ public class CryptoOperationHelper<T extends Parcelable, S extends OperationResu
protected void startActivityForResult(Intent intent, int requestCode) {
if (mUseFragment) {
- mFragment.startActivityForResult(intent, mId + requestCode);
+ mFragment.startActivityForResult(intent, mHelperId + requestCode);
} else {
- mActivity.startActivityForResult(intent, mId + requestCode);
+ mActivity.startActivityForResult(intent, mHelperId + requestCode);
}
}
@@ -176,13 +180,13 @@ public class CryptoOperationHelper<T extends Parcelable, S extends OperationResu
public boolean handleActivityResult(int requestCode, int resultCode, Intent data) {
Log.d(Constants.TAG, "received activity result in OperationHelper");
- if ((requestCode & mId) != mId) {
+ if ((requestCode & HELPER_ID_BITMASK) != mHelperId) {
// this wasn't meant for us to handle
return false;
}
Log.d(Constants.TAG, "handling activity result in OperationHelper");
- // filter out mId from requestCode
- requestCode ^= mId;
+ // filter out mHelperId from requestCode
+ requestCode ^= mHelperId;
if (resultCode == Activity.RESULT_CANCELED) {
mCallback.onCryptoOperationCancelled();
@@ -313,7 +317,7 @@ public class CryptoOperationHelper<T extends Parcelable, S extends OperationResu
}
public void cryptoOperation() {
- cryptoOperation(new CryptoInputParcel());
+ cryptoOperation(new CryptoInputParcel(new Date()));
}
public void onHandleResult(OperationResult result) {
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/FileDialogFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/FileDialogFragment.java
index 5ef8618ce..84774ae5e 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/FileDialogFragment.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/FileDialogFragment.java
@@ -23,6 +23,7 @@ import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
+import android.os.Build;
import android.os.Bundle;
import android.os.Message;
import android.os.Messenger;
@@ -119,14 +120,19 @@ public class FileDialogFragment extends DialogFragment {
mFilename = (EditText) view.findViewById(R.id.input);
mFilename.setText(mFile.getName());
mBrowse = (ImageButton) view.findViewById(R.id.btn_browse);
- mBrowse.setOnClickListener(new View.OnClickListener() {
- public void onClick(View v) {
- // only .asc or .gpg files
- // setting it to text/plain prevents Cynaogenmod's file manager from selecting asc
- // or gpg types!
- FileHelper.openFile(FileDialogFragment.this, Uri.fromFile(mFile), "*/*", REQUEST_CODE);
- }
- });
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
+ mBrowse.setVisibility(View.GONE);
+ } else {
+ mBrowse.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ // only .asc or .gpg files
+ // setting it to text/plain prevents Cynaogenmod's file manager from selecting asc
+ // or gpg types!
+ FileHelper.saveDocumentKitKat(
+ FileDialogFragment.this, "*/*", mFile.getName(), REQUEST_CODE);
+ }
+ });
+ }
mCheckBox = (CheckBox) view.findViewById(R.id.checkbox);
if (checkboxText == null) {
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateFinalFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateFinalFragment.java
new file mode 100644
index 000000000..e09b1e755
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateFinalFragment.java
@@ -0,0 +1,225 @@
+package org.sufficientlysecure.keychain.ui.linked;
+
+
+import android.graphics.PorterDuff;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+import android.widget.ViewAnimator;
+
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.linked.LinkedAttribute;
+import org.sufficientlysecure.keychain.linked.LinkedTokenResource;
+import org.sufficientlysecure.keychain.operations.results.LinkedVerifyResult;
+import org.sufficientlysecure.keychain.operations.results.OperationResult;
+import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
+import org.sufficientlysecure.keychain.pgp.WrappedUserAttribute;
+import org.sufficientlysecure.keychain.service.SaveKeyringParcel;
+import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
+import org.sufficientlysecure.keychain.ui.base.CryptoOperationFragment;
+import org.sufficientlysecure.keychain.ui.util.Notify;
+
+
+public abstract class LinkedIdCreateFinalFragment extends CryptoOperationFragment {
+
+ protected LinkedIdWizard mLinkedIdWizard;
+
+ private ImageView mVerifyImage;
+ private TextView mVerifyStatus;
+ private ViewAnimator mVerifyAnimator;
+
+ // This is a resource, set AFTER it has been verified
+ LinkedTokenResource mVerifiedResource = null;
+ private ViewAnimator mVerifyButtonAnimator;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ mLinkedIdWizard = (LinkedIdWizard) getActivity();
+ }
+
+ protected abstract View newView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState);
+
+ @Override @NonNull
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ final View view = newView(inflater, container, savedInstanceState);
+
+ View nextButton = view.findViewById(R.id.next_button);
+ if (nextButton != null) {
+ nextButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ cryptoOperation();
+ }
+ });
+ }
+
+ view.findViewById(R.id.back_button).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mLinkedIdWizard.loadFragment(null, null, LinkedIdWizard.FRAG_ACTION_TO_LEFT);
+ }
+ });
+
+ mVerifyAnimator = (ViewAnimator) view.findViewById(R.id.verify_progress);
+ mVerifyImage = (ImageView) view.findViewById(R.id.verify_image);
+ mVerifyStatus = (TextView) view.findViewById(R.id.verify_status);
+ mVerifyButtonAnimator = (ViewAnimator) view.findViewById(R.id.verify_buttons);
+
+ view.findViewById(R.id.button_verify).setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ proofVerify();
+ }
+ });
+
+ view.findViewById(R.id.button_retry).setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ proofVerify();
+ }
+ });
+
+ setVerifyProgress(false, null);
+ mVerifyStatus.setText(R.string.linked_verify_pending);
+
+ return view;
+ }
+
+ abstract LinkedTokenResource getResource(OperationLog log);
+
+ private void setVerifyProgress(boolean on, Boolean success) {
+ if (success == null) {
+ mVerifyStatus.setText(R.string.linked_verifying);
+ displayButton(on ? 2 : 0);
+ } else if (success) {
+ mVerifyStatus.setText(R.string.linked_verify_success);
+ mVerifyImage.setImageResource(R.drawable.status_signature_verified_cutout_24dp);
+ mVerifyImage.setColorFilter(getResources().getColor(R.color.android_green_dark),
+ PorterDuff.Mode.SRC_IN);
+ displayButton(2);
+ } else {
+ mVerifyStatus.setText(R.string.linked_verify_error);
+ mVerifyImage.setImageResource(R.drawable.status_signature_unknown_cutout_24dp);
+ mVerifyImage.setColorFilter(getResources().getColor(R.color.android_red_dark),
+ PorterDuff.Mode.SRC_IN);
+ displayButton(1);
+ }
+ mVerifyAnimator.setDisplayedChild(on ? 1 : 0);
+ }
+
+ public void displayButton(int button) {
+ if (mVerifyButtonAnimator.getDisplayedChild() == button) {
+ return;
+ }
+ mVerifyButtonAnimator.setDisplayedChild(button);
+ }
+
+ protected void proofVerify() {
+ setVerifyProgress(true, null);
+
+ new AsyncTask<Void,Void,LinkedVerifyResult>() {
+
+ @Override
+ protected LinkedVerifyResult doInBackground(Void... params) {
+ long timer = System.currentTimeMillis();
+
+ OperationLog log = new OperationLog();
+ LinkedTokenResource resource = getResource(log);
+ if (resource == null) {
+ return new LinkedVerifyResult(LinkedVerifyResult.RESULT_ERROR, log);
+ }
+
+ LinkedVerifyResult result = resource.verify(getActivity(), mLinkedIdWizard.mFingerprint);
+
+ // ux flow: this operation should take at last a second
+ timer = System.currentTimeMillis() -timer;
+ if (timer < 1000) try {
+ Thread.sleep(1000 -timer);
+ } catch (InterruptedException e) {
+ // never mind
+ }
+
+ if (result.success()) {
+ mVerifiedResource = resource;
+ }
+ return result;
+ }
+
+ @Override
+ protected void onPostExecute(LinkedVerifyResult result) {
+ super.onPostExecute(result);
+ if (result.success()) {
+ setVerifyProgress(false, true);
+ } else {
+ setVerifyProgress(false, false);
+ // on error, show error message
+ result.createNotify(getActivity()).show(LinkedIdCreateFinalFragment.this);
+ }
+ }
+ }.execute();
+
+ }
+
+ @Override
+ protected void cryptoOperation() {
+ if (mVerifiedResource == null) {
+ Notify.create(getActivity(), R.string.linked_need_verify, Notify.Style.ERROR)
+ .show(LinkedIdCreateFinalFragment.this);
+ return;
+ }
+
+ super.cryptoOperation();
+ }
+
+ @Override
+ protected void cryptoOperation(CryptoInputParcel cryptoInput) {
+ if (mVerifiedResource == null) {
+ Notify.create(getActivity(), R.string.linked_need_verify, Notify.Style.ERROR)
+ .show(LinkedIdCreateFinalFragment.this);
+ return;
+ }
+
+ super.cryptoOperation(cryptoInput);
+ }
+
+ @Nullable
+ @Override
+ public Parcelable createOperationInput() {
+ SaveKeyringParcel skp =
+ new SaveKeyringParcel(mLinkedIdWizard.mMasterKeyId, mLinkedIdWizard.mFingerprint);
+
+ WrappedUserAttribute ua =
+ LinkedAttribute.fromResource(mVerifiedResource).toUserAttribute();
+
+ skp.mAddUserAttribute.add(ua);
+
+ return skp;
+ }
+
+ @Override
+ public void onCryptoOperationSuccess(OperationResult result) {
+ // if bad -> display here!
+ if (!result.success()) {
+ result.createNotify(getActivity()).show(LinkedIdCreateFinalFragment.this);
+ return;
+ }
+
+ getActivity().finish();
+ }
+
+ @Override
+ public void onCryptoOperationError(OperationResult result) {
+
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateGithubFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateGithubFragment.java
new file mode 100644
index 000000000..ccb20a764
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateGithubFragment.java
@@ -0,0 +1,706 @@
+/*
+ * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui.linked;
+
+
+import java.io.BufferedInputStream;
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.net.SocketTimeoutException;
+import java.net.URI;
+import java.net.URL;
+import java.util.Random;
+
+import android.app.Activity;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnDismissListener;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Build;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.app.ActivityOptionsCompat;
+import android.support.v4.app.FragmentActivity;
+import android.util.Base64;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.webkit.CookieManager;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.TextView;
+import android.widget.ViewAnimator;
+
+import javax.net.ssl.HttpsURLConnection;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.spongycastle.util.encoders.Hex;
+import org.sufficientlysecure.keychain.BuildConfig;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.linked.LinkedAttribute;
+import org.sufficientlysecure.keychain.linked.resources.GithubResource;
+import org.sufficientlysecure.keychain.operations.results.EditKeyResult;
+import org.sufficientlysecure.keychain.pgp.WrappedUserAttribute;
+import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
+import org.sufficientlysecure.keychain.service.SaveKeyringParcel;
+import org.sufficientlysecure.keychain.ui.ViewKeyActivity;
+import org.sufficientlysecure.keychain.ui.base.CryptoOperationFragment;
+import org.sufficientlysecure.keychain.ui.util.Notify;
+import org.sufficientlysecure.keychain.ui.util.Notify.Style;
+import org.sufficientlysecure.keychain.ui.widget.StatusIndicator;
+import org.sufficientlysecure.keychain.ui.widget.StatusIndicator.Status;
+import org.sufficientlysecure.keychain.util.Log;
+
+
+public class LinkedIdCreateGithubFragment extends CryptoOperationFragment<SaveKeyringParcel,EditKeyResult> {
+
+ public static final String ARG_GITHUB_COOKIE = "github_cookie";
+ private Button mRetryButton;
+
+ enum State {
+ IDLE, AUTH_PROCESS, AUTH_ERROR, POST_PROCESS, POST_ERROR, LID_PROCESS, LID_ERROR, DONE
+ }
+
+ ViewAnimator mButtonContainer;
+
+ StatusIndicator mStatus1, mStatus2, mStatus3;
+
+ byte[] mFingerprint;
+ long mMasterKeyId;
+ private SaveKeyringParcel mSaveKeyringParcel;
+ private TextView mLinkedIdTitle, mLinkedIdComment;
+ private boolean mFinishOnStop;
+
+ public static LinkedIdCreateGithubFragment newInstance() {
+ return new LinkedIdCreateGithubFragment();
+ }
+
+ public LinkedIdCreateGithubFragment() {
+ super(null);
+ }
+
+ @Override @NonNull
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.linked_create_github_fragment, container, false);
+
+ mButtonContainer = (ViewAnimator) view.findViewById(R.id.button_container);
+
+ mStatus1 = (StatusIndicator) view.findViewById(R.id.linked_status_step1);
+ mStatus2 = (StatusIndicator) view.findViewById(R.id.linked_status_step2);
+ mStatus3 = (StatusIndicator) view.findViewById(R.id.linked_status_step3);
+
+ mRetryButton = (Button) view.findViewById(R.id.button_retry);
+
+ ((ImageView) view.findViewById(R.id.linked_id_type_icon)).setImageResource(R.drawable.linked_github);
+ ((ImageView) view.findViewById(R.id.linked_id_certified_icon)).setImageResource(R.drawable.octo_link_24dp);
+ mLinkedIdTitle = (TextView) view.findViewById(R.id.linked_id_title);
+ mLinkedIdComment = (TextView) view.findViewById(R.id.linked_id_comment);
+
+ view.findViewById(R.id.back_button).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ LinkedIdWizard activity = (LinkedIdWizard) getActivity();
+ if (activity == null) {
+ return;
+ }
+ activity.loadFragment(null, null, LinkedIdWizard.FRAG_ACTION_TO_LEFT);
+ }
+ });
+
+ view.findViewById(R.id.button_send).setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ step1GetOAuthCode();
+ // for animation testing
+ // onCryptoOperationSuccess(null);
+ }
+ });
+
+ return view;
+ }
+
+ @Override
+ public void onActivityCreated(@Nullable Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ LinkedIdWizard wizard = (LinkedIdWizard) getActivity();
+ mFingerprint = wizard.mFingerprint;
+ mMasterKeyId = wizard.mMasterKeyId;
+ }
+
+ private void step1GetOAuthCode() {
+
+ setState(State.AUTH_PROCESS);
+
+ mButtonContainer.setDisplayedChild(1);
+
+ new Handler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ oAuthRequest("github.com/login/oauth/authorize", BuildConfig.GITHUB_CLIENT_ID, "gist");
+ }
+ }, 300);
+
+ }
+
+ private void showRetryForOAuth() {
+
+ mRetryButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ v.setOnClickListener(null);
+ step1GetOAuthCode();
+ }
+ });
+ mButtonContainer.setDisplayedChild(3);
+
+ }
+
+ private void step1GetOAuthToken() {
+
+ if (mOAuthCode == null) {
+ setState(State.AUTH_ERROR);
+ showRetryForOAuth();
+ return;
+ }
+
+ Activity activity = getActivity();
+ if (activity == null) {
+ return;
+ }
+
+ final String gistText = GithubResource.generate(activity, mFingerprint);
+
+ new AsyncTask<Void,Void,JSONObject>() {
+
+ Exception mException;
+
+ @Override
+ protected JSONObject doInBackground(Void... dummy) {
+ try {
+
+ JSONObject params = new JSONObject();
+ params.put("client_id", BuildConfig.GITHUB_CLIENT_ID);
+ params.put("client_secret", BuildConfig.GITHUB_CLIENT_SECRET);
+ params.put("code", mOAuthCode);
+ params.put("state", mOAuthState);
+
+ return jsonHttpRequest("https://github.com/login/oauth/access_token", params, null);
+
+ } catch (IOException | HttpResultException e) {
+ mException = e;
+ } catch (JSONException e) {
+ throw new AssertionError("json error, this is a bug!");
+ }
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(JSONObject result) {
+ super.onPostExecute(result);
+
+ Activity activity = getActivity();
+ if (activity == null) {
+ // we couldn't show an error anyways
+ return;
+ }
+
+ Log.d(Constants.TAG, "response: " + result);
+
+ if (result == null || result.optString("access_token", null) == null) {
+ setState(State.AUTH_ERROR);
+ showRetryForOAuth();
+
+ if (result != null) {
+ Notify.create(activity, R.string.linked_error_auth_failed, Style.ERROR).show();
+ return;
+ }
+
+ if (mException instanceof SocketTimeoutException) {
+ Notify.create(activity, R.string.linked_error_timeout, Style.ERROR).show();
+ } else if (mException instanceof HttpResultException) {
+ Notify.create(activity, activity.getString(R.string.linked_error_http,
+ ((HttpResultException) mException).mResponse),
+ Style.ERROR).show();
+ } else if (mException instanceof IOException) {
+ Notify.create(activity, R.string.linked_error_network, Style.ERROR).show();
+ }
+
+ return;
+ }
+
+ step2PostGist(result.optString("access_token"), gistText);
+
+ }
+ }.execute();
+
+ }
+
+ private void step2PostGist(final String accessToken, final String gistText) {
+
+ setState(State.POST_PROCESS);
+
+ new AsyncTask<Void,Void,JSONObject>() {
+
+ Exception mException;
+
+ @Override
+ protected JSONObject doInBackground(Void... dummy) {
+ try {
+
+ long timer = System.currentTimeMillis();
+
+ JSONObject file = new JSONObject();
+ file.put("content", gistText);
+
+ JSONObject files = new JSONObject();
+ files.put("openpgp.txt", file);
+
+ JSONObject params = new JSONObject();
+ params.put("public", true);
+ params.put("description", getString(R.string.linked_gist_description));
+ params.put("files", files);
+
+ JSONObject result = jsonHttpRequest("https://api.github.com/gists", params, accessToken);
+
+ // ux flow: this operation should take at last a second
+ timer = System.currentTimeMillis() -timer;
+ if (timer < 1000) try {
+ Thread.sleep(1000 -timer);
+ } catch (InterruptedException e) {
+ // never mind
+ }
+
+ return result;
+
+ } catch (IOException | HttpResultException e) {
+ mException = e;
+ } catch (JSONException e) {
+ throw new AssertionError("json error, this is a bug!");
+ }
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(JSONObject result) {
+ super.onPostExecute(result);
+
+ Log.d(Constants.TAG, "response: " + result);
+
+ Activity activity = getActivity();
+ if (activity == null) {
+ // we couldn't show an error anyways
+ return;
+ }
+
+ if (result == null) {
+ setState(State.POST_ERROR);
+ showRetryForOAuth();
+
+ if (mException instanceof SocketTimeoutException) {
+ Notify.create(activity, R.string.linked_error_timeout, Style.ERROR).show();
+ } else if (mException instanceof HttpResultException) {
+ Notify.create(activity, activity.getString(R.string.linked_error_http,
+ ((HttpResultException) mException).mResponse),
+ Style.ERROR).show();
+ } else if (mException instanceof IOException) {
+ Notify.create(activity, R.string.linked_error_network, Style.ERROR).show();
+ }
+
+ return;
+ }
+
+ GithubResource resource;
+
+ try {
+ String gistId = result.getString("id");
+ JSONObject owner = result.getJSONObject("owner");
+ String gistLogin = owner.getString("login");
+
+ URI uri = URI.create("https://gist.github.com/" + gistLogin + "/" + gistId);
+ resource = GithubResource.create(uri);
+ } catch (JSONException e) {
+ setState(State.POST_ERROR);
+ return;
+ }
+
+ View linkedItem = mButtonContainer.getChildAt(2);
+ if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
+ linkedItem.setTransitionName(resource.toUri().toString());
+ }
+
+ // we only need authorization for this one operation, drop it afterwards
+ revokeToken(accessToken);
+
+ step3EditKey(resource);
+ }
+
+ }.execute();
+
+ }
+
+ private void revokeToken(final String token) {
+
+ new AsyncTask<Void,Void,Void>() {
+ @Override
+ protected Void doInBackground(Void... dummy) {
+ try {
+ HttpsURLConnection nection = (HttpsURLConnection) new URL(
+ "https://api.github.com/applications/" + BuildConfig.GITHUB_CLIENT_ID + "/tokens/" + token)
+ .openConnection();
+ nection.setRequestMethod("DELETE");
+ String encoded = Base64.encodeToString(
+ (BuildConfig.GITHUB_CLIENT_ID + ":" + BuildConfig.GITHUB_CLIENT_SECRET).getBytes(), Base64.DEFAULT);
+ nection.setRequestProperty("Authorization", "Basic " + encoded);
+ nection.connect();
+ } catch (IOException e) {
+ // nvm
+ }
+ return null;
+ }
+ }.execute();
+
+ }
+
+ private void step3EditKey(final GithubResource resource) {
+
+ // set item data while we're there
+ {
+ Context context = getActivity();
+ mLinkedIdTitle.setText(resource.getDisplayTitle(context));
+ mLinkedIdComment.setText(resource.getDisplayComment(context));
+ }
+
+ setState(State.LID_PROCESS);
+
+ new Handler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+
+ WrappedUserAttribute ua = LinkedAttribute.fromResource(resource).toUserAttribute();
+ mSaveKeyringParcel = new SaveKeyringParcel(mMasterKeyId, mFingerprint);
+ mSaveKeyringParcel.mAddUserAttribute.add(ua);
+
+ cryptoOperation();
+
+ }
+ }, 250);
+
+ }
+
+ @Nullable
+ @Override
+ public SaveKeyringParcel createOperationInput() {
+ // if this is null, the cryptoOperation silently aborts - which is what we want in that case
+ return mSaveKeyringParcel;
+ }
+
+ @Override
+ public void onCryptoOperationSuccess(EditKeyResult result) {
+
+ setState(State.DONE);
+
+ mButtonContainer.getInAnimation().setDuration(750);
+ mButtonContainer.setDisplayedChild(2);
+
+ new Handler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ FragmentActivity activity = getActivity();
+ Intent intent = new Intent(activity, ViewKeyActivity.class);
+ intent.setData(KeyRings.buildGenericKeyRingUri(mMasterKeyId));
+ // intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ intent.putExtra(ViewKeyActivity.EXTRA_LINKED_TRANSITION, true);
+ View linkedItem = mButtonContainer.getChildAt(2);
+
+ Bundle options = ActivityOptionsCompat.makeSceneTransitionAnimation(
+ activity, linkedItem, linkedItem.getTransitionName()).toBundle();
+ activity.startActivity(intent, options);
+ mFinishOnStop = true;
+ } else {
+ activity.startActivity(intent);
+ activity.finish();
+ }
+ }
+ }, 1000);
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+
+ // cookies are automatically saved, we don't want that
+ CookieManager cookieManager = CookieManager.getInstance();
+ String cookie = cookieManager.getCookie("https://github.com/");
+ outState.putString(ARG_GITHUB_COOKIE, cookie);
+ }
+
+ @Override
+ public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+
+ if (savedInstanceState != null) {
+ String cookie = savedInstanceState.getString(ARG_GITHUB_COOKIE);
+ CookieManager cookieManager = CookieManager.getInstance();
+ cookieManager.setCookie("https://github.com/", cookie);
+ }
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ try {
+ // cookies are automatically saved, we don't want that
+ CookieManager cookieManager = CookieManager.getInstance();
+ // noinspection deprecation (replacement is api lvl 21)
+ cookieManager.removeAllCookie();
+ } catch (Exception e) {
+ // no biggie if this fails
+ }
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+
+ if (mFinishOnStop) {
+ Activity activity = getActivity();
+ activity.setResult(Activity.RESULT_OK);
+ activity.finish();
+ }
+ }
+
+ @Override
+ public void onCryptoOperationError(EditKeyResult result) {
+ result.createNotify(getActivity()).show(this);
+ setState(State.LID_ERROR);
+ }
+
+ @Override
+ public void onCryptoOperationCancelled() {
+ mRetryButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ v.setOnClickListener(null);
+ mButtonContainer.setDisplayedChild(1);
+ setState(State.LID_PROCESS);
+ cryptoOperation();
+ }
+ });
+ mButtonContainer.setDisplayedChild(3);
+ setState(State.LID_ERROR);
+ }
+
+ private String mOAuthCode, mOAuthState;
+
+ public void oAuthRequest(String hostAndPath, String clientId, String scope) {
+
+ Activity activity = getActivity();
+ if (activity == null) {
+ return;
+ }
+
+ byte[] buf = new byte[16];
+ new Random().nextBytes(buf);
+ mOAuthState = new String(Hex.encode(buf));
+ mOAuthCode = null;
+
+ final Dialog auth_dialog = new Dialog(activity);
+ auth_dialog.setContentView(R.layout.oauth_webview);
+ WebView web = (WebView) auth_dialog.findViewById(R.id.web_view);
+ web.getSettings().setSaveFormData(false);
+ web.getSettings().setUserAgentString("OpenKeychain " + BuildConfig.VERSION_NAME);
+ web.setWebViewClient(new WebViewClient() {
+
+ @Override
+ public boolean shouldOverrideUrlLoading(WebView view, String url) {
+ Uri uri = Uri.parse(url);
+ if ("oauth-openkeychain".equals(uri.getScheme())) {
+
+ if (mOAuthCode != null) {
+ return true;
+ }
+
+ if (uri.getQueryParameter("error") != null) {
+ Log.i(Constants.TAG, "got oauth error: " + uri.getQueryParameter("error"));
+ auth_dialog.dismiss();
+ return true;
+ }
+
+ // check if mOAuthState == queryParam[state]
+ mOAuthCode = uri.getQueryParameter("code");
+
+ auth_dialog.dismiss();
+ return true;
+ }
+ // don't surf away from github!
+ if (!"github.com".equals(uri.getHost())) {
+ auth_dialog.dismiss();
+ return true;
+ }
+ return false;
+ }
+
+ });
+
+ auth_dialog.setTitle(R.string.linked_webview_title_github);
+ auth_dialog.setCancelable(true);
+ auth_dialog.setOnDismissListener(new OnDismissListener() {
+ @Override
+ public void onDismiss(DialogInterface dialog) {
+ step1GetOAuthToken();
+ }
+ });
+ auth_dialog.show();
+
+ web.loadUrl("https://" + hostAndPath +
+ "?client_id=" + clientId +
+ "&scope=" + scope +
+ "&redirect_uri=oauth-openkeychain://linked/" +
+ "&state=" + mOAuthState);
+
+ }
+
+ public void setState(State state) {
+ switch (state) {
+ case IDLE:
+ mStatus1.setDisplayedChild(Status.IDLE);
+ mStatus2.setDisplayedChild(Status.IDLE);
+ mStatus3.setDisplayedChild(Status.IDLE);
+ break;
+ case AUTH_PROCESS:
+ mStatus1.setDisplayedChild(Status.PROGRESS);
+ mStatus2.setDisplayedChild(Status.IDLE);
+ mStatus3.setDisplayedChild(Status.IDLE);
+ break;
+ case AUTH_ERROR:
+ mStatus1.setDisplayedChild(Status.ERROR);
+ mStatus2.setDisplayedChild(Status.IDLE);
+ mStatus3.setDisplayedChild(Status.IDLE);
+ break;
+ case POST_PROCESS:
+ mStatus1.setDisplayedChild(Status.OK);
+ mStatus2.setDisplayedChild(Status.PROGRESS);
+ mStatus3.setDisplayedChild(Status.IDLE);
+ break;
+ case POST_ERROR:
+ mStatus1.setDisplayedChild(Status.OK);
+ mStatus2.setDisplayedChild(Status.ERROR);
+ mStatus3.setDisplayedChild(Status.IDLE);
+ break;
+ case LID_PROCESS:
+ mStatus1.setDisplayedChild(Status.OK);
+ mStatus2.setDisplayedChild(Status.OK);
+ mStatus3.setDisplayedChild(Status.PROGRESS);
+ break;
+ case LID_ERROR:
+ mStatus1.setDisplayedChild(Status.OK);
+ mStatus2.setDisplayedChild(Status.OK);
+ mStatus3.setDisplayedChild(Status.ERROR);
+ break;
+ case DONE:
+ mStatus1.setDisplayedChild(Status.OK);
+ mStatus2.setDisplayedChild(Status.OK);
+ mStatus3.setDisplayedChild(Status.OK);
+ }
+ }
+
+ private static JSONObject jsonHttpRequest(String url, JSONObject params, String accessToken)
+ throws IOException, HttpResultException {
+
+ HttpsURLConnection nection = (HttpsURLConnection) new URL(url).openConnection();
+ nection.setDoInput(true);
+ nection.setDoOutput(true);
+ nection.setConnectTimeout(2000);
+ nection.setReadTimeout(1000);
+ nection.setRequestProperty("Content-Type", "application/json");
+ nection.setRequestProperty("Accept", "application/json");
+ nection.setRequestProperty("User-Agent", "OpenKeychain " + BuildConfig.VERSION_NAME);
+ if (accessToken != null) {
+ nection.setRequestProperty("Authorization", "token " + accessToken);
+ }
+
+ OutputStream os = nection.getOutputStream();
+ BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(os, "UTF-8"));
+ writer.write(params.toString());
+ writer.flush();
+ writer.close();
+ os.close();
+
+ try {
+
+ nection.connect();
+
+ int code = nection.getResponseCode();
+ if (code != HttpsURLConnection.HTTP_CREATED && code != HttpsURLConnection.HTTP_OK) {
+ throw new HttpResultException(nection.getResponseCode(), nection.getResponseMessage());
+ }
+
+ InputStream in = new BufferedInputStream(nection.getInputStream());
+ BufferedReader reader = new BufferedReader(new InputStreamReader(in));
+ StringBuilder response = new StringBuilder();
+ while (true) {
+ String line = reader.readLine();
+ if (line == null) {
+ break;
+ }
+ response.append(line);
+ }
+
+ try {
+ return new JSONObject(response.toString());
+ } catch (JSONException e) {
+ throw new IOException(e);
+ }
+
+ } finally {
+ nection.disconnect();
+ }
+
+ }
+
+ static class HttpResultException extends Exception {
+ final int mCode;
+ final String mResponse;
+
+ HttpResultException(int code, String response) {
+ mCode = code;
+ mResponse = response;
+ }
+
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateHttpsStep1Fragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateHttpsStep1Fragment.java
new file mode 100644
index 000000000..8a05c35db
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateHttpsStep1Fragment.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui.linked;
+
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.util.Patterns;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.EditText;
+
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.linked.resources.GenericHttpsResource;
+
+public class LinkedIdCreateHttpsStep1Fragment extends Fragment {
+
+ LinkedIdWizard mLinkedIdWizard;
+
+ EditText mEditUri;
+
+ public static LinkedIdCreateHttpsStep1Fragment newInstance() {
+ LinkedIdCreateHttpsStep1Fragment frag = new LinkedIdCreateHttpsStep1Fragment();
+
+ Bundle args = new Bundle();
+ frag.setArguments(args);
+
+ return frag;
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ mLinkedIdWizard = (LinkedIdWizard) getActivity();
+
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ final View view = inflater.inflate(R.layout.linked_create_https_fragment_step1, container, false);
+
+ view.findViewById(R.id.next_button).setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+
+ String uri = "https://" + mEditUri.getText();
+
+ if (!checkUri(uri)) {
+ return;
+ }
+
+ String proofText = GenericHttpsResource.generateText(getActivity(),
+ mLinkedIdWizard.mFingerprint);
+
+ LinkedIdCreateHttpsStep2Fragment frag =
+ LinkedIdCreateHttpsStep2Fragment.newInstance(uri, proofText);
+
+ mLinkedIdWizard.loadFragment(null, frag, LinkedIdWizard.FRAG_ACTION_TO_RIGHT);
+
+ }
+ });
+
+ view.findViewById(R.id.back_button).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mLinkedIdWizard.loadFragment(null, null, LinkedIdWizard.FRAG_ACTION_TO_LEFT);
+ }
+ });
+
+ mEditUri = (EditText) view.findViewById(R.id.linked_create_https_uri);
+
+ mEditUri.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) {
+ }
+
+ @Override
+ public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) {
+ }
+
+ @Override
+ public void afterTextChanged(Editable editable) {
+ String uri = "https://" + editable;
+ if (uri.length() > 0) {
+ if (checkUri(uri)) {
+ mEditUri.setCompoundDrawablesWithIntrinsicBounds(0, 0,
+ R.drawable.ic_stat_retyped_ok, 0);
+ } else {
+ mEditUri.setCompoundDrawablesWithIntrinsicBounds(0, 0,
+ R.drawable.ic_stat_retyped_bad, 0);
+ }
+ } else {
+ // remove drawable if email is empty
+ mEditUri.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
+ }
+ }
+ });
+
+ // mEditUri.setText("mugenguild.com/pgpkey.txt");
+
+ return view;
+ }
+
+ private static boolean checkUri(String uri) {
+ return Patterns.WEB_URL.matcher(uri).matches();
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateHttpsStep2Fragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateHttpsStep2Fragment.java
new file mode 100644
index 000000000..22a201ba3
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateHttpsStep2Fragment.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui.linked;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Environment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.EditText;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
+import org.sufficientlysecure.keychain.linked.resources.GenericHttpsResource;
+import org.sufficientlysecure.keychain.ui.util.Notify;
+import org.sufficientlysecure.keychain.ui.util.Notify.Style;
+import org.sufficientlysecure.keychain.util.FileHelper;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.PrintWriter;
+import java.net.URI;
+import java.net.URISyntaxException;
+
+public class LinkedIdCreateHttpsStep2Fragment extends LinkedIdCreateFinalFragment {
+
+ private static final int REQUEST_CODE_OUTPUT = 0x00007007;
+
+ public static final String ARG_URI = "uri", ARG_TEXT = "text";
+
+ EditText mEditUri;
+
+ URI mResourceUri;
+ String mResourceString;
+
+ public static LinkedIdCreateHttpsStep2Fragment newInstance
+ (String uri, String proofText) {
+
+ LinkedIdCreateHttpsStep2Fragment frag = new LinkedIdCreateHttpsStep2Fragment();
+
+ Bundle args = new Bundle();
+ args.putString(ARG_URI, uri);
+ args.putString(ARG_TEXT, proofText);
+ frag.setArguments(args);
+
+ return frag;
+ }
+
+ @Override
+ GenericHttpsResource getResource(OperationLog log) {
+ return GenericHttpsResource.createNew(mResourceUri);
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ try {
+ mResourceUri = new URI(getArguments().getString(ARG_URI));
+ } catch (URISyntaxException e) {
+ e.printStackTrace();
+ getActivity().finish();
+ }
+
+ mResourceString = getArguments().getString(ARG_TEXT);
+
+ }
+
+ protected View newView(LayoutInflater inflater,
+ ViewGroup container, Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.linked_create_https_fragment_step2, container, false);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = super.onCreateView(inflater, container, savedInstanceState);
+
+ if (view != null) {
+
+ view.findViewById(R.id.button_send).setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ proofSend();
+ }
+ });
+
+ view.findViewById(R.id.button_save).setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ proofSave();
+ }
+ });
+
+ mEditUri = (EditText) view.findViewById(R.id.linked_create_https_uri);
+ mEditUri.setText(mResourceUri.toString());
+ }
+
+ return view;
+ }
+
+ private void proofSend () {
+ Intent sendIntent = new Intent();
+ sendIntent.setAction(Intent.ACTION_SEND);
+ sendIntent.putExtra(Intent.EXTRA_TEXT, mResourceString);
+ sendIntent.setType("text/plain");
+ startActivity(sendIntent);
+ }
+
+ private void proofSave () {
+ String state = Environment.getExternalStorageState();
+ if (!Environment.MEDIA_MOUNTED.equals(state)) {
+ Notify.create(getActivity(), "External storage not available!", Style.ERROR);
+ return;
+ }
+
+ String targetName = "pgpkey.txt";
+
+ FileHelper.saveDocument(this,
+ targetName, Uri.fromFile(new File(Constants.Path.APP_DIR, targetName)),
+ "text/plain", R.string.title_decrypt_to_file, R.string.specify_file_to_decrypt_to,
+ REQUEST_CODE_OUTPUT);
+ }
+
+ private void saveFile(Uri uri) {
+ try {
+ PrintWriter out =
+ new PrintWriter(getActivity().getContentResolver().openOutputStream(uri));
+ out.print(mResourceString);
+ if (out.checkError()) {
+ Notify.create(getActivity(), "Error writing file!", Style.ERROR).show();
+ }
+ } catch (FileNotFoundException e) {
+ Notify.create(getActivity(), "File could not be opened for writing!", Style.ERROR).show();
+ }
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ switch (requestCode) {
+ // For saving a file
+ case REQUEST_CODE_OUTPUT:
+ if (data == null) {
+ return;
+ }
+ Uri uri = data.getData();
+ saveFile(uri);
+ break;
+ default:
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateTwitterStep1Fragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateTwitterStep1Fragment.java
new file mode 100644
index 000000000..c25f775b0
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateTwitterStep1Fragment.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui.linked;
+
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.EditText;
+
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.ui.util.Notify;
+
+public class LinkedIdCreateTwitterStep1Fragment extends Fragment {
+
+ LinkedIdWizard mLinkedIdWizard;
+
+ EditText mEditHandle;
+
+ public static LinkedIdCreateTwitterStep1Fragment newInstance() {
+ LinkedIdCreateTwitterStep1Fragment frag = new LinkedIdCreateTwitterStep1Fragment();
+
+ Bundle args = new Bundle();
+ frag.setArguments(args);
+
+ return frag;
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ mLinkedIdWizard = (LinkedIdWizard) getActivity();
+
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ final View view = inflater.inflate(R.layout.linked_create_twitter_fragment_step1, container, false);
+
+ view.findViewById(R.id.next_button).setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+
+ final String handle = mEditHandle.getText().toString();
+
+ if ("".equals(handle)) {
+ mEditHandle.setError("Please input a Twitter handle!");
+ return;
+ }
+
+ new AsyncTask<Void,Void,Boolean>() {
+
+ @Override
+ protected Boolean doInBackground(Void... params) {
+ return true;
+ // return checkHandle(handle);
+ }
+
+ @Override
+ protected void onPostExecute(Boolean result) {
+ super.onPostExecute(result);
+
+ if (result == null) {
+ Notify.create(getActivity(),
+ "Connection error while checking username!",
+ Notify.Style.ERROR).show(LinkedIdCreateTwitterStep1Fragment.this);
+ return;
+ }
+
+ if (!result) {
+ Notify.create(getActivity(),
+ "This handle does not exist on Twitter!",
+ Notify.Style.ERROR).show(LinkedIdCreateTwitterStep1Fragment.this);
+ return;
+ }
+
+ LinkedIdCreateTwitterStep2Fragment frag =
+ LinkedIdCreateTwitterStep2Fragment.newInstance(handle);
+
+ mLinkedIdWizard.loadFragment(null, frag, LinkedIdWizard.FRAG_ACTION_TO_RIGHT);
+ }
+ }.execute();
+
+ }
+ });
+
+ view.findViewById(R.id.back_button).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mLinkedIdWizard.loadFragment(null, null, LinkedIdWizard.FRAG_ACTION_TO_LEFT);
+ }
+ });
+
+ mEditHandle = (EditText) view.findViewById(R.id.linked_create_twitter_handle);
+
+ return view;
+ }
+
+ /* not used at this point, too many problems
+ private static Boolean checkHandle(String handle) {
+ try {
+ HttpURLConnection nection =
+ (HttpURLConnection) new URL("https://twitter.com/" + handle).openConnection();
+ nection.setRequestMethod("HEAD");
+ nection.setRequestProperty("User-Agent", "OpenKeychain");
+ return nection.getResponseCode() == 200;
+ } catch (IOException e) {
+ return null;
+ }
+ }
+ */
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateTwitterStep2Fragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateTwitterStep2Fragment.java
new file mode 100644
index 000000000..362798bc8
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateTwitterStep2Fragment.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui.linked;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.text.Html;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
+import org.sufficientlysecure.keychain.linked.LinkedTokenResource;
+import org.sufficientlysecure.keychain.linked.resources.TwitterResource;
+
+public class LinkedIdCreateTwitterStep2Fragment extends LinkedIdCreateFinalFragment {
+
+ public static final String ARG_HANDLE = "handle";
+
+ String mResourceHandle;
+ String mResourceString;
+
+ public static LinkedIdCreateTwitterStep2Fragment newInstance
+ (String handle) {
+
+ LinkedIdCreateTwitterStep2Fragment frag = new LinkedIdCreateTwitterStep2Fragment();
+
+ Bundle args = new Bundle();
+ args.putString(ARG_HANDLE, handle);
+ frag.setArguments(args);
+
+ return frag;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ mResourceString =
+ TwitterResource.generate(mLinkedIdWizard.mFingerprint);
+
+ mResourceHandle = getArguments().getString(ARG_HANDLE);
+
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = super.onCreateView(inflater, container, savedInstanceState);
+
+ if (view != null) {
+ view.findViewById(R.id.button_send).setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ proofSend();
+ }
+ });
+
+ view.findViewById(R.id.button_share).setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ proofShare();
+ }
+ });
+
+ ((TextView) view.findViewById(R.id.linked_tweet_published)).setText(
+ Html.fromHtml(getString(R.string.linked_create_twitter_2_3, mResourceHandle))
+ );
+ }
+
+ return view;
+ }
+
+ @Override
+ LinkedTokenResource getResource(OperationLog log) {
+ return TwitterResource.searchInTwitterStream(getActivity(),
+ mResourceHandle, mResourceString, log);
+ }
+
+ @Override
+ protected View newView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.linked_create_twitter_fragment_step2, container, false);
+ }
+
+ private void proofShare() {
+ Intent sendIntent = new Intent();
+ sendIntent.setAction(Intent.ACTION_SEND);
+ sendIntent.putExtra(Intent.EXTRA_TEXT, mResourceString);
+ sendIntent.setType("text/plain");
+ startActivity(sendIntent);
+ }
+
+ private void proofSend() {
+
+ Uri.Builder builder = Uri.parse("https://twitter.com/intent/tweet").buildUpon();
+ builder.appendQueryParameter("text", mResourceString);
+ Uri uri = builder.build();
+
+ Intent intent = new Intent(Intent.ACTION_VIEW, uri);
+ getActivity().startActivity(intent);
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdSelectFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdSelectFragment.java
new file mode 100644
index 000000000..a17a97013
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdSelectFragment.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui.linked;
+
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import org.sufficientlysecure.keychain.R;
+
+public class LinkedIdSelectFragment extends Fragment {
+
+ LinkedIdWizard mLinkedIdWizard;
+
+ /**
+ * Creates new instance of this fragment
+ */
+ public static LinkedIdSelectFragment newInstance() {
+ LinkedIdSelectFragment frag = new LinkedIdSelectFragment();
+
+ Bundle args = new Bundle();
+ frag.setArguments(args);
+
+ return frag;
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.linked_select_fragment, container, false);
+
+ view.findViewById(R.id.linked_create_https_button)
+ .setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ LinkedIdCreateHttpsStep1Fragment frag =
+ LinkedIdCreateHttpsStep1Fragment.newInstance();
+
+ mLinkedIdWizard.loadFragment(null, frag, LinkedIdWizard.FRAG_ACTION_TO_RIGHT);
+ }
+ });
+
+ /*
+ view.findViewById(R.id.linked_create_dns_button)
+ .setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ LinkedIdCreateDnsStep1Fragment frag =
+ LinkedIdCreateDnsStep1Fragment.newInstance();
+
+ mLinkedIdWizard.loadFragment(null, frag, LinkedIdWizard.FRAG_ACTION_TO_RIGHT);
+ }
+ });
+ */
+
+ view.findViewById(R.id.linked_create_twitter_button)
+ .setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ LinkedIdCreateTwitterStep1Fragment frag =
+ LinkedIdCreateTwitterStep1Fragment.newInstance();
+
+ mLinkedIdWizard.loadFragment(null, frag, LinkedIdWizard.FRAG_ACTION_TO_RIGHT);
+ }
+ });
+
+ view.findViewById(R.id.linked_create_github_button)
+ .setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ LinkedIdCreateGithubFragment frag =
+ LinkedIdCreateGithubFragment.newInstance();
+
+ mLinkedIdWizard.loadFragment(null, frag, LinkedIdWizard.FRAG_ACTION_TO_RIGHT);
+ }
+ });
+
+
+ return view;
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ mLinkedIdWizard = (LinkedIdWizard) getActivity();
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdViewFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdViewFragment.java
new file mode 100644
index 000000000..5630932b4
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdViewFragment.java
@@ -0,0 +1,560 @@
+package org.sufficientlysecure.keychain.ui.linked;
+
+import java.io.IOException;
+import java.util.Collections;
+
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.graphics.PorterDuff;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Parcelable;
+import android.support.annotation.Nullable;
+import android.support.v4.app.FragmentManager;
+import android.support.v4.app.FragmentManager.OnBackStackChangedListener;
+import android.support.v4.app.LoaderManager;
+import android.support.v4.content.CursorLoader;
+import android.support.v4.content.Loader;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextSwitcher;
+import android.widget.TextView;
+import android.widget.ViewAnimator;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.Constants.key;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.operations.results.LinkedVerifyResult;
+import org.sufficientlysecure.keychain.linked.LinkedTokenResource;
+import org.sufficientlysecure.keychain.linked.LinkedAttribute;
+import org.sufficientlysecure.keychain.linked.LinkedResource;
+import org.sufficientlysecure.keychain.linked.UriAttribute;
+import org.sufficientlysecure.keychain.operations.results.OperationResult;
+import org.sufficientlysecure.keychain.provider.KeychainContract.Certs;
+import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets;
+import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables;
+import org.sufficientlysecure.keychain.service.CertifyActionsParcel;
+import org.sufficientlysecure.keychain.service.CertifyActionsParcel.CertifyAction;
+import org.sufficientlysecure.keychain.ui.adapter.LinkedIdsAdapter;
+import org.sufficientlysecure.keychain.ui.adapter.UserIdsAdapter;
+import org.sufficientlysecure.keychain.ui.base.CryptoOperationFragment;
+import org.sufficientlysecure.keychain.ui.linked.LinkedIdViewFragment.ViewHolder.VerifyState;
+import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
+import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils.State;
+import org.sufficientlysecure.keychain.ui.util.Notify;
+import org.sufficientlysecure.keychain.ui.util.Notify.Style;
+import org.sufficientlysecure.keychain.ui.util.SubtleAttentionSeeker;
+import org.sufficientlysecure.keychain.ui.widget.CertListWidget;
+import org.sufficientlysecure.keychain.ui.widget.CertifyKeySpinner;
+import org.sufficientlysecure.keychain.util.Log;
+
+public class LinkedIdViewFragment extends CryptoOperationFragment implements
+ LoaderManager.LoaderCallbacks<Cursor>, OnBackStackChangedListener {
+
+ private static final String ARG_DATA_URI = "data_uri";
+ private static final String ARG_LID_RANK = "rank";
+ private static final String ARG_IS_SECRET = "verified";
+ private static final String ARG_FINGERPRINT = "fingerprint";
+ private static final int LOADER_ID_LINKED_ID = 1;
+
+ private UriAttribute mLinkedId;
+ private LinkedTokenResource mLinkedResource;
+ private boolean mIsSecret;
+
+ private Context mContext;
+ private byte[] mFingerprint;
+
+ private AsyncTask mInProgress;
+
+ private Uri mDataUri;
+ private ViewHolder mViewHolder;
+ private int mLidRank;
+ private OnIdentityLoadedListener mIdLoadedListener;
+ private long mCertifyKeyId;
+
+ public static LinkedIdViewFragment newInstance(Uri dataUri, int rank,
+ boolean isSecret, byte[] fingerprint) throws IOException {
+ LinkedIdViewFragment frag = new LinkedIdViewFragment();
+
+ Bundle args = new Bundle();
+ args.putParcelable(ARG_DATA_URI, dataUri);
+ args.putInt(ARG_LID_RANK, rank);
+ args.putBoolean(ARG_IS_SECRET, isSecret);
+ args.putByteArray(ARG_FINGERPRINT, fingerprint);
+ frag.setArguments(args);
+
+ return frag;
+ }
+
+ public LinkedIdViewFragment() {
+ // IMPORTANT: the id must be unique in the ViewKeyActivity CryptoOperationHelper id namespace!
+ // no initial progress message -> we handle progress ourselves!
+ super(5, null);
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ Bundle args = getArguments();
+ mDataUri = args.getParcelable(ARG_DATA_URI);
+ mLidRank = args.getInt(ARG_LID_RANK);
+
+ mIsSecret = args.getBoolean(ARG_IS_SECRET);
+ mFingerprint = args.getByteArray(ARG_FINGERPRINT);
+
+ mContext = getActivity();
+
+ getLoaderManager().initLoader(LOADER_ID_LINKED_ID, null, this);
+
+ }
+
+ @Override
+ public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+ switch (id) {
+ case LOADER_ID_LINKED_ID:
+ return new CursorLoader(getActivity(), mDataUri,
+ UserIdsAdapter.USER_PACKETS_PROJECTION,
+ Tables.USER_PACKETS + "." + UserPackets.RANK
+ + " = " + Integer.toString(mLidRank), null, null);
+ default:
+ return null;
+ }
+ }
+
+ @Override
+ public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
+ switch (loader.getId()) {
+ case LOADER_ID_LINKED_ID:
+
+ // Nothing to load means break if we are *expected* to load
+ if (!cursor.moveToFirst()) {
+ if (mIdLoadedListener != null) {
+ Notify.create(getActivity(), "Error loading identity!",
+ Notify.LENGTH_LONG, Style.ERROR).show();
+ finishFragment();
+ }
+ // Or just ignore, this is probably some intermediate state during certify
+ break;
+ }
+
+ try {
+ int certStatus = cursor.getInt(UserIdsAdapter.INDEX_VERIFIED);
+
+ byte[] data = cursor.getBlob(UserIdsAdapter.INDEX_ATTRIBUTE_DATA);
+ UriAttribute linkedId = LinkedAttribute.fromAttributeData(data);
+
+ loadIdentity(linkedId, certStatus);
+
+ if (mIdLoadedListener != null) {
+ mIdLoadedListener.onIdentityLoaded();
+ mIdLoadedListener = null;
+ }
+
+ } catch (IOException e) {
+ Log.e(Constants.TAG, "error parsing identity", e);
+ Notify.create(getActivity(), "Error parsing identity!",
+ Notify.LENGTH_LONG, Style.ERROR).show();
+ finishFragment();
+ }
+
+ break;
+ }
+ }
+
+ public void finishFragment() {
+ new Handler().post(new Runnable() {
+ @Override
+ public void run() {
+ FragmentManager manager = getFragmentManager();
+ manager.removeOnBackStackChangedListener(LinkedIdViewFragment.this);
+ manager.popBackStack("linked_id", FragmentManager.POP_BACK_STACK_INCLUSIVE);
+ }
+ });
+ }
+
+ public interface OnIdentityLoadedListener {
+ void onIdentityLoaded();
+ }
+
+ public void setOnIdentityLoadedListener(OnIdentityLoadedListener listener) {
+ mIdLoadedListener = listener;
+ }
+
+ private void loadIdentity(UriAttribute linkedId, int certStatus) {
+ mLinkedId = linkedId;
+
+ if (mLinkedId instanceof LinkedAttribute) {
+ LinkedResource res = ((LinkedAttribute) mLinkedId).mResource;
+ mLinkedResource = (LinkedTokenResource) res;
+ }
+
+ if (!mIsSecret) {
+ switch (certStatus) {
+ case Certs.VERIFIED_SECRET:
+ KeyFormattingUtils.setStatusImage(mContext, mViewHolder.mLinkedIdHolder.vVerified,
+ null, State.VERIFIED, KeyFormattingUtils.DEFAULT_COLOR);
+ break;
+ case Certs.VERIFIED_SELF:
+ KeyFormattingUtils.setStatusImage(mContext, mViewHolder.mLinkedIdHolder.vVerified,
+ null, State.UNVERIFIED, KeyFormattingUtils.DEFAULT_COLOR);
+ break;
+ default:
+ KeyFormattingUtils.setStatusImage(mContext, mViewHolder.mLinkedIdHolder.vVerified,
+ null, State.INVALID, KeyFormattingUtils.DEFAULT_COLOR);
+ break;
+ }
+ } else {
+ mViewHolder.mLinkedIdHolder.vVerified.setImageResource(R.drawable.octo_link_24dp);
+ }
+
+ mViewHolder.mLinkedIdHolder.setData(mContext, mLinkedId);
+
+ setShowVerifying(false);
+
+ // no resource, nothing further we can do…
+ if (mLinkedResource == null) {
+ mViewHolder.vButtonView.setVisibility(View.GONE);
+ mViewHolder.vButtonVerify.setVisibility(View.GONE);
+ return;
+ }
+
+ if (mLinkedResource.isViewable()) {
+ mViewHolder.vButtonView.setVisibility(View.VISIBLE);
+ mViewHolder.vButtonView.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Intent intent = mLinkedResource.getViewIntent();
+ if (intent == null) {
+ return;
+ }
+ getActivity().startActivity(intent);
+ }
+ });
+ } else {
+ mViewHolder.vButtonView.setVisibility(View.GONE);
+ }
+
+ }
+
+ @Override
+ public void onLoaderReset(Loader<Cursor> loader) {
+
+ }
+
+ static class ViewHolder {
+ private final View vButtonView;
+ private final ViewAnimator vVerifyingContainer;
+ private final ViewAnimator vItemCertified;
+ private final View vKeySpinnerContainer;
+ LinkedIdsAdapter.ViewHolder mLinkedIdHolder;
+
+ private ViewAnimator vButtonSwitcher;
+ private CertListWidget vLinkedCerts;
+ private CertifyKeySpinner vKeySpinner;
+ private final View vButtonVerify;
+ private final View vButtonRetry;
+ private final View vButtonConfirm;
+
+ private final ViewAnimator vProgress;
+ private final TextSwitcher vText;
+
+ ViewHolder(View root) {
+ vLinkedCerts = (CertListWidget) root.findViewById(R.id.linked_id_certs);
+ vKeySpinner = (CertifyKeySpinner) root.findViewById(R.id.cert_key_spinner);
+ vKeySpinnerContainer = root.findViewById(R.id.cert_key_spincontainer);
+ vButtonSwitcher = (ViewAnimator) root.findViewById(R.id.button_animator);
+
+ mLinkedIdHolder = new LinkedIdsAdapter.ViewHolder(root);
+
+ vButtonVerify = root.findViewById(R.id.button_verify);
+ vButtonRetry = root.findViewById(R.id.button_retry);
+ vButtonConfirm = root.findViewById(R.id.button_confirm);
+ vButtonView = root.findViewById(R.id.button_view);
+
+ vVerifyingContainer = (ViewAnimator) root.findViewById(R.id.linked_verify_container);
+ vItemCertified = (ViewAnimator) root.findViewById(R.id.linked_id_certified);
+
+ vProgress = (ViewAnimator) root.findViewById(R.id.linked_cert_progress);
+ vText = (TextSwitcher) root.findViewById(R.id.linked_cert_text);
+ }
+
+ enum VerifyState {
+ VERIFYING, VERIFY_OK, VERIFY_ERROR, CERTIFYING
+ }
+
+ void setVerifyingState(Context context, VerifyState state, boolean isSecret) {
+ switch (state) {
+ case VERIFYING:
+ vProgress.setDisplayedChild(0);
+ vText.setText(context.getString(R.string.linked_text_verifying));
+ vKeySpinnerContainer.setVisibility(View.GONE);
+ break;
+
+ case VERIFY_OK:
+ vProgress.setDisplayedChild(1);
+ if (!isSecret) {
+ showButton(2);
+ if (!vKeySpinner.isSingleEntry()) {
+ vKeySpinnerContainer.setVisibility(View.VISIBLE);
+ }
+ } else {
+ showButton(1);
+ vKeySpinnerContainer.setVisibility(View.GONE);
+ }
+ break;
+
+ case VERIFY_ERROR:
+ showButton(1);
+ vProgress.setDisplayedChild(2);
+ vText.setText(context.getString(R.string.linked_text_error));
+ vKeySpinnerContainer.setVisibility(View.GONE);
+ break;
+
+ case CERTIFYING:
+ vProgress.setDisplayedChild(0);
+ vText.setText(context.getString(R.string.linked_text_confirming));
+ vKeySpinnerContainer.setVisibility(View.GONE);
+ break;
+ }
+ }
+
+ void showVerifyingContainer(Context context, boolean show, boolean isSecret) {
+ if (vVerifyingContainer.getDisplayedChild() == (show ? 1 : 0)) {
+ return;
+ }
+
+ vVerifyingContainer.setInAnimation(context, show ? R.anim.fade_in_up : R.anim.fade_in_down);
+ vVerifyingContainer.setOutAnimation(context, show ? R.anim.fade_out_up : R.anim.fade_out_down);
+ vVerifyingContainer.setDisplayedChild(show ? 1 : 0);
+
+ vItemCertified.setInAnimation(context, show ? R.anim.fade_in_up : R.anim.fade_in_down);
+ vItemCertified.setOutAnimation(context, show ? R.anim.fade_out_up : R.anim.fade_out_down);
+ vItemCertified.setDisplayedChild(show || isSecret ? 1 : 0);
+ }
+
+ void showButton(int which) {
+ if (vButtonSwitcher.getDisplayedChild() == which) {
+ return;
+ }
+ vButtonSwitcher.setDisplayedChild(which);
+ }
+
+ }
+
+ private boolean mVerificationState = false;
+ /** Switches between the 'verifying' ui bit and certificate status. This method
+ * must behave correctly in all states, showing or hiding the appropriate views
+ * and cancelling pending operations where necessary.
+ *
+ * This method also handles back button functionality in combination with
+ * onBackStateChanged.
+ */
+ void setShowVerifying(boolean show) {
+ if (!show) {
+ if (mInProgress != null) {
+ mInProgress.cancel(false);
+ mInProgress = null;
+ }
+ getFragmentManager().removeOnBackStackChangedListener(this);
+ new Handler().post(new Runnable() {
+ @Override
+ public void run() {
+ getFragmentManager().popBackStack("verification",
+ FragmentManager.POP_BACK_STACK_INCLUSIVE);
+ }
+ });
+
+ if (!mVerificationState) {
+ return;
+ }
+ mVerificationState = false;
+
+ mViewHolder.showButton(0);
+ mViewHolder.vKeySpinnerContainer.setVisibility(View.GONE);
+ mViewHolder.showVerifyingContainer(mContext, false, mIsSecret);
+ return;
+ }
+
+ if (mVerificationState) {
+ return;
+ }
+ mVerificationState = true;
+
+ FragmentManager manager = getFragmentManager();
+ manager.beginTransaction().addToBackStack("verification").commit();
+ manager.executePendingTransactions();
+ manager.addOnBackStackChangedListener(this);
+ mViewHolder.showVerifyingContainer(mContext, true, mIsSecret);
+
+ }
+
+ @Override
+ public void onBackStackChanged() {
+ setShowVerifying(false);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup superContainer, Bundle savedInstanceState) {
+ View root = inflater.inflate(R.layout.linked_id_view_fragment, null);
+
+ mViewHolder = new ViewHolder(root);
+ root.setTag(mViewHolder);
+
+ ((ImageView) root.findViewById(R.id.status_icon_verified))
+ .setColorFilter(mContext.getResources().getColor(R.color.android_green_light),
+ PorterDuff.Mode.SRC_IN);
+ ((ImageView) root.findViewById(R.id.status_icon_invalid))
+ .setColorFilter(mContext.getResources().getColor(R.color.android_red_light),
+ PorterDuff.Mode.SRC_IN);
+
+ mViewHolder.vButtonVerify.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ verifyResource();
+ }
+ });
+ mViewHolder.vButtonRetry.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ verifyResource();
+ }
+ });
+ mViewHolder.vButtonConfirm.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ initiateCertifying();
+ }
+ });
+
+ {
+ Bundle args = new Bundle();
+ args.putParcelable(CertListWidget.ARG_URI, Certs.buildLinkedIdCertsUri(mDataUri, mLidRank));
+ args.putBoolean(CertListWidget.ARG_IS_SECRET, mIsSecret);
+ getLoaderManager().initLoader(CertListWidget.LOADER_ID_LINKED_CERTS,
+ args, mViewHolder.vLinkedCerts);
+ }
+
+ return root;
+ }
+
+ void verifyResource() {
+
+ // only one at a time (no sync needed, mInProgress is only touched in ui thread)
+ if (mInProgress != null) {
+ return;
+ }
+
+ setShowVerifying(true);
+
+ mViewHolder.vKeySpinnerContainer.setVisibility(View.GONE);
+ mViewHolder.setVerifyingState(mContext, VerifyState.VERIFYING, mIsSecret);
+
+ mInProgress = new AsyncTask<Void,Void,LinkedVerifyResult>() {
+ @Override
+ protected LinkedVerifyResult doInBackground(Void... params) {
+ long timer = System.currentTimeMillis();
+ LinkedVerifyResult result = mLinkedResource.verify(getActivity(), mFingerprint);
+
+ // ux flow: this operation should take at last a second
+ timer = System.currentTimeMillis() -timer;
+ if (timer < 1000) try {
+ Thread.sleep(1000 -timer);
+ } catch (InterruptedException e) {
+ // never mind
+ }
+
+ return result;
+ }
+
+ @Override
+ protected void onPostExecute(LinkedVerifyResult result) {
+ if (isCancelled()) {
+ return;
+ }
+ if (result.success()) {
+ mViewHolder.vText.setText(getString(mLinkedResource.getVerifiedText(mIsSecret)));
+ // hack to preserve bold text
+ ((TextView) mViewHolder.vText.getCurrentView()).setText(
+ mLinkedResource.getVerifiedText(mIsSecret));
+ mViewHolder.setVerifyingState(mContext, VerifyState.VERIFY_OK, mIsSecret);
+ mViewHolder.mLinkedIdHolder.seekAttention();
+ } else {
+ mViewHolder.setVerifyingState(mContext, VerifyState.VERIFY_ERROR, mIsSecret);
+ result.createNotify(getActivity()).show();
+ }
+ mInProgress = null;
+ }
+ }.execute();
+
+ }
+
+ private void initiateCertifying() {
+
+ if (mIsSecret) {
+ return;
+ }
+
+ // get the user's passphrase for this key (if required)
+ mCertifyKeyId = mViewHolder.vKeySpinner.getSelectedKeyId();
+ if (mCertifyKeyId == key.none || mCertifyKeyId == key.symmetric) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ SubtleAttentionSeeker.tintBackground(mViewHolder.vKeySpinnerContainer, 600).start();
+ } else {
+ Notify.create(getActivity(), R.string.select_key_to_certify, Style.ERROR).show();
+ }
+ return;
+ }
+
+ mViewHolder.setVerifyingState(mContext, VerifyState.CERTIFYING, false);
+ cryptoOperation();
+
+ }
+
+ @Override
+ public void onCryptoOperationCancelled() {
+ super.onCryptoOperationCancelled();
+
+ // go back to 'verified ok'
+ setShowVerifying(false);
+
+ }
+
+ @Nullable
+ @Override
+ public Parcelable createOperationInput() {
+ long masterKeyId = KeyFormattingUtils.convertFingerprintToKeyId(mFingerprint);
+ CertifyAction action = new CertifyAction(masterKeyId, null,
+ Collections.singletonList(mLinkedId.toUserAttribute()));
+
+ // fill values for this action
+ CertifyActionsParcel parcel = new CertifyActionsParcel(mCertifyKeyId);
+ parcel.mCertifyActions.addAll(Collections.singletonList(action));
+
+ return parcel;
+ }
+
+ @Override
+ public void onCryptoOperationSuccess(OperationResult result) {
+ result.createNotify(getActivity()).show();
+ // no need to do anything else, we will get a loader refresh!
+ }
+
+ @Override
+ public void onCryptoOperationError(OperationResult result) {
+ result.createNotify(getActivity()).show();
+ }
+
+ @Override
+ public boolean onCryptoSetProgress(String msg, int progress, int max) {
+ return true;
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdWizard.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdWizard.java
new file mode 100644
index 000000000..8c677199d
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdWizard.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui.linked;
+
+
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentTransaction;
+import android.support.v4.app.NavUtils;
+import android.support.v4.app.TaskStackBuilder;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.inputmethod.InputMethodManager;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException;
+import org.sufficientlysecure.keychain.provider.CachedPublicKeyRing;
+import org.sufficientlysecure.keychain.provider.KeychainContract;
+import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
+import org.sufficientlysecure.keychain.provider.ProviderHelper;
+import org.sufficientlysecure.keychain.ui.base.BaseActivity;
+import org.sufficientlysecure.keychain.util.Log;
+
+public class LinkedIdWizard extends BaseActivity {
+
+ public static final int FRAG_ACTION_START = 0;
+ public static final int FRAG_ACTION_TO_RIGHT = 1;
+ public static final int FRAG_ACTION_TO_LEFT = 2;
+
+ long mMasterKeyId;
+ byte[] mFingerprint;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setTitle(getString(R.string.title_linked_id_create));
+
+ try {
+ Uri uri = getIntent().getData();
+ uri = KeychainContract.KeyRings.buildUnifiedKeyRingUri(uri);
+ CachedPublicKeyRing ring = new ProviderHelper(this).getCachedPublicKeyRing(uri);
+ if (!ring.hasAnySecret()) {
+ Log.e(Constants.TAG, "Linked Identities can only be added to secret keys!");
+ finish();
+ return;
+ }
+
+ mMasterKeyId = ring.extractOrGetMasterKeyId();
+ mFingerprint = ring.getFingerprint();
+ } catch (PgpKeyNotFoundException e) {
+ Log.e(Constants.TAG, "Invalid uri given, key does not exist!");
+ finish();
+ return;
+ }
+
+ // pass extras into fragment
+ LinkedIdSelectFragment frag = LinkedIdSelectFragment.newInstance();
+ loadFragment(null, frag, FRAG_ACTION_START);
+ }
+
+ @Override
+ protected void initLayout() {
+ setContentView(R.layout.create_key_activity);
+ }
+
+ public void loadFragment(Bundle savedInstanceState, Fragment fragment, int action) {
+ // However, if we're being restored from a previous state,
+ // then we don't need to do anything and should return or else
+ // we could end up with overlapping fragments.
+ if (savedInstanceState != null) {
+ return;
+ }
+
+ hideKeyboard();
+
+ // Add the fragment to the 'fragment_container' FrameLayout
+ // NOTE: We use commitAllowingStateLoss() to prevent weird crashes!
+ FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
+
+ switch (action) {
+ case FRAG_ACTION_START:
+ transaction.setCustomAnimations(0, 0);
+ transaction.replace(R.id.create_key_fragment_container, fragment)
+ .commitAllowingStateLoss();
+ break;
+ case FRAG_ACTION_TO_LEFT:
+ getSupportFragmentManager().popBackStackImmediate();
+ break;
+ case FRAG_ACTION_TO_RIGHT:
+ transaction.setCustomAnimations(R.anim.frag_slide_in_from_right, R.anim.frag_slide_out_to_left,
+ R.anim.frag_slide_in_from_left, R.anim.frag_slide_out_to_right);
+ transaction.addToBackStack(null);
+ transaction.replace(R.id.create_key_fragment_container, fragment)
+ .commitAllowingStateLoss();
+ break;
+
+ }
+ // do it immediately!
+ getSupportFragmentManager().executePendingTransactions();
+ }
+
+ private void hideKeyboard() {
+ InputMethodManager inputManager = (InputMethodManager)
+ getSystemService(Context.INPUT_METHOD_SERVICE);
+
+ // check if no view has focus
+ View v = getCurrentFocus();
+ if (v == null)
+ return;
+
+ inputManager.hideSoftInputFromWindow(v.getWindowToken(), 0);
+ }
+
+ @Override
+ public void onBackPressed() {
+ if (!getFragmentManager().popBackStackImmediate()) {
+ navigateBack();
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ // Respond to the action bar's Up/Home button
+ case android.R.id.home:
+ navigateBack();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ private void navigateBack() {
+ Intent upIntent = NavUtils.getParentActivityIntent(this);
+ upIntent.setData(KeyRings.buildGenericKeyRingUri(mMasterKeyId));
+ // This activity is NOT part of this app's task, so create a new task
+ // when navigating up, with a synthesized back stack.
+ TaskStackBuilder.create(this)
+ // Add all of this activity's parents to the back stack
+ .addNextIntentWithParentStack(upIntent)
+ // Navigate up to the closest parent
+ .startActivities();
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/ExperimentalWordConfirm.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/ExperimentalWordConfirm.java
new file mode 100644
index 000000000..43ccac24f
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/ExperimentalWordConfirm.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2015 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui.util;
+
+import android.content.Context;
+
+import org.spongycastle.util.Arrays;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.util.Log;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.BitSet;
+
+public class ExperimentalWordConfirm {
+
+ public static String getWords(Context context, byte[] fingerprintBlob) {
+ ArrayList<String> words = new ArrayList<>();
+
+ BufferedReader reader = null;
+ try {
+ reader = new BufferedReader(new InputStreamReader(
+ context.getAssets().open("word_confirm_list.txt"),
+ "UTF-8"
+ ));
+
+ String line = reader.readLine();
+ while (line != null) {
+ words.add(line);
+
+ line = reader.readLine();
+ }
+ } catch (IOException e) {
+ throw new RuntimeException("IOException", e);
+ } finally {
+ if (reader != null) {
+ try {
+ reader.close();
+ } catch (IOException ignored) {
+ }
+ }
+ }
+
+ String fingerprint = "";
+
+ // NOTE: 160 bit SHA-1 truncated to 156 bit
+ byte[] fingerprintBlobTruncated = Arrays.copyOfRange(fingerprintBlob, 0, 156 / 8);
+
+ // TODO: implement key stretching to minimize fp length?
+
+ // BitSet bits = BitSet.valueOf(fingerprintBlob); // min API 19 and little endian!
+ BitSet bits = bitSetToByteArray(fingerprintBlobTruncated);
+ Log.d(Constants.TAG, "bits: " + bits.toString());
+
+ final int CHUNK_SIZE = 13;
+ final int LAST_CHUNK_INDEX = fingerprintBlobTruncated.length * 8 / CHUNK_SIZE; // 12
+ Log.d(Constants.TAG, "LAST_CHUNK_INDEX: " + LAST_CHUNK_INDEX);
+
+ int from = 0;
+ int to = CHUNK_SIZE;
+ for (int i = 0; i < (LAST_CHUNK_INDEX + 1); i++) {
+ Log.d(Constants.TAG, "from: " + from + " to: " + to);
+
+ BitSet setIndex = bits.get(from, to);
+ int wordIndex = (int) bitSetToLong(setIndex);
+ // int wordIndex = (int) setIndex.toLongArray()[0]; // min API 19
+
+ fingerprint += words.get(wordIndex);
+
+ if (i != LAST_CHUNK_INDEX) {
+ // line break every 3 words
+ if (to % (CHUNK_SIZE * 3) == 0) {
+ fingerprint += "\n";
+ } else {
+ fingerprint += " ";
+ }
+ }
+
+ from = to;
+ to += CHUNK_SIZE;
+ }
+
+ return fingerprint;
+ }
+
+ /**
+ * Returns a BitSet containing the values in bytes.
+ * BIG ENDIAN!
+ */
+ private static BitSet bitSetToByteArray(byte[] bytes) {
+ int arrayLength = bytes.length * 8;
+ BitSet bits = new BitSet();
+
+ for (int i = 0; i < arrayLength; i++) {
+ if ((bytes[bytes.length - i / 8 - 1] & (1 << (i % 8))) > 0) {
+ bits.set(i);
+ }
+ }
+ return bits;
+ }
+
+ private static long bitSetToLong(BitSet bits) {
+ long value = 0L;
+ for (int i = 0; i < bits.length(); ++i) {
+ value += bits.get(i) ? (1L << i) : 0L;
+ }
+ return value;
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/KeyFormattingUtils.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/KeyFormattingUtils.java
index 9984c245e..284c17e7a 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/KeyFormattingUtils.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/KeyFormattingUtils.java
@@ -269,6 +269,10 @@ public class KeyFormattingUtils {
return hexString;
}
+ public static long convertFingerprintToKeyId(byte[] fingerprint) {
+ return ByteBuffer.wrap(fingerprint, 12, 8).getLong();
+ }
+
/**
* Makes a human-readable version of a key ID, which is usually 64 bits: lower-case, no
* leading 0x, space-separated quartets (for keys whose length in hex is divisible by 4)
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/SubtleAttentionSeeker.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/SubtleAttentionSeeker.java
index 87444c226..4549e8993 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/SubtleAttentionSeeker.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/SubtleAttentionSeeker.java
@@ -17,13 +17,19 @@
package org.sufficientlysecure.keychain.ui.util;
+import android.animation.AnimatorInflater;
+import android.animation.ArgbEvaluator;
import android.animation.Keyframe;
import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
import android.annotation.TargetApi;
+import android.content.Context;
import android.os.Build.VERSION_CODES;
import android.view.View;
+import org.sufficientlysecure.keychain.R;
+
+
@TargetApi(VERSION_CODES.ICE_CREAM_SANDWICH)
/** Simple animation helper for subtle attention seeker stuff.
*
@@ -36,6 +42,10 @@ public class SubtleAttentionSeeker {
}
public static ObjectAnimator tada(View view, float shakeFactor) {
+ return tada(view, shakeFactor, 1400);
+ }
+
+ public static ObjectAnimator tada(View view, float shakeFactor, int duration) {
PropertyValuesHolder pvhScaleX = PropertyValuesHolder.ofKeyframe(View.SCALE_X,
Keyframe.ofFloat(0f, 1f),
@@ -80,7 +90,19 @@ public class SubtleAttentionSeeker {
);
return ObjectAnimator.ofPropertyValuesHolder(view, pvhScaleX, pvhScaleY, pvhRotate).
- setDuration(1400);
+ setDuration(duration);
+ }
+
+ @TargetApi(VERSION_CODES.LOLLIPOP)
+ public static ObjectAnimator tintBackground(View view, int duration) {
+ return ObjectAnimator.ofArgb(view, "backgroundColor",
+ 0x00FF0000, 0x33FF0000, 0x00FF0000).setDuration(duration);
+ }
+
+ @TargetApi(VERSION_CODES.LOLLIPOP)
+ public static ObjectAnimator tintText(View view, int duration) {
+ return ObjectAnimator.ofArgb(view, "backgroundColor",
+ 0x00FF7F00, 0x33FF7F00, 0x00FF7F00).setDuration(duration);
}
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/ThemeChanger.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/ThemeChanger.java
index 75a0d1ea5..375483d89 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/ThemeChanger.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/ThemeChanger.java
@@ -40,9 +40,9 @@ public class ThemeChanger {
// hack to get holo design (which is not automatically applied due to activity's
// Theme.NoDisplay)
if (Constants.Pref.Theme.DARK.equals(preferences.getTheme())) {
- return new ContextThemeWrapper(context, R.style.Theme_AppCompat_Dialog);
+ return new ContextThemeWrapper(context, R.style.Theme_Keychain_Dark);
} else {
- return new ContextThemeWrapper(context, R.style.Theme_AppCompat_Light_Dialog);
+ return new ContextThemeWrapper(context, R.style.Theme_Keychain_Light);
}
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/CertListWidget.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/CertListWidget.java
new file mode 100644
index 000000000..c413a00be
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/CertListWidget.java
@@ -0,0 +1,143 @@
+package org.sufficientlysecure.keychain.ui.widget;
+
+import java.util.Date;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.v4.app.LoaderManager;
+import android.support.v4.content.CursorLoader;
+import android.support.v4.content.Loader;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.ViewAnimator;
+
+import org.ocpsoft.prettytime.PrettyTime;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.provider.KeychainContract;
+import org.sufficientlysecure.keychain.provider.KeychainContract.Certs;
+
+public class CertListWidget extends ViewAnimator
+ implements LoaderManager.LoaderCallbacks<Cursor> {
+
+ public static final int LOADER_ID_LINKED_CERTS = 38572;
+
+ public static final String ARG_URI = "uri";
+ public static final String ARG_IS_SECRET = "is_secret";
+
+
+ // These are the rows that we will retrieve.
+ static final String[] CERTS_PROJECTION = new String[]{
+ KeychainContract.Certs._ID,
+ KeychainContract.Certs.MASTER_KEY_ID,
+ KeychainContract.Certs.VERIFIED,
+ KeychainContract.Certs.TYPE,
+ KeychainContract.Certs.RANK,
+ KeychainContract.Certs.KEY_ID_CERTIFIER,
+ KeychainContract.Certs.USER_ID,
+ KeychainContract.Certs.SIGNER_UID,
+ KeychainContract.Certs.CREATION
+ };
+ public static final int INDEX_MASTER_KEY_ID = 1;
+ public static final int INDEX_VERIFIED = 2;
+ public static final int INDEX_TYPE = 3;
+ public static final int INDEX_RANK = 4;
+ public static final int INDEX_KEY_ID_CERTIFIER = 5;
+ public static final int INDEX_USER_ID = 6;
+ public static final int INDEX_SIGNER_UID = 7;
+ public static final int INDEX_CREATION = 8;
+
+ private TextView vCollapsed;
+ private ListView vExpanded;
+ private View vExpandButton;
+ private boolean mIsSecret;
+
+ public CertListWidget(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+
+ View root = getRootView();
+ vCollapsed = (TextView) root.findViewById(R.id.cert_collapsed_list);
+ vExpanded = (ListView) root.findViewById(R.id.cert_expanded_list);
+ vExpandButton = root.findViewById(R.id.cert_expand_button);
+
+ // for now
+ vExpandButton.setVisibility(View.GONE);
+ vExpandButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ toggleExpanded();
+ }
+ });
+
+ // vExpanded.setAdapter(null);
+
+ }
+
+ void toggleExpanded() {
+ setDisplayedChild(getDisplayedChild() == 1 ? 0 : 1);
+ }
+
+ void setExpanded(boolean expanded) {
+ setDisplayedChild(expanded ? 1 : 0);
+ }
+
+ @Override
+ public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+ Uri uri = args.getParcelable(ARG_URI);
+ mIsSecret = args.getBoolean(ARG_IS_SECRET, false);
+ return new CursorLoader(getContext(), uri,
+ CERTS_PROJECTION, null, null, null);
+ }
+
+ @Override
+ public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
+
+ if (data == null || !data.moveToFirst()) {
+ return;
+ }
+
+ // TODO support external certificates
+ Date userCert = null;
+ while (!data.isAfterLast()) {
+
+ int verified = data.getInt(INDEX_VERIFIED);
+ Date creation = new Date(data.getLong(INDEX_CREATION) * 1000);
+
+ if (verified == Certs.VERIFIED_SECRET) {
+ if (userCert == null || userCert.after(creation)) {
+ userCert = creation;
+ }
+ }
+
+ data.moveToNext();
+ }
+
+ if (userCert != null) {
+ PrettyTime format = new PrettyTime();
+ if (mIsSecret) {
+ vCollapsed.setText("You created this identity "
+ + format.format(userCert) + ".");
+ } else {
+ vCollapsed.setText("You verified and confirmed this identity "
+ + format.format(userCert) + ".");
+ }
+ } else {
+ vCollapsed.setText("This identity is not yet verified or confirmed.");
+ }
+
+ }
+
+ @Override
+ public void onLoaderReset(Loader<Cursor> loader) {
+ setVisibility(View.GONE);
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/CertifyKeySpinner.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/CertifyKeySpinner.java
index 6cd33aada..6a51085f3 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/CertifyKeySpinner.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/CertifyKeySpinner.java
@@ -17,25 +17,24 @@
package org.sufficientlysecure.keychain.ui.widget;
-
-import java.util.Arrays;
-import java.util.List;
-
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
+import android.support.annotation.StringRes;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.util.AttributeSet;
import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.provider.KeychainContract;
import org.sufficientlysecure.keychain.provider.KeychainDatabase;
import org.sufficientlysecure.keychain.ui.adapter.KeyAdapter;
public class CertifyKeySpinner extends KeySpinner {
private long mHiddenMasterKeyId = Constants.key.none;
+ private boolean mIsSingle;
public CertifyKeySpinner(Context context) {
super(context);
@@ -94,9 +93,11 @@ public class CertifyKeySpinner extends KeySpinner {
if (!data.isNull(mIndexHasCertify)) {
if (selection == -1) {
selection = data.getPosition() + 1;
+ mIsSingle = true;
} else {
// if selection is already set, we have more than one certify key!
// get back to "none"!
+ mIsSingle = false;
selection = 0;
}
}
@@ -106,6 +107,9 @@ public class CertifyKeySpinner extends KeySpinner {
}
}
+ public boolean isSingleEntry() {
+ return mIsSingle && getSelectedItemPosition() != 0;
+ }
@Override
boolean isItemEnabled(Cursor cursor) {
@@ -128,4 +132,10 @@ public class CertifyKeySpinner extends KeySpinner {
return true;
}
+ @Override
+ public @StringRes int getNoneString() {
+ return R.string.choice_select_cert;
+ }
+
+
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/KeySpinner.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/KeySpinner.java
index 1884daf12..04ed35deb 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/KeySpinner.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/KeySpinner.java
@@ -23,6 +23,7 @@ import android.database.Cursor;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.annotation.NonNull;
+import android.support.annotation.StringRes;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.Loader;
@@ -34,6 +35,7 @@ import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.SpinnerAdapter;
+import android.widget.TextView;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
@@ -117,8 +119,7 @@ public abstract class KeySpinner extends AppCompatSpinner implements
if (getContext() instanceof FragmentActivity) {
((FragmentActivity) getContext()).getSupportLoaderManager().restartLoader(LOADER_ID, null, this);
} else {
- throw new AssertionError("KeySpinner must be attached to FragmentActivity, this is "
- + getContext().getClass());
+ // ignore, this happens during preview! we use fragmentactivities everywhere either way
}
}
@@ -226,9 +227,11 @@ public abstract class KeySpinner extends AppCompatSpinner implements
return inner.getView(position -1, convertView, parent);
}
- return convertView != null ? convertView :
+ View view = convertView != null ? convertView :
LayoutInflater.from(getContext()).inflate(
R.layout.keyspinner_item_none, parent, false);
+ ((TextView) view.findViewById(R.id.keyspinner_key_name)).setText(getNoneString());
+ return view;
}
}
@@ -259,4 +262,9 @@ public abstract class KeySpinner extends AppCompatSpinner implements
bundle.putLong(ARG_KEY_ID, getSelectedKeyId());
return bundle;
}
+
+ public @StringRes int getNoneString() {
+ return R.string.cert_none;
+ }
+
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/PrefixedEditText.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/PrefixedEditText.java
new file mode 100644
index 000000000..3cbb114e8
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/PrefixedEditText.java
@@ -0,0 +1,45 @@
+package org.sufficientlysecure.keychain.ui.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.*;
+import android.support.annotation.NonNull;
+import android.util.AttributeSet;
+import android.widget.EditText;
+
+import org.sufficientlysecure.keychain.R;
+
+public class PrefixedEditText extends EditText {
+
+ private String mPrefix;
+ private Rect mPrefixRect = new Rect();
+
+ public PrefixedEditText(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ TypedArray style = context.getTheme().obtainStyledAttributes(
+ attrs, R.styleable.PrefixedEditText, 0, 0);
+ mPrefix = style.getString(R.styleable.PrefixedEditText_prefix);
+ if (mPrefix == null) {
+ mPrefix = "";
+ }
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ getPaint().getTextBounds(mPrefix, 0, mPrefix.length(), mPrefixRect);
+
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+
+ @Override
+ protected void onDraw(@NonNull Canvas canvas) {
+ super.onDraw(canvas);
+ canvas.drawText(mPrefix, super.getCompoundPaddingLeft(), getBaseline(), getPaint());
+ }
+
+ @Override
+ public int getCompoundPaddingLeft() {
+ return super.getCompoundPaddingLeft() + mPrefixRect.width();
+ }
+
+} \ No newline at end of file
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/StatusIndicator.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/StatusIndicator.java
new file mode 100644
index 000000000..2784ac5f0
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/StatusIndicator.java
@@ -0,0 +1,45 @@
+package org.sufficientlysecure.keychain.ui.widget;
+
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.animation.AnimationUtils;
+
+import org.sufficientlysecure.keychain.R;
+
+
+public class StatusIndicator extends ToolableViewAnimator {
+
+ public enum Status {
+ IDLE, PROGRESS, OK, ERROR
+ }
+
+ public StatusIndicator(Context context) {
+ super(context);
+
+ LayoutInflater.from(context).inflate(R.layout.status_indicator, this, true);
+ setInAnimation(AnimationUtils.loadAnimation(context, R.anim.fade_in));
+ setOutAnimation(AnimationUtils.loadAnimation(context, R.anim.fade_out));
+ }
+
+ public StatusIndicator(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ LayoutInflater.from(context).inflate(R.layout.status_indicator, this, true);
+ setInAnimation(AnimationUtils.loadAnimation(context, R.anim.fade_in));
+ setOutAnimation(AnimationUtils.loadAnimation(context, R.anim.fade_out));
+ }
+
+ @Override
+ public void setDisplayedChild(int whichChild) {
+ if (whichChild != getDisplayedChild()) {
+ super.setDisplayedChild(whichChild);
+ }
+ }
+
+ public void setDisplayedChild(Status status) {
+ setDisplayedChild(status.ordinal());
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ExportHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ExportHelper.java
index 5f2329170..45dc33906 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ExportHelper.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ExportHelper.java
@@ -20,6 +20,8 @@ package org.sufficientlysecure.keychain.util;
import java.io.File;
+import android.content.Intent;
+import android.net.Uri;
import android.support.v4.app.FragmentActivity;
import org.sufficientlysecure.keychain.Constants;
@@ -67,7 +69,7 @@ public class ExportHelper
: R.string.specify_backup_dest_single);
}
- FileHelper.saveFile(new FileHelper.FileDialogCallback() {
+ FileHelper.saveDocumentDialog(new FileHelper.FileDialogCallback() {
@Override
public void onFileSelected(File file, boolean checked) {
mExportFile = file;
@@ -98,7 +100,10 @@ public class ExportHelper
}
@Override
- public void onCryptoOperationSuccess(ExportResult result) {
+ final public void onCryptoOperationSuccess(ExportResult result) {
+ // trigger scan of the created 'media' file so it shows up on MTP
+ // http://stackoverflow.com/questions/13737261/nexus-4-not-showing-files-via-mtp
+ mActivity.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(mExportFile)));
result.createNotify(mActivity).show();
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/FileHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/FileHelper.java
index 4a00f46cb..9fb362412 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/FileHelper.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/FileHelper.java
@@ -28,16 +28,19 @@ import android.graphics.Bitmap;
import android.graphics.Point;
import android.net.Uri;
import android.os.Build;
+import android.os.Build.VERSION_CODES;
import android.os.Environment;
import android.os.Handler;
import android.os.Message;
import android.os.Messenger;
import android.provider.DocumentsContract;
import android.provider.OpenableColumns;
+import android.support.annotation.StringRes;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.widget.Toast;
+import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.compatibility.DialogFragmentWorkaround;
import org.sufficientlysecure.keychain.ui.dialog.FileDialogFragment;
@@ -46,42 +49,95 @@ import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
-import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.text.DecimalFormat;
+
+/** This class offers a number of helper functions for saving documents.
+ *
+ * There are three entry points here: openDocument, saveDocument and
+ * saveDocumentDialog. Each behaves a little differently depending on whether
+ * the Android version used is pre or post KitKat.
+ *
+ * - openDocument queries for a document for reading. Used in "open encrypted
+ * file" ui flow. On pre-kitkat, this relies on an external file manager,
+ * and will fail with a toast message if none is installed.
+ *
+ * - saveDocument queries for a document name for saving. on pre-kitkat, this
+ * shows a dialog where a filename can be input. on kitkat and up, it
+ * directly triggers a "save document" intent. Used in "save encrypted file"
+ * ui flow.
+ *
+ * - saveDocumentDialog queries for a document. this shows a dialog on all
+ * versions of android. the browse button opens an external browser on
+ * pre-kitkat or the "save document" intent on post-kitkat devices. Used in
+ * "backup key" ui flow.
+ *
+ * It is noteworthy that the "saveDocument" call is essentially substituted
+ * by the "saveDocumentDialog" on pre-kitkat devices.
+ *
+ */
public class FileHelper {
- /**
- * Checks if external storage is mounted if file is located on external storage
- *
- * @return true if storage is mounted
- */
- public static boolean isStorageMounted(String file) {
- if (file.startsWith(Environment.getExternalStorageDirectory().getAbsolutePath())) {
- if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
- return false;
- }
+ public static void openDocument(Fragment fragment, Uri last, String mimeType, boolean multiple, int requestCode) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
+ openDocumentPreKitKat(fragment, last, mimeType, multiple, requestCode);
+ } else {
+ openDocumentKitKat(fragment, mimeType, multiple, requestCode);
}
+ }
- return true;
+ public static void saveDocument(Fragment fragment, String targetName, Uri inputUri,
+ @StringRes int title, @StringRes int message, int requestCode) {
+ saveDocument(fragment, targetName, inputUri, "*/*", title, message, requestCode);
}
- /**
- * Opens the preferred installed file manager on Android and shows a toast if no manager is
- * installed.
- *
- * @param last default selected Uri, not supported by all file managers
- * @param mimeType can be text/plain for example
- * @param requestCode requestCode used to identify the result coming back from file manager to
- * onActivityResult() in your activity
- */
- public static void openFile(Fragment fragment, Uri last, String mimeType, int requestCode) {
+ public static void saveDocument(Fragment fragment, String targetName, Uri inputUri, String mimeType,
+ @StringRes int title, @StringRes int message, int requestCode) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
+ saveDocumentDialog(fragment, targetName, inputUri, title, message, requestCode);
+ } else {
+ saveDocumentKitKat(fragment, mimeType, targetName, requestCode);
+ }
+ }
+
+ public static void saveDocumentDialog(final Fragment fragment, String targetName, Uri inputUri,
+ @StringRes int title, @StringRes int message, final int requestCode) {
+
+ saveDocumentDialog(fragment, targetName, inputUri, title, message, new FileDialogCallback() {
+ // is this a good idea? seems hacky...
+ @Override
+ public void onFileSelected(File file, boolean checked) {
+ Intent intent = new Intent();
+ intent.setData(Uri.fromFile(file));
+ fragment.onActivityResult(requestCode, Activity.RESULT_OK, intent);
+ }
+ });
+ }
+
+ public static void saveDocumentDialog(final Fragment fragment, String targetName, Uri inputUri,
+ @StringRes int title, @StringRes int message, FileDialogCallback callback) {
+
+ File file = inputUri == null ? null : new File(inputUri.getPath());
+ File parentDir = file != null && file.exists() ? file.getParentFile() : Constants.Path.APP_DIR;
+ File targetFile = new File(parentDir, targetName);
+ saveDocumentDialog(callback, fragment.getActivity().getSupportFragmentManager(),
+ fragment.getString(title), fragment.getString(message), targetFile, null);
+
+ }
+
+ /** Opens the preferred installed file manager on Android and shows a toast
+ * if no manager is installed. */
+ private static void openDocumentPreKitKat(
+ Fragment fragment, Uri last, String mimeType, boolean multiple, int requestCode) {
+
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
-
+ if (Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR2) {
+ intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, multiple);
+ }
intent.setData(last);
intent.setType(mimeType);
@@ -92,11 +148,34 @@ public class FileHelper {
Toast.makeText(fragment.getActivity(), R.string.no_filemanager_installed,
Toast.LENGTH_SHORT).show();
}
+
+ }
+
+ /** Opens the storage browser on Android 4.4 or later for opening a file */
+ @TargetApi(Build.VERSION_CODES.KITKAT)
+ private static void openDocumentKitKat(Fragment fragment, String mimeType, boolean multiple, int requestCode) {
+ Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
+ intent.addCategory(Intent.CATEGORY_OPENABLE);
+ intent.setType(mimeType);
+ intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, multiple);
+ fragment.startActivityForResult(intent, requestCode);
+ }
+
+ /** Opens the storage browser on Android 4.4 or later for saving a file. */
+ @TargetApi(Build.VERSION_CODES.KITKAT)
+ public static void saveDocumentKitKat(Fragment fragment, String mimeType, String suggestedName, int requestCode) {
+ Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
+ intent.addCategory(Intent.CATEGORY_OPENABLE);
+ intent.setType(mimeType);
+ intent.putExtra("android.content.extra.SHOW_ADVANCED", true); // Note: This is not documented, but works
+ intent.putExtra(Intent.EXTRA_TITLE, suggestedName);
+ fragment.startActivityForResult(intent, requestCode);
}
- public static void saveFile(final FileDialogCallback callback, final FragmentManager fragmentManager,
- final String title, final String message, final File defaultFile,
- final String checkMsg) {
+ public static void saveDocumentDialog(
+ final FileDialogCallback callback, final FragmentManager fragmentManager,
+ final String title, final String message, final File defaultFile,
+ final String checkMsg) {
// Message is received after file is selected
Handler returnHandler = new Handler() {
@Override
@@ -123,61 +202,6 @@ public class FileHelper {
});
}
- public static void saveFile(Fragment fragment, String title, String message, File defaultFile, int requestCode) {
- saveFile(fragment, title, message, defaultFile, requestCode, null);
- }
-
- public static void saveFile(final Fragment fragment, String title, String message, File defaultFile,
- final int requestCode, String checkMsg) {
- saveFile(new FileDialogCallback() {
- @Override
- public void onFileSelected(File file, boolean checked) {
- Intent intent = new Intent();
- intent.setData(Uri.fromFile(file));
- fragment.onActivityResult(requestCode, Activity.RESULT_OK, intent);
- }
- }, fragment.getActivity().getSupportFragmentManager(), title, message, defaultFile, checkMsg);
- }
-
- @TargetApi(Build.VERSION_CODES.KITKAT)
- public static void openDocument(Fragment fragment, String mimeType, int requestCode) {
- openDocument(fragment, mimeType, false, requestCode);
- }
-
- /**
- * Opens the storage browser on Android 4.4 or later for opening a file
- *
- * @param mimeType can be text/plain for example
- * @param multiple allow file chooser to return multiple files
- * @param requestCode used to identify the result coming back from storage browser onActivityResult() in your
- */
- @TargetApi(Build.VERSION_CODES.KITKAT)
- public static void openDocument(Fragment fragment, String mimeType, boolean multiple, int requestCode) {
- Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
- intent.addCategory(Intent.CATEGORY_OPENABLE);
- intent.setType(mimeType);
- intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, multiple);
-
- fragment.startActivityForResult(intent, requestCode);
- }
-
- /**
- * Opens the storage browser on Android 4.4 or later for saving a file
- *
- * @param mimeType can be text/plain for example
- * @param suggestedName a filename desirable for the file to be saved
- * @param requestCode used to identify the result coming back from storage browser onActivityResult() in your
- */
- @TargetApi(Build.VERSION_CODES.KITKAT)
- public static void saveDocument(Fragment fragment, String mimeType, String suggestedName, int requestCode) {
- Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
- intent.addCategory(Intent.CATEGORY_OPENABLE);
- intent.setType(mimeType);
- intent.putExtra("android.content.extra.SHOW_ADVANCED", true); // Note: This is not documented, but works
- intent.putExtra(Intent.EXTRA_TITLE, suggestedName);
- fragment.startActivityForResult(intent, requestCode);
- }
-
public static String getFilename(Context context, Uri uri) {
String filename = null;
try {
@@ -298,6 +322,17 @@ public class FileHelper {
}
}
+ /** Checks if external storage is mounted if file is located on external storage. */
+ public static boolean isStorageMounted(String file) {
+ if (file.startsWith(Environment.getExternalStorageDirectory().getAbsolutePath())) {
+ if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
public interface FileDialogCallback {
void onFileSelected(File file, boolean checked);
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/FilterCursorWrapper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/FilterCursorWrapper.java
new file mode 100644
index 000000000..ab73f59b8
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/FilterCursorWrapper.java
@@ -0,0 +1,76 @@
+package org.sufficientlysecure.keychain.util;
+
+
+import android.database.Cursor;
+import android.database.CursorWrapper;
+
+public abstract class FilterCursorWrapper extends CursorWrapper {
+ private int[] mIndex;
+ private int mCount = 0;
+ private int mPos = 0;
+
+ public abstract boolean isVisible(Cursor cursor);
+
+ public FilterCursorWrapper(Cursor cursor) {
+ super(cursor);
+ mCount = super.getCount();
+ mIndex = new int[mCount];
+ for (int i = 0; i < mCount; i++) {
+ super.moveToPosition(i);
+ if (isVisible(cursor)) {
+ mIndex[mPos++] = i;
+ }
+ }
+ mCount = mPos;
+ mPos = 0;
+ super.moveToFirst();
+ }
+
+ @Override
+ public boolean move(int offset) {
+ return this.moveToPosition(mPos + offset);
+ }
+
+ @Override
+ public boolean moveToNext() {
+ return this.moveToPosition(mPos + 1);
+ }
+
+ @Override
+ public boolean moveToPrevious() {
+ return this.moveToPosition(mPos - 1);
+ }
+
+ @Override
+ public boolean moveToFirst() {
+ return this.moveToPosition(0);
+ }
+
+ @Override
+ public boolean moveToLast() {
+ return this.moveToPosition(mCount - 1);
+ }
+
+ @Override
+ public boolean moveToPosition(int position) {
+ if (position >= mCount || position < 0) {
+ return false;
+ }
+ return super.moveToPosition(mIndex[position]);
+ }
+
+ @Override
+ public int getCount() {
+ return mCount;
+ }
+
+ public int getHiddenCount() {
+ return super.getCount() - mCount;
+ }
+
+ @Override
+ public int getPosition() {
+ return mPos;
+ }
+
+} \ No newline at end of file
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Preferences.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Preferences.java
index 0596b0079..4ef215036 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Preferences.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Preferences.java
@@ -25,7 +25,7 @@ import android.content.res.Resources;
import android.preference.PreferenceManager;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.Constants.Pref;
-import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.service.KeyserverSyncAdapterService;
import java.net.Proxy;
import java.util.ArrayList;
@@ -285,16 +285,19 @@ public class Preferences {
}
public Proxy.Type getProxyType() {
- final String typeHttp = mResources.getString(R.string.pref_proxy_type_value_http);
- final String typeSocks = mResources.getString(R.string.pref_proxy_type_value_socks);
+ final String typeHttp = Pref.ProxyType.TYPE_HTTP;
+ final String typeSocks = Pref.ProxyType.TYPE_SOCKS;
String type = mSharedPreferences.getString(Pref.PROXY_TYPE, typeHttp);
- if (type.equals(typeHttp)) return Proxy.Type.HTTP;
- else if (type.equals(typeSocks)) return Proxy.Type.SOCKS;
- else { // shouldn't happen
- Log.e(Constants.TAG, "Invalid Proxy Type in preferences");
- return null;
+ switch (type) {
+ case typeHttp:
+ return Proxy.Type.HTTP;
+ case typeSocks:
+ return Proxy.Type.SOCKS;
+ default: // shouldn't happen
+ Log.e(Constants.TAG, "Invalid Proxy Type in preferences");
+ return null;
}
}
@@ -306,7 +309,7 @@ public class Preferences {
return new ProxyPrefs(true, false, Constants.Orbot.PROXY_HOST, Constants.Orbot.PROXY_PORT,
Constants.Orbot.PROXY_TYPE);
} else if (useNormalProxy) {
- return new ProxyPrefs(useTor, useNormalProxy, getProxyHost(), getProxyPort(), getProxyType());
+ return new ProxyPrefs(false, true, getProxyHost(), getProxyPort(), getProxyType());
} else {
return new ProxyPrefs(false, false, null, -1, null);
}
@@ -331,7 +334,7 @@ public class Preferences {
}
}
- // proxy preference functions ends here
+ // cloud prefs
public CloudSearchPrefs getCloudSearchPrefs() {
return new CloudSearchPrefs(mSharedPreferences.getBoolean(Pref.SEARCH_KEYSERVER, true),
@@ -356,7 +359,39 @@ public class Preferences {
}
}
- public void upgradePreferences() {
+ // experimental prefs
+
+ public void setExperimentalEnableWordConfirm(boolean enableWordConfirm) {
+ SharedPreferences.Editor editor = mSharedPreferences.edit();
+ editor.putBoolean(Pref.EXPERIMENTAL_ENABLE_WORD_CONFIRM, enableWordConfirm);
+ editor.commit();
+ }
+
+ public boolean getExperimentalEnableWordConfirm() {
+ return mSharedPreferences.getBoolean(Pref.EXPERIMENTAL_ENABLE_WORD_CONFIRM, false);
+ }
+
+ public void setExperimentalEnableLinkedIdentities(boolean enableLinkedIdentities) {
+ SharedPreferences.Editor editor = mSharedPreferences.edit();
+ editor.putBoolean(Pref.EXPERIMENTAL_ENABLE_LINKED_IDENTITIES, enableLinkedIdentities);
+ editor.commit();
+ }
+
+ public boolean getExperimentalEnableLinkedIdentities() {
+ return mSharedPreferences.getBoolean(Pref.EXPERIMENTAL_ENABLE_LINKED_IDENTITIES, false);
+ }
+
+ public void setExperimentalEnableKeybase(boolean enableKeybase) {
+ SharedPreferences.Editor editor = mSharedPreferences.edit();
+ editor.putBoolean(Pref.EXPERIMENTAL_ENABLE_KEYBASE, enableKeybase);
+ editor.commit();
+ }
+
+ public boolean getExperimentalEnableKeybase() {
+ return mSharedPreferences.getBoolean(Pref.EXPERIMENTAL_ENABLE_KEYBASE, false);
+ }
+
+ public void upgradePreferences(Context context) {
if (mSharedPreferences.getInt(Constants.Pref.PREF_DEFAULT_VERSION, 0) !=
Constants.Defaults.PREF_VERSION) {
switch (mSharedPreferences.getInt(Constants.Pref.PREF_DEFAULT_VERSION, 0)) {
@@ -394,6 +429,10 @@ public class Preferences {
}
// fall through
case 5: {
+ KeyserverSyncAdapterService.enableKeyserverSync(context);
+ }
+ // fall through
+ case 6: {
}
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/orbot/OrbotHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/orbot/OrbotHelper.java
index d5ee75f9b..d85ad9128 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/orbot/OrbotHelper.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/orbot/OrbotHelper.java
@@ -361,7 +361,7 @@ public class OrbotHelper {
.show(fragmentActivity.getSupportFragmentManager(),
"OrbotHelperOrbotStartDialog");
}
- }.startOrbotAndListen(fragmentActivity);
+ }.startOrbotAndListen(fragmentActivity, true);
return false;
} else {
@@ -385,7 +385,8 @@ public class OrbotHelper {
* activities wishing to respond to a change in Orbot state.
*/
public static void bestPossibleOrbotStart(final DialogActions dialogActions,
- final Activity activity) {
+ final Activity activity,
+ boolean showProgress) {
new SilentStartManager() {
@Override
@@ -397,23 +398,23 @@ public class OrbotHelper {
protected void onSilentStartDisabled() {
requestShowOrbotStart(activity);
}
- }.startOrbotAndListen(activity);
+ }.startOrbotAndListen(activity, showProgress);
}
/**
* base class for listening to silent orbot starts. Also handles display of progress dialog.
*/
- private static abstract class SilentStartManager {
+ public static abstract class SilentStartManager {
private ProgressDialog mProgressDialog;
- public void startOrbotAndListen(Context context) {
- mProgressDialog = new ProgressDialog(ThemeChanger.getDialogThemeWrapper(context));
- mProgressDialog.setMessage(context.getString(R.string.progress_starting_orbot));
- mProgressDialog.setCancelable(false);
- mProgressDialog.show();
+ public void startOrbotAndListen(final Context context, final boolean showProgress) {
+ Log.d(Constants.TAG, "starting orbot listener");
+ if (showProgress) {
+ showProgressDialog(context);
+ }
- BroadcastReceiver receiver = new BroadcastReceiver() {
+ final BroadcastReceiver receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
switch (intent.getStringExtra(OrbotHelper.EXTRA_STATUS)) {
@@ -423,14 +424,18 @@ public class OrbotHelper {
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
- mProgressDialog.dismiss();
+ if (showProgress) {
+ mProgressDialog.dismiss();
+ }
onOrbotStarted();
}
}, 1000);
break;
case OrbotHelper.STATUS_STARTS_DISABLED:
context.unregisterReceiver(this);
- mProgressDialog.dismiss();
+ if (showProgress) {
+ mProgressDialog.dismiss();
+ }
onSilentStartDisabled();
break;
@@ -444,6 +449,13 @@ public class OrbotHelper {
requestStartTor(context);
}
+ private void showProgressDialog(Context context) {
+ mProgressDialog = new ProgressDialog(ThemeChanger.getDialogThemeWrapper(context));
+ mProgressDialog.setMessage(context.getString(R.string.progress_starting_orbot));
+ mProgressDialog.setCancelable(false);
+ mProgressDialog.show();
+ }
+
protected abstract void onOrbotStarted();
protected abstract void onSilentStartDisabled();