aboutsummaryrefslogtreecommitdiffstats
path: root/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util
diff options
context:
space:
mode:
Diffstat (limited to 'OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util')
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ContactHelper.java13
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/EmailKeyHelper.java69
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ExportHelper.java124
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/FileHelper.java271
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/KeyUpdateHelper.java10
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/OrientationUtils.java85
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ParcelableFileCache.java25
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ParcelableHashMap.java63
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ParcelableProxy.java91
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Passphrase.java13
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Preferences.java205
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ShareHelper.java1
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/TlsHelper.java51
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/orbot/OrbotHelper.java463
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/orbot/TorServiceUtils.java156
15 files changed, 1423 insertions, 217 deletions
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ContactHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ContactHelper.java
index e1efd5abc..77aa1a055 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ContactHelper.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ContactHelper.java
@@ -303,10 +303,13 @@ public class ContactHelper {
Cursor contactMasterKey = context.getContentResolver().query(contactUri,
new String[]{ContactsContract.Data.DATA2}, null, null, null);
if (contactMasterKey != null) {
- if (contactMasterKey.moveToNext()) {
- return KeychainContract.KeyRings.buildGenericKeyRingUri(contactMasterKey.getLong(0));
+ try {
+ if (contactMasterKey.moveToNext()) {
+ return KeychainContract.KeyRings.buildGenericKeyRingUri(contactMasterKey.getLong(0));
+ }
+ } finally {
+ contactMasterKey.close();
}
- contactMasterKey.close();
}
return null;
}
@@ -537,7 +540,7 @@ public class ContactHelper {
KEYS_TO_CONTACT_PROJECTION,
KeychainContract.KeyRings.HAS_ANY_SECRET + "!=0",
null, null);
- if (cursor != null) {
+ if (cursor != null) try {
while (cursor.moveToNext()) {
long masterKeyId = cursor.getLong(INDEX_MASTER_KEY_ID);
boolean isExpired = cursor.getInt(INDEX_IS_EXPIRED) != 0;
@@ -565,6 +568,8 @@ public class ContactHelper {
}
}
}
+ } finally {
+ cursor.close();
}
for (long masterKeyId : keysToDelete) {
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/EmailKeyHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/EmailKeyHelper.java
index 8334b37ec..d7491ab26 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/EmailKeyHelper.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/EmailKeyHelper.java
@@ -18,16 +18,16 @@
package org.sufficientlysecure.keychain.util;
import android.content.Context;
-import android.content.Intent;
-import android.os.Bundle;
-import android.os.Messenger;
import org.sufficientlysecure.keychain.keyimport.HkpKeyserver;
import org.sufficientlysecure.keychain.keyimport.ImportKeysListEntry;
import org.sufficientlysecure.keychain.keyimport.Keyserver;
import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing;
-import org.sufficientlysecure.keychain.service.KeychainIntentService;
+import org.sufficientlysecure.keychain.operations.results.ImportKeyResult;
+import org.sufficientlysecure.keychain.service.ImportKeyringParcel;
+import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper;
+import java.net.Proxy;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
@@ -35,27 +35,40 @@ import java.util.Locale;
import java.util.Set;
public class EmailKeyHelper {
+ // TODO: Make this not require a proxy in it's constructor, redesign when it is to be used
+ // to import keys, simply use CryptoOperationHelper with this callback
+ public abstract class ImportContactKeysCallback
+ implements CryptoOperationHelper.Callback<ImportKeyringParcel, ImportKeyResult> {
- public static void importContacts(Context context, Messenger messenger) {
- importAll(context, messenger, ContactHelper.getContactMails(context));
- }
+ private ArrayList<ParcelableKeyRing> mKeyList;
+ private String mKeyserver;
- public static void importAll(Context context, Messenger messenger, List<String> mails) {
- // Collect all candidates as ImportKeysListEntry (set for deduplication)
- Set<ImportKeysListEntry> entries = new HashSet<>();
- for (String mail : mails) {
- entries.addAll(getEmailKeys(context, mail));
+ public ImportContactKeysCallback(Context context, String keyserver, Proxy proxy) {
+ this(context, ContactHelper.getContactMails(context), keyserver, proxy);
}
- // Put them in a list and import
- ArrayList<ParcelableKeyRing> keys = new ArrayList<>(entries.size());
- for (ImportKeysListEntry entry : entries) {
- keys.add(new ParcelableKeyRing(entry.getFingerprintHex(), entry.getKeyIdHex(), null));
+ public ImportContactKeysCallback(Context context, List<String> mails, String keyserver,
+ Proxy proxy) {
+ Set<ImportKeysListEntry> entries = new HashSet<>();
+ for (String mail : mails) {
+ entries.addAll(getEmailKeys(context, mail, proxy));
+ }
+
+ // Put them in a list and import
+ ArrayList<ParcelableKeyRing> keys = new ArrayList<>(entries.size());
+ for (ImportKeysListEntry entry : entries) {
+ keys.add(new ParcelableKeyRing(entry.getFingerprintHex(), entry.getKeyIdHex(), null));
+ }
+ mKeyList = keys;
+ mKeyserver = keyserver;
+ }
+ @Override
+ public ImportKeyringParcel createOperationInput() {
+ return new ImportKeyringParcel(mKeyList, mKeyserver);
}
- importKeys(context, messenger, keys);
}
- public static Set<ImportKeysListEntry> getEmailKeys(Context context, String mail) {
+ public static Set<ImportKeysListEntry> getEmailKeys(Context context, String mail, Proxy proxy) {
Set<ImportKeysListEntry> keys = new HashSet<>();
// Try _hkp._tcp SRV record first
@@ -63,7 +76,7 @@ public class EmailKeyHelper {
if (mailparts.length == 2) {
HkpKeyserver hkp = HkpKeyserver.resolve(mailparts[1]);
if (hkp != null) {
- keys.addAll(getEmailKeys(mail, hkp));
+ keys.addAll(getEmailKeys(mail, hkp, proxy));
}
}
@@ -72,27 +85,17 @@ public class EmailKeyHelper {
String server = Preferences.getPreferences(context).getPreferredKeyserver();
if (server != null) {
HkpKeyserver hkp = new HkpKeyserver(server);
- keys.addAll(getEmailKeys(mail, hkp));
+ keys.addAll(getEmailKeys(mail, hkp, proxy));
}
}
return keys;
}
- private static void importKeys(Context context, Messenger messenger, ArrayList<ParcelableKeyRing> keys) {
- Intent importIntent = new Intent(context, KeychainIntentService.class);
- importIntent.setAction(KeychainIntentService.ACTION_IMPORT_KEYRING);
- Bundle importData = new Bundle();
- importData.putParcelableArrayList(KeychainIntentService.IMPORT_KEY_LIST, keys);
- importIntent.putExtra(KeychainIntentService.EXTRA_DATA, importData);
- importIntent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger);
-
- context.startService(importIntent);
- }
-
- public static List<ImportKeysListEntry> getEmailKeys(String mail, Keyserver keyServer) {
+ public static List<ImportKeysListEntry> getEmailKeys(String mail, Keyserver keyServer,
+ Proxy proxy) {
Set<ImportKeysListEntry> keys = new HashSet<>();
try {
- for (ImportKeysListEntry key : keyServer.search(mail)) {
+ for (ImportKeysListEntry key : keyServer.search(mail, proxy)) {
if (key.isRevoked() || key.isExpired()) continue;
for (String userId : key.getUserIds()) {
if (userId.toLowerCase().contains(mail.toLowerCase(Locale.ENGLISH))) {
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 7efb7c5af..45dc33906 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ExportHelper.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ExportHelper.java
@@ -17,41 +17,40 @@
package org.sufficientlysecure.keychain.util;
-import android.app.ProgressDialog;
+
+import java.io.File;
+
import android.content.Intent;
-import android.os.Bundle;
-import android.os.Message;
-import android.os.Messenger;
+import android.net.Uri;
import android.support.v4.app.FragmentActivity;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.operations.results.ExportResult;
-import org.sufficientlysecure.keychain.service.KeychainIntentService;
-import org.sufficientlysecure.keychain.service.ServiceProgressHandler;
-import org.sufficientlysecure.keychain.ui.dialog.ProgressDialogFragment;
+import org.sufficientlysecure.keychain.service.ExportKeyringParcel;
+import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper;
-import java.io.File;
-
-public class ExportHelper {
+public class ExportHelper
+ implements CryptoOperationHelper.Callback <ExportKeyringParcel, ExportResult> {
protected File mExportFile;
FragmentActivity mActivity;
+ private boolean mExportSecret;
+ private long[] mMasterKeyIds;
+
public ExportHelper(FragmentActivity activity) {
super();
this.mActivity = activity;
}
- /**
- * Show dialog where to export keys
- */
- public void showExportKeysDialog(final long[] masterKeyIds, final File exportFile,
- final boolean showSecretCheckbox) {
+ /** Show dialog where to export keys */
+ public void showExportKeysDialog(final Long masterKeyId, final File exportFile,
+ final boolean exportSecret) {
mExportFile = exportFile;
- String title = null;
- if (masterKeyIds == null) {
+ String title;
+ if (masterKeyId == null) {
// export all keys
title = mActivity.getString(R.string.title_export_keys);
} else {
@@ -59,72 +58,67 @@ public class ExportHelper {
title = mActivity.getString(R.string.title_export_key);
}
- String message = mActivity.getString(R.string.specify_file_to_export_to);
- String checkMsg = showSecretCheckbox ?
- mActivity.getString(R.string.also_export_secret_keys) : null;
+ String message;
+ if (exportSecret) {
+ message = mActivity.getString(masterKeyId == null
+ ? R.string.specify_backup_dest_secret
+ : R.string.specify_backup_dest_secret_single);
+ } else {
+ message = mActivity.getString(masterKeyId == null
+ ? R.string.specify_backup_dest
+ : 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;
- exportKeys(masterKeyIds, checked);
+ exportKeys(masterKeyId == null ? null : new long[] { masterKeyId }, exportSecret);
}
- }, mActivity.getSupportFragmentManager() ,title, message, exportFile, checkMsg);
+ }, mActivity.getSupportFragmentManager(), title, message, exportFile, null);
}
+ // TODO: If ExportHelper requires pending data (see CryptoOPerationHelper), activities using
+ // TODO: this class should be able to call mExportOpHelper.handleActivity
+
/**
* Export keys
*/
public void exportKeys(long[] masterKeyIds, boolean exportSecret) {
Log.d(Constants.TAG, "exportKeys started");
+ mExportSecret = exportSecret;
+ mMasterKeyIds = masterKeyIds; // if masterKeyIds is null it means export all
- // Send all information needed to service to export key in other thread
- final Intent intent = new Intent(mActivity, KeychainIntentService.class);
-
- intent.setAction(KeychainIntentService.ACTION_EXPORT_KEYRING);
-
- // fill values for this action
- Bundle data = new Bundle();
-
- data.putString(KeychainIntentService.EXPORT_FILENAME, mExportFile.getAbsolutePath());
- data.putBoolean(KeychainIntentService.EXPORT_SECRET, exportSecret);
-
- if (masterKeyIds == null) {
- data.putBoolean(KeychainIntentService.EXPORT_ALL, true);
- } else {
- data.putLongArray(KeychainIntentService.EXPORT_KEY_RING_MASTER_KEY_ID, masterKeyIds);
- }
-
- intent.putExtra(KeychainIntentService.EXTRA_DATA, data);
-
- // Message is received after exporting is done in KeychainIntentService
- ServiceProgressHandler exportHandler = new ServiceProgressHandler(mActivity,
- mActivity.getString(R.string.progress_exporting),
- ProgressDialog.STYLE_HORIZONTAL,
- ProgressDialogFragment.ServiceType.KEYCHAIN_INTENT) {
- public void handleMessage(Message message) {
- // handle messages by standard KeychainIntentServiceHandler first
- super.handleMessage(message);
+ CryptoOperationHelper<ExportKeyringParcel, ExportResult> exportOpHelper =
+ new CryptoOperationHelper<>(1, mActivity, this, R.string.progress_exporting);
+ exportOpHelper.cryptoOperation();
+ }
- if (message.arg1 == MessageStatus.OKAY.ordinal()) {
- // get returned data bundle
- Bundle data = message.getData();
+ @Override
+ public ExportKeyringParcel createOperationInput() {
+ return new ExportKeyringParcel(mMasterKeyIds, mExportSecret, mExportFile.getAbsolutePath());
+ }
- ExportResult result = data.getParcelable(ExportResult.EXTRA_RESULT);
- result.createNotify(mActivity).show();
- }
- }
- };
+ @Override
+ 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();
+ }
- // Create a new Messenger for the communication back
- Messenger messenger = new Messenger(exportHandler);
- intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger);
+ @Override
+ public void onCryptoOperationCancelled() {
- // show progress dialog
- exportHandler.showProgressDialog(mActivity);
+ }
- // start service with intent
- mActivity.startService(intent);
+ @Override
+ public void onCryptoOperationError(ExportResult result) {
+ result.createNotify(mActivity).show();
}
+ @Override
+ public boolean onCryptoSetProgress(String msg, int progress, int max) {
+ return false;
+ }
}
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 677acb1b8..9fb362412 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/FileHelper.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/FileHelper.java
@@ -20,6 +20,7 @@ package org.sufficientlysecure.keychain.util;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.ActivityNotFoundException;
+import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
@@ -27,55 +28,116 @@ 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;
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.ByteArrayOutputStream;
import java.io.File;
+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
- *
- * @param file
- * @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 fragment
- * @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);
@@ -86,11 +148,34 @@ public class FileHelper {
Toast.makeText(fragment.getActivity(), R.string.no_filemanager_installed,
Toast.LENGTH_SHORT).show();
}
+
}
- public static void saveFile(final FileDialogCallback callback, final FragmentManager fragmentManager,
- final String title, final String message, final File defaultFile,
- final String checkMsg) {
+ /** 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 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
@@ -117,63 +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 fragment
- * @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 fragment
- * @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 {
@@ -234,7 +262,78 @@ public class FileHelper {
return new DecimalFormat("#,##0.#").format(size / Math.pow(1024, digitGroups)) + " " + units[digitGroups];
}
- public static interface FileDialogCallback {
- public void onFileSelected(File file, boolean checked);
+ public static String readTextFromUri(Context context, Uri outputUri, String charset)
+ throws IOException {
+
+ byte[] decryptedMessage;
+ {
+ InputStream in = context.getContentResolver().openInputStream(outputUri);
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ byte[] buf = new byte[256];
+ int read;
+ while ( (read = in.read(buf)) > 0) {
+ out.write(buf, 0, read);
+ }
+ in.close();
+ out.close();
+ decryptedMessage = out.toByteArray();
+ }
+
+ String plaintext;
+ if (charset != null) {
+ try {
+ plaintext = new String(decryptedMessage, charset);
+ } catch (UnsupportedEncodingException e) {
+ // if we can't decode properly, just fall back to utf-8
+ plaintext = new String(decryptedMessage);
+ }
+ } else {
+ plaintext = new String(decryptedMessage);
+ }
+
+ return plaintext;
+
+ }
+
+ public static void copyUriData(Context context, Uri fromUri, Uri toUri) throws IOException {
+ BufferedInputStream bis = null;
+ BufferedOutputStream bos = null;
+
+ try {
+ ContentResolver resolver = context.getContentResolver();
+ bis = new BufferedInputStream(resolver.openInputStream(fromUri));
+ bos = new BufferedOutputStream(resolver.openOutputStream(toUri));
+ byte[] buf = new byte[1024];
+ int len;
+ while ( (len = bis.read(buf)) > 0) {
+ bos.write(buf, 0, len);
+ }
+ } finally {
+ try {
+ if (bis != null) {
+ bis.close();
+ }
+ if (bos != null) {
+ bos.close();
+ }
+ } catch (IOException e) {
+ // ignore, it's just stream closin'
+ }
+ }
+ }
+
+ /** 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/KeyUpdateHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/KeyUpdateHelper.java
index 3bbd86d6a..8a614d64d 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/KeyUpdateHelper.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/KeyUpdateHelper.java
@@ -51,15 +51,15 @@ public class KeyUpdateHelper {
}
// Start the service and update the keys
- Intent importIntent = new Intent(mContext, KeychainIntentService.class);
- importIntent.setAction(KeychainIntentService.ACTION_DOWNLOAD_AND_IMPORT_KEYS);
+ Intent importIntent = new Intent(mContext, KeychainService.class);
+ importIntent.setAction(KeychainService.ACTION_DOWNLOAD_AND_IMPORT_KEYS);
Bundle importData = new Bundle();
- importData.putParcelableArrayList(KeychainIntentService.DOWNLOAD_KEY_LIST,
+ importData.putParcelableArrayList(KeychainService.DOWNLOAD_KEY_LIST,
new ArrayList<ImportKeysListEntry>(keys));
- importIntent.putExtra(KeychainIntentService.EXTRA_SERVICE_INTENT, importData);
+ importIntent.putExtra(KeychainService.EXTRA_SERVICE_INTENT, importData);
- importIntent.putExtra(KeychainIntentService.EXTRA_MESSENGER, new Messenger(mHandler));
+ importIntent.putExtra(KeychainService.EXTRA_MESSENGER, new Messenger(mHandler));
mContext.startService(importIntent);
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/OrientationUtils.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/OrientationUtils.java
new file mode 100644
index 000000000..43ed12429
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/OrientationUtils.java
@@ -0,0 +1,85 @@
+/*
+ * 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.util;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.content.res.Configuration;
+import android.view.Display;
+import android.view.Surface;
+import android.view.WindowManager;
+
+/**
+ * Static methods related to device orientation.
+ */
+public class OrientationUtils {
+
+ /**
+ * Locks the device window in landscape mode.
+ */
+ public static void lockOrientationLandscape(Activity activity) {
+ activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
+ }
+
+ /**
+ * Locks the device window in portrait mode.
+ */
+ public static void lockOrientationPortrait(Activity activity) {
+ activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
+ }
+
+ /**
+ * Locks the device window in actual screen mode.
+ */
+ public static void lockOrientation(Activity activity) {
+ Display display = ((WindowManager) activity.getSystemService(Context.WINDOW_SERVICE))
+ .getDefaultDisplay();
+ int rotation = display.getRotation();
+ int tempOrientation = activity.getResources().getConfiguration().orientation;
+ int orientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
+
+ switch (tempOrientation) {
+ case Configuration.ORIENTATION_LANDSCAPE: {
+ if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_90) {
+ orientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
+ } else {
+ orientation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
+ }
+ break;
+ }
+ case Configuration.ORIENTATION_PORTRAIT: {
+ if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_270) {
+ orientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
+ } else {
+ orientation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT;
+ }
+ break;
+ }
+ }
+ activity.setRequestedOrientation(orientation);
+ }
+
+ /**
+ * Unlocks the device window in user defined screen mode.
+ */
+ public static void unlockOrientation(Activity activity) {
+ activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_USER);
+ }
+
+} \ No newline at end of file
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ParcelableFileCache.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ParcelableFileCache.java
index 5a314ad0b..eabbf83b8 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ParcelableFileCache.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ParcelableFileCache.java
@@ -66,21 +66,24 @@ public class ParcelableFileCache<E extends Parcelable> {
File tempFile = new File(mContext.getCacheDir(), mFilename);
- DataOutputStream oos = new DataOutputStream(new FileOutputStream(tempFile));
- oos.writeInt(numEntries);
+ DataOutputStream oos = new DataOutputStream(new FileOutputStream(tempFile));
- while (it.hasNext()) {
- Parcel p = Parcel.obtain(); // creating empty parcel object
- p.writeParcelable(it.next(), 0); // saving bundle as parcel
- byte[] buf = p.marshall();
- oos.writeInt(buf.length);
- oos.write(buf);
- p.recycle();
+ try {
+ oos.writeInt(numEntries);
+
+ while (it.hasNext()) {
+ Parcel p = Parcel.obtain(); // creating empty parcel object
+ p.writeParcelable(it.next(), 0); // saving bundle as parcel
+ byte[] buf = p.marshall();
+ oos.writeInt(buf.length);
+ oos.write(buf);
+ p.recycle();
+ }
+ } finally {
+ oos.close();
}
- oos.close();
-
}
/**
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ParcelableHashMap.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ParcelableHashMap.java
new file mode 100644
index 000000000..fa4081acc
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ParcelableHashMap.java
@@ -0,0 +1,63 @@
+package org.sufficientlysecure.keychain.util;
+
+
+import java.util.HashMap;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.support.annotation.NonNull;
+
+import org.sufficientlysecure.keychain.KeychainApplication;
+
+
+public class ParcelableHashMap <K extends Parcelable, V extends Parcelable> implements Parcelable {
+
+ HashMap<K,V> mInner;
+
+ public ParcelableHashMap(HashMap<K,V> inner) {
+ mInner = inner;
+ }
+
+ protected ParcelableHashMap(@NonNull Parcel in) {
+ mInner = new HashMap<>();
+ ClassLoader loader = KeychainApplication.class.getClassLoader();
+
+ int num = in.readInt();
+ while (num-- > 0) {
+ K key = in.readParcelable(loader);
+ V val = in.readParcelable(loader);
+ mInner.put(key, val);
+ }
+ }
+
+ public HashMap<K,V> getMap() {
+ return mInner;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeInt(mInner.size());
+ for (HashMap.Entry<K,V> entry : mInner.entrySet()) {
+ parcel.writeParcelable(entry.getKey(), 0);
+ parcel.writeParcelable(entry.getValue(), 0);
+ }
+ }
+
+ public static final Creator<ParcelableHashMap> CREATOR = new Creator<ParcelableHashMap>() {
+ @Override
+ public ParcelableHashMap createFromParcel(Parcel in) {
+ return new ParcelableHashMap(in);
+ }
+
+ @Override
+ public ParcelableHashMap[] newArray(int size) {
+ return new ParcelableHashMap[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ParcelableProxy.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ParcelableProxy.java
new file mode 100644
index 000000000..7e788d04c
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ParcelableProxy.java
@@ -0,0 +1,91 @@
+/*
+ * 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.util;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.net.InetSocketAddress;
+import java.net.Proxy;
+
+/**
+ * used to simply transport java.net.Proxy objects created using InetSockets between services/activities
+ */
+public class ParcelableProxy implements Parcelable {
+ private String mProxyHost;
+ private int mProxyPort;
+ private Proxy.Type mProxyType;
+
+ public ParcelableProxy(String hostName, int port, Proxy.Type type) {
+ mProxyHost = hostName;
+
+ if (hostName == null) {
+ return; // represents a null proxy
+ }
+
+ mProxyPort = port;
+
+ mProxyType = type;
+ }
+
+ public static ParcelableProxy getForNoProxy() {
+ return new ParcelableProxy(null, -1, null);
+ }
+
+ public Proxy getProxy() {
+ if (mProxyHost == null) {
+ return null;
+ }
+ /*
+ * InetSocketAddress.createUnresolved so we can use this method even in the main thread
+ * (no network call)
+ */
+ return new Proxy(mProxyType, InetSocketAddress.createUnresolved(mProxyHost, mProxyPort));
+ }
+
+ protected ParcelableProxy(Parcel in) {
+ mProxyHost = in.readString();
+ mProxyPort = in.readInt();
+ mProxyType = (Proxy.Type) in.readSerializable();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(mProxyHost);
+ dest.writeInt(mProxyPort);
+ dest.writeSerializable(mProxyType);
+ }
+
+ @SuppressWarnings("unused")
+ public static final Parcelable.Creator<ParcelableProxy> CREATOR = new Parcelable.Creator<ParcelableProxy>() {
+ @Override
+ public ParcelableProxy createFromParcel(Parcel in) {
+ return new ParcelableProxy(in);
+ }
+
+ @Override
+ public ParcelableProxy[] newArray(int size) {
+ return new ParcelableProxy[size];
+ }
+ };
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Passphrase.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Passphrase.java
index 06efdde4d..fe42c7a2c 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Passphrase.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Passphrase.java
@@ -117,6 +117,13 @@ public class Passphrase implements Parcelable {
}
}
+ /**
+ * Creates a new String from the char[]. This is considered unsafe!
+ */
+ public String toStringUnsafe() {
+ return new String(mPassphrase);
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) {
@@ -127,11 +134,7 @@ public class Passphrase implements Parcelable {
}
Passphrase that = (Passphrase) o;
- if (!Arrays.equals(mPassphrase, that.mPassphrase)) {
- return false;
- }
-
- return true;
+ return Arrays.equals(mPassphrase, that.mPassphrase);
}
@Override
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 303687315..4ef215036 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Preferences.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Preferences.java
@@ -21,9 +21,13 @@ package org.sufficientlysecure.keychain.util;
import android.content.Context;
import android.content.SharedPreferences;
+import android.content.res.Resources;
+import android.preference.PreferenceManager;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.Constants.Pref;
+import org.sufficientlysecure.keychain.service.KeyserverSyncAdapterService;
+import java.net.Proxy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.ListIterator;
@@ -35,6 +39,10 @@ import java.util.Vector;
public class Preferences {
private static Preferences sPreferences;
private SharedPreferences mSharedPreferences;
+ private Resources mResources;
+
+ private static String PREF_FILE_NAME = "APG.main";
+ private static int PREF_FILE_MODE = Context.MODE_MULTI_PROCESS;
public static synchronized Preferences getPreferences(Context context) {
return getPreferences(context, false);
@@ -51,12 +59,18 @@ public class Preferences {
}
private Preferences(Context context) {
+ mResources = context.getResources();
updateSharedPreferences(context);
}
+ public static void setPreferenceManagerFileAndMode(PreferenceManager manager) {
+ manager.setSharedPreferencesName(PREF_FILE_NAME);
+ manager.setSharedPreferencesMode(PREF_FILE_MODE);
+ }
+
public void updateSharedPreferences(Context context) {
// multi-process safe preferences
- mSharedPreferences = context.getSharedPreferences("APG.main", Context.MODE_MULTI_PROCESS);
+ mSharedPreferences = context.getSharedPreferences(PREF_FILE_NAME, PREF_FILE_MODE);
}
public String getLanguage() {
@@ -138,6 +152,9 @@ public class Preferences {
public String[] getKeyServers() {
String rawData = mSharedPreferences.getString(Constants.Pref.KEY_SERVERS,
Constants.Defaults.KEY_SERVERS);
+ if (rawData.equals("")) {
+ return new String[0];
+ }
Vector<String> servers = new Vector<>();
String chunks[] = rawData.split(",");
for (String c : chunks) {
@@ -150,7 +167,8 @@ public class Preferences {
}
public String getPreferredKeyserver() {
- return getKeyServers()[0];
+ String[] keyservers = getKeyServers();
+ return keyservers.length == 0 ? null : keyservers[0];
}
public void setKeyServers(String[] value) {
@@ -182,6 +200,142 @@ public class Preferences {
editor.commit();
}
+ public void setFilesUseCompression(boolean compress) {
+ SharedPreferences.Editor editor = mSharedPreferences.edit();
+ editor.putBoolean(Pref.FILE_USE_COMPRESSION, compress);
+ editor.commit();
+ }
+
+ public boolean getFilesUseCompression() {
+ return mSharedPreferences.getBoolean(Pref.FILE_USE_COMPRESSION, true);
+ }
+
+ public void setTextUseCompression(boolean compress) {
+ SharedPreferences.Editor editor = mSharedPreferences.edit();
+ editor.putBoolean(Pref.TEXT_USE_COMPRESSION, compress);
+ editor.commit();
+ }
+
+ public boolean getTextUseCompression() {
+ return mSharedPreferences.getBoolean(Pref.TEXT_USE_COMPRESSION, true);
+ }
+
+ public String getTheme() {
+ return mSharedPreferences.getString(Pref.THEME, Pref.Theme.LIGHT);
+ }
+
+ public void setTheme(String value) {
+ SharedPreferences.Editor editor = mSharedPreferences.edit();
+ editor.putString(Constants.Pref.THEME, value);
+ editor.commit();
+ }
+
+ public void setUseArmor(boolean useArmor) {
+ SharedPreferences.Editor editor = mSharedPreferences.edit();
+ editor.putBoolean(Pref.USE_ARMOR, useArmor);
+ editor.commit();
+ }
+
+ public boolean getUseArmor() {
+ return mSharedPreferences.getBoolean(Pref.USE_ARMOR, false);
+ }
+
+ public void setEncryptFilenames(boolean encryptFilenames) {
+ SharedPreferences.Editor editor = mSharedPreferences.edit();
+ editor.putBoolean(Pref.ENCRYPT_FILENAMES, encryptFilenames);
+ editor.commit();
+ }
+
+ public boolean getEncryptFilenames() {
+ return mSharedPreferences.getBoolean(Pref.ENCRYPT_FILENAMES, true);
+ }
+
+ // proxy preference functions start here
+
+ public boolean getUseNormalProxy() {
+ return mSharedPreferences.getBoolean(Constants.Pref.USE_NORMAL_PROXY, false);
+ }
+
+ public boolean getUseTorProxy() {
+ return mSharedPreferences.getBoolean(Constants.Pref.USE_TOR_PROXY, false);
+ }
+
+ public String getProxyHost() {
+ return mSharedPreferences.getString(Constants.Pref.PROXY_HOST, null);
+ }
+
+ /**
+ * we store port as String for easy interfacing with EditTextPreference, but return it as an integer
+ *
+ * @return port number of proxy
+ */
+ public int getProxyPort() {
+ return Integer.parseInt(mSharedPreferences.getString(Pref.PROXY_PORT, "-1"));
+ }
+
+ /**
+ * we store port as String for easy interfacing with EditTextPreference, but return it as an integer
+ *
+ * @param port proxy port
+ */
+ public void setProxyPort(String port) {
+ SharedPreferences.Editor editor = mSharedPreferences.edit();
+ editor.putString(Pref.PROXY_PORT, port);
+ editor.commit();
+ }
+
+ public Proxy.Type getProxyType() {
+ final String typeHttp = Pref.ProxyType.TYPE_HTTP;
+ final String typeSocks = Pref.ProxyType.TYPE_SOCKS;
+
+ String type = mSharedPreferences.getString(Pref.PROXY_TYPE, typeHttp);
+
+ 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;
+ }
+ }
+
+ public ProxyPrefs getProxyPrefs() {
+ boolean useTor = getUseTorProxy();
+ boolean useNormalProxy = getUseNormalProxy();
+
+ if (useTor) {
+ return new ProxyPrefs(true, false, Constants.Orbot.PROXY_HOST, Constants.Orbot.PROXY_PORT,
+ Constants.Orbot.PROXY_TYPE);
+ } else if (useNormalProxy) {
+ return new ProxyPrefs(false, true, getProxyHost(), getProxyPort(), getProxyType());
+ } else {
+ return new ProxyPrefs(false, false, null, -1, null);
+ }
+ }
+
+ public static class ProxyPrefs {
+ public final ParcelableProxy parcelableProxy;
+ public final boolean torEnabled;
+ public final boolean normalPorxyEnabled;
+
+ /**
+ * torEnabled and normalProxyEnabled are not expected to both be true
+ *
+ * @param torEnabled if Tor is to be used
+ * @param normalPorxyEnabled if user-specified proxy is to be used
+ */
+ public ProxyPrefs(boolean torEnabled, boolean normalPorxyEnabled, String hostName, int port, Proxy.Type type) {
+ this.torEnabled = torEnabled;
+ this.normalPorxyEnabled = normalPorxyEnabled;
+ if (!torEnabled && !normalPorxyEnabled) this.parcelableProxy = new ParcelableProxy(null, -1, null);
+ else this.parcelableProxy = new ParcelableProxy(hostName, port, type);
+ }
+ }
+
+ // cloud prefs
+
public CloudSearchPrefs getCloudSearchPrefs() {
return new CloudSearchPrefs(mSharedPreferences.getBoolean(Pref.SEARCH_KEYSERVER, true),
mSharedPreferences.getBoolean(Pref.SEARCH_KEYBASE, true),
@@ -205,7 +359,39 @@ public class Preferences {
}
}
- public void updatePreferences() {
+ // 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)) {
@@ -239,6 +425,14 @@ public class Preferences {
}
// fall through
case 4: {
+ setTheme(Constants.Pref.Theme.DEFAULT);
+ }
+ // fall through
+ case 5: {
+ KeyserverSyncAdapterService.enableKeyserverSync(context);
+ }
+ // fall through
+ case 6: {
}
}
@@ -248,4 +442,9 @@ public class Preferences {
.commit();
}
}
+
+ public void clear() {
+ mSharedPreferences.edit().clear().commit();
+ }
+
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ShareHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ShareHelper.java
index 120b84a3b..0297d149c 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ShareHelper.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ShareHelper.java
@@ -91,6 +91,7 @@ public class ShareHelper {
// Create chooser with only one Intent in it
Intent chooserIntent = Intent.createChooser(targetedShareIntents.remove(targetedShareIntents.size() - 1), title);
// append all other Intents
+ // TODO this line looks wrong?!
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, targetedShareIntents.toArray(new Parcelable[]{}));
return chooserIntent;
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/TlsHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/TlsHelper.java
index 4ff14e3bb..d1d1ada2a 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/TlsHelper.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/TlsHelper.java
@@ -19,6 +19,7 @@ package org.sufficientlysecure.keychain.util;
import android.content.res.AssetManager;
+import com.squareup.okhttp.OkHttpClient;
import org.sufficientlysecure.keychain.Constants;
import java.io.ByteArrayInputStream;
@@ -26,7 +27,6 @@ import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
-import java.net.URLConnection;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
@@ -61,7 +61,7 @@ public class TlsHelper {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int reads = is.read();
- while(reads != -1){
+ while (reads != -1) {
baos.write(reads);
reads = is.read();
}
@@ -74,15 +74,56 @@ public class TlsHelper {
}
}
- public static URLConnection openConnection(URL url) throws IOException, TlsHelperException {
+ public static void pinCertificateIfNecessary(OkHttpClient client, URL url) throws TlsHelperException, IOException {
if (url.getProtocol().equals("https")) {
for (String domain : sStaticCA.keySet()) {
if (url.getHost().endsWith(domain)) {
- return openCAConnection(sStaticCA.get(domain), url);
+ pinCertificate(sStaticCA.get(domain), client);
}
}
}
- return url.openConnection();
+ }
+
+ /**
+ * Modifies the client to accept only requests with a given certificate. Applies to all URLs requested by the
+ * client.
+ * Therefore a client that is pinned this way should be used to only make requests to URLs with passed certificate.
+ * TODO: Refactor - More like SSH StrictHostKeyChecking than pinning?
+ *
+ * @param certificate certificate to pin
+ * @param client OkHttpClient to enforce pinning on
+ * @throws TlsHelperException
+ * @throws IOException
+ */
+ private static void pinCertificate(byte[] certificate, OkHttpClient client)
+ throws TlsHelperException, IOException {
+ // We don't use OkHttp's CertificatePinner since it depends on a TrustManager to verify it too. Refer to
+ // note at end of description: http://square.github.io/okhttp/javadoc/com/squareup/okhttp/CertificatePinner.html
+ // Creating our own TrustManager that trusts only our certificate eliminates the need for certificate pinning
+ try {
+ // Load CA
+ CertificateFactory cf = CertificateFactory.getInstance("X.509");
+ Certificate ca = cf.generateCertificate(new ByteArrayInputStream(certificate));
+
+ // Create a KeyStore containing our trusted CAs
+ String keyStoreType = KeyStore.getDefaultType();
+ KeyStore keyStore = KeyStore.getInstance(keyStoreType);
+ keyStore.load(null, null);
+ keyStore.setCertificateEntry("ca", ca);
+
+ // Create a TrustManager that trusts the CAs in our KeyStore
+ String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
+ TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
+ tmf.init(keyStore);
+
+ // Create an SSLContext that uses our TrustManager
+ SSLContext context = SSLContext.getInstance("TLS");
+ context.init(null, tmf.getTrustManagers(), null);
+
+ client.setSslSocketFactory(context.getSocketFactory());
+ } catch (CertificateException | KeyStoreException | KeyManagementException | NoSuchAlgorithmException e) {
+ throw new TlsHelperException(e);
+ }
}
/**
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
new file mode 100644
index 000000000..d85ad9128
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/orbot/OrbotHelper.java
@@ -0,0 +1,463 @@
+/* This is the license for Orlib, a free software project to
+ provide anonymity on the Internet from a Google Android smartphone.
+
+ For more information about Orlib, see https://guardianproject.info/
+
+ If you got this file as a part of a larger bundle, there may be other
+ license terms that you should be aware of.
+ ===============================================================================
+ Orlib is distributed under this license (aka the 3-clause BSD license)
+
+ Copyright (c) 2009-2010, Nathan Freitas, The Guardian Project
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are
+ met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following disclaimer
+ in the documentation and/or other materials provided with the
+ distribution.
+
+ * Neither the names of the copyright owners nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+ *****
+ Orlib contains a binary distribution of the JSocks library:
+ http://code.google.com/p/jsocks-mirror/
+ which is licensed under the GNU Lesser General Public License:
+ http://www.gnu.org/licenses/lgpl.html
+
+ *****
+*/
+
+package org.sufficientlysecure.keychain.util.orbot;
+
+import android.app.Activity;
+import android.app.ProgressDialog;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Message;
+import android.os.Messenger;
+import android.support.v4.app.DialogFragment;
+import android.support.v4.app.FragmentActivity;
+import android.text.TextUtils;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.ui.dialog.SupportInstallDialogFragment;
+import org.sufficientlysecure.keychain.ui.dialog.OrbotStartDialogFragment;
+import org.sufficientlysecure.keychain.ui.dialog.PreferenceInstallDialogFragment;
+import org.sufficientlysecure.keychain.ui.util.ThemeChanger;
+import org.sufficientlysecure.keychain.util.Log;
+import org.sufficientlysecure.keychain.util.Preferences;
+
+import java.util.List;
+
+/**
+ * This class is taken from the NetCipher library: https://github.com/guardianproject/NetCipher/
+ */
+public class OrbotHelper {
+
+ public interface DialogActions {
+ void onOrbotStarted();
+
+ void onNeutralButton();
+
+ void onCancel();
+ }
+
+ private final static int REQUEST_CODE_STATUS = 100;
+
+ public final static String ORBOT_PACKAGE_NAME = "org.torproject.android";
+ public final static String ORBOT_MARKET_URI = "market://details?id=" + ORBOT_PACKAGE_NAME;
+ public final static String ORBOT_FDROID_URI = "https://f-droid.org/repository/browse/?fdid="
+ + ORBOT_PACKAGE_NAME;
+ public final static String ORBOT_PLAY_URI = "https://play.google.com/store/apps/details?id="
+ + ORBOT_PACKAGE_NAME;
+
+ /**
+ * A request to Orbot to transparently start Tor services
+ */
+ public final static String ACTION_START = "org.torproject.android.intent.action.START";
+ /**
+ * {@link Intent} send by Orbot with {@code ON/OFF/STARTING/STOPPING} status
+ */
+ public final static String ACTION_STATUS = "org.torproject.android.intent.action.STATUS";
+ /**
+ * {@code String} that contains a status constant: {@link #STATUS_ON},
+ * {@link #STATUS_OFF}, {@link #STATUS_STARTING}, or
+ * {@link #STATUS_STOPPING}
+ */
+ public final static String EXTRA_STATUS = "org.torproject.android.intent.extra.STATUS";
+ /**
+ * A {@link String} {@code packageName} for Orbot to direct its status reply
+ * to, used in {@link #ACTION_START} {@link Intent}s sent to Orbot
+ */
+ public final static String EXTRA_PACKAGE_NAME = "org.torproject.android.intent.extra.PACKAGE_NAME";
+
+ /**
+ * All tor-related services and daemons are stopped
+ */
+ @SuppressWarnings("unused") // we might use this later, sent by Orbot
+ public final static String STATUS_OFF = "OFF";
+ /**
+ * All tor-related services and daemons have completed starting
+ */
+ public final static String STATUS_ON = "ON";
+ @SuppressWarnings("unused") // we might use this later, sent by Orbot
+ public final static String STATUS_STARTING = "STARTING";
+ @SuppressWarnings("unused") // we might use this later, sent by Orbot
+ public final static String STATUS_STOPPING = "STOPPING";
+ /**
+ * The user has disabled the ability for background starts triggered by
+ * apps. Fallback to the old Intent that brings up Orbot.
+ */
+ public final static String STATUS_STARTS_DISABLED = "STARTS_DISABLED";
+
+ public final static String ACTION_START_TOR = "org.torproject.android.START_TOR";
+ /**
+ * request code used to start tor
+ */
+ public final static int START_TOR_RESULT = 0x9234;
+
+ private final static String FDROID_PACKAGE_NAME = "org.fdroid.fdroid";
+ private final static String PLAY_PACKAGE_NAME = "com.android.vending";
+
+ private OrbotHelper() {
+ // only static utility methods, do not instantiate
+ }
+
+ public static boolean isOrbotRunning(Context context) {
+ int procId = TorServiceUtils.findProcessId(context);
+
+ return (procId != -1);
+ }
+
+ public static boolean isOrbotInstalled(Context context) {
+ return isAppInstalled(context, ORBOT_PACKAGE_NAME);
+ }
+
+ private static boolean isAppInstalled(Context context, String uri) {
+ try {
+ PackageManager pm = context.getPackageManager();
+ pm.getPackageInfo(uri, PackageManager.GET_ACTIVITIES);
+ return true;
+ } catch (PackageManager.NameNotFoundException e) {
+ return false;
+ }
+ }
+
+ /**
+ * First, checks whether Orbot is installed, then checks whether Orbot is
+ * running. If Orbot is installed and not running, then an {@link Intent} is
+ * sent to request Orbot to start, which will show the main Orbot screen.
+ * The result will be returned in
+ * {@link Activity#onActivityResult(int requestCode, int resultCode, Intent data)}
+ * with a {@code requestCode} of {@code START_TOR_RESULT}
+ *
+ * @param activity the {@link Activity} that gets the
+ * {@code START_TOR_RESULT} result
+ * @return whether the start request was sent to Orbot
+ */
+ public static boolean requestShowOrbotStart(Activity activity) {
+ if (OrbotHelper.isOrbotInstalled(activity)) {
+ if (!OrbotHelper.isOrbotRunning(activity)) {
+ Intent intent = getShowOrbotStartIntent();
+ activity.startActivityForResult(intent, START_TOR_RESULT);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public static Intent getShowOrbotStartIntent() {
+ Intent intent = new Intent(ACTION_START_TOR);
+ intent.setPackage(ORBOT_PACKAGE_NAME);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ return intent;
+ }
+
+ /**
+ * First, checks whether Orbot is installed. If Orbot is installed, then a
+ * broadcast {@link Intent} is sent to request Orbot to start transparently
+ * in the background. When Orbot receives this {@code Intent}, it will
+ * immediately reply to this all with its status via an
+ * {@link #ACTION_STATUS} {@code Intent} that is broadcast to the
+ * {@code packageName} of the provided {@link Context} (i.e.
+ * {@link Context#getPackageName()}.
+ *
+ * @param context the app {@link Context} will receive the reply
+ * @return whether the start request was sent to Orbot
+ */
+ public static boolean requestStartTor(Context context) {
+ if (OrbotHelper.isOrbotInstalled(context)) {
+ Log.i("OrbotHelper", "requestStartTor " + context.getPackageName());
+ Intent intent = getOrbotStartIntent();
+ intent.putExtra(EXTRA_PACKAGE_NAME, context.getPackageName());
+ context.sendBroadcast(intent);
+ return true;
+ }
+ return false;
+ }
+
+ public static Intent getOrbotStartIntent() {
+ Intent intent = new Intent(ACTION_START);
+ intent.setPackage(ORBOT_PACKAGE_NAME);
+ return intent;
+ }
+
+ public static Intent getOrbotInstallIntent(Context context) {
+ final Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setData(Uri.parse(ORBOT_MARKET_URI));
+
+ PackageManager pm = context.getPackageManager();
+ List<ResolveInfo> resInfos = pm.queryIntentActivities(intent, 0);
+
+ String foundPackageName = null;
+ for (ResolveInfo r : resInfos) {
+ Log.i("OrbotHelper", "market: " + r.activityInfo.packageName);
+ if (TextUtils.equals(r.activityInfo.packageName, FDROID_PACKAGE_NAME)
+ || TextUtils.equals(r.activityInfo.packageName, PLAY_PACKAGE_NAME)) {
+ foundPackageName = r.activityInfo.packageName;
+ break;
+ }
+ }
+
+ if (foundPackageName == null) {
+ intent.setData(Uri.parse(ORBOT_FDROID_URI));
+ } else {
+ intent.setPackage(foundPackageName);
+ }
+ return intent;
+ }
+
+ /**
+ * hack to get around the fact that PreferenceActivity still supports only android.app.DialogFragment
+ */
+ public static android.app.DialogFragment getPreferenceInstallDialogFragment() {
+ return PreferenceInstallDialogFragment.newInstance(R.string.orbot_install_dialog_title,
+ R.string.orbot_install_dialog_content, ORBOT_PACKAGE_NAME);
+ }
+
+ public static DialogFragment getInstallDialogFragmentWithThirdButton(Messenger messenger, int middleButton) {
+ return SupportInstallDialogFragment.newInstance(messenger, R.string.orbot_install_dialog_title,
+ R.string.orbot_install_dialog_content, ORBOT_PACKAGE_NAME, middleButton, true);
+ }
+
+ public static DialogFragment getOrbotStartDialogFragment(Messenger messenger, int middleButton) {
+ return OrbotStartDialogFragment.newInstance(messenger, R.string.orbot_start_dialog_title, R.string
+ .orbot_start_dialog_content,
+ middleButton);
+ }
+
+ /**
+ * checks preferences to see if Orbot is required, and if yes, if it is installed and running
+ *
+ * @param context used to retrieve preferences
+ * @return false if Tor is selected proxy and Orbot is not installed or running, true
+ * otherwise
+ */
+ public static boolean isOrbotInRequiredState(Context context) {
+ Preferences.ProxyPrefs proxyPrefs = Preferences.getPreferences(context).getProxyPrefs();
+ if (!proxyPrefs.torEnabled) {
+ return true;
+ } else if (!OrbotHelper.isOrbotInstalled(context) || !OrbotHelper.isOrbotRunning(context)) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * checks if Tor is enabled and if it is, that Orbot is installed and running. Generates appropriate dialogs.
+ *
+ * @param middleButton resourceId of string to display as the middle button of install and enable dialogs
+ * @param proxyPrefs proxy preferences used to determine if Tor is required to be started
+ * @return true if Tor is not enabled or Tor is enabled and Orbot is installed and running, else false
+ */
+ public static boolean putOrbotInRequiredState(final int middleButton,
+ final DialogActions dialogActions,
+ Preferences.ProxyPrefs proxyPrefs,
+ final FragmentActivity fragmentActivity) {
+
+ if (!proxyPrefs.torEnabled) {
+ return true;
+ }
+
+ if (!OrbotHelper.isOrbotInstalled(fragmentActivity)) {
+ Handler ignoreTorHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case SupportInstallDialogFragment.MESSAGE_MIDDLE_CLICKED:
+ dialogActions.onNeutralButton();
+ break;
+ case SupportInstallDialogFragment.MESSAGE_DIALOG_DISMISSED:
+ // both install and cancel buttons mean we don't go ahead with an
+ // operation, so it's okay to cancel
+ dialogActions.onCancel();
+ break;
+ }
+ }
+ };
+
+ OrbotHelper.getInstallDialogFragmentWithThirdButton(
+ new Messenger(ignoreTorHandler),
+ middleButton
+ ).show(fragmentActivity.getSupportFragmentManager(), "OrbotHelperOrbotInstallDialog");
+
+ return false;
+ } else if (!OrbotHelper.isOrbotRunning(fragmentActivity)) {
+
+ final Handler dialogHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case OrbotStartDialogFragment.MESSAGE_MIDDLE_BUTTON:
+ dialogActions.onNeutralButton();
+ break;
+ case OrbotStartDialogFragment.MESSAGE_DIALOG_CANCELLED:
+ dialogActions.onCancel();
+ break;
+ case OrbotStartDialogFragment.MESSAGE_ORBOT_STARTED:
+ dialogActions.onOrbotStarted();
+ break;
+ }
+ }
+ };
+
+ new SilentStartManager() {
+
+ @Override
+ protected void onOrbotStarted() {
+ dialogActions.onOrbotStarted();
+ }
+
+ @Override
+ protected void onSilentStartDisabled() {
+ getOrbotStartDialogFragment(new Messenger(dialogHandler), middleButton)
+ .show(fragmentActivity.getSupportFragmentManager(),
+ "OrbotHelperOrbotStartDialog");
+ }
+ }.startOrbotAndListen(fragmentActivity, true);
+
+ return false;
+ } else {
+ return true;
+ }
+ }
+
+ public static boolean putOrbotInRequiredState(DialogActions dialogActions,
+ FragmentActivity fragmentActivity) {
+ return putOrbotInRequiredState(R.string.orbot_ignore_tor,
+ dialogActions,
+ Preferences.getPreferences(fragmentActivity).getProxyPrefs(),
+ fragmentActivity);
+ }
+
+ /**
+ * will attempt a silent start, which if disabled will fallback to the
+ * {@link #requestShowOrbotStart(Activity) requestShowOrbotStart} method, which returns the
+ * result in {@link Activity#onActivityResult(int requestCode, int resultCode, Intent data)}
+ * with a {@code requestCode} of {@code START_TOR_RESULT}, which will have to be implemented by
+ * activities wishing to respond to a change in Orbot state.
+ */
+ public static void bestPossibleOrbotStart(final DialogActions dialogActions,
+ final Activity activity,
+ boolean showProgress) {
+ new SilentStartManager() {
+
+ @Override
+ protected void onOrbotStarted() {
+ dialogActions.onOrbotStarted();
+ }
+
+ @Override
+ protected void onSilentStartDisabled() {
+ requestShowOrbotStart(activity);
+ }
+ }.startOrbotAndListen(activity, showProgress);
+ }
+
+ /**
+ * base class for listening to silent orbot starts. Also handles display of progress dialog.
+ */
+ public static abstract class SilentStartManager {
+
+ private ProgressDialog mProgressDialog;
+
+ public void startOrbotAndListen(final Context context, final boolean showProgress) {
+ Log.d(Constants.TAG, "starting orbot listener");
+ if (showProgress) {
+ showProgressDialog(context);
+ }
+
+ final BroadcastReceiver receiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ switch (intent.getStringExtra(OrbotHelper.EXTRA_STATUS)) {
+ case OrbotHelper.STATUS_ON:
+ context.unregisterReceiver(this);
+ // generally Orbot starts working a little after this status is received
+ new Handler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ if (showProgress) {
+ mProgressDialog.dismiss();
+ }
+ onOrbotStarted();
+ }
+ }, 1000);
+ break;
+ case OrbotHelper.STATUS_STARTS_DISABLED:
+ context.unregisterReceiver(this);
+ if (showProgress) {
+ mProgressDialog.dismiss();
+ }
+ onSilentStartDisabled();
+ break;
+
+ }
+ Log.d(Constants.TAG, "Orbot silent start broadcast: " +
+ intent.getStringExtra(OrbotHelper.EXTRA_STATUS));
+ }
+ };
+ context.registerReceiver(receiver, new IntentFilter(OrbotHelper.ACTION_STATUS));
+
+ 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();
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/orbot/TorServiceUtils.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/orbot/TorServiceUtils.java
new file mode 100644
index 000000000..2638f8cd5
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/orbot/TorServiceUtils.java
@@ -0,0 +1,156 @@
+/* This is the license for Orlib, a free software project to
+ provide anonymity on the Internet from a Google Android smartphone.
+
+ For more information about Orlib, see https://guardianproject.info/
+
+ If you got this file as a part of a larger bundle, there may be other
+ license terms that you should be aware of.
+ ===============================================================================
+ Orlib is distributed under this license (aka the 3-clause BSD license)
+
+ Copyright (c) 2009-2010, Nathan Freitas, The Guardian Project
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are
+ met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following disclaimer
+ in the documentation and/or other materials provided with the
+ distribution.
+
+ * Neither the names of the copyright owners nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+ *****
+ Orlib contains a binary distribution of the JSocks library:
+ http://code.google.com/p/jsocks-mirror/
+ which is licensed under the GNU Lesser General Public License:
+ http://www.gnu.org/licenses/lgpl.html
+
+ *****
+*/
+
+package org.sufficientlysecure.keychain.util.orbot;
+
+import android.content.Context;
+
+import org.sufficientlysecure.keychain.util.Log;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.InputStreamReader;
+import java.net.URLEncoder;
+import java.util.StringTokenizer;
+
+/**
+ * This class is taken from the NetCipher library: https://github.com/guardianproject/NetCipher/
+ */
+public class TorServiceUtils {
+
+ private final static String TAG = "TorUtils";
+
+ public final static String SHELL_CMD_PS = "ps";
+ public final static String SHELL_CMD_PIDOF = "pidof";
+
+ public static int findProcessId(Context context) {
+ String dataPath = context.getFilesDir().getParentFile().getParentFile().getAbsolutePath();
+ String command = dataPath + "/" + OrbotHelper.ORBOT_PACKAGE_NAME + "/app_bin/tor";
+ int procId = -1;
+
+ try {
+ procId = findProcessIdWithPidOf(command);
+
+ if (procId == -1)
+ procId = findProcessIdWithPS(command);
+ } catch (Exception e) {
+ try {
+ procId = findProcessIdWithPS(command);
+ } catch (Exception e2) {
+ Log.e(TAG, "Unable to get proc id for command: " + URLEncoder.encode(command), e2);
+ }
+ }
+
+ return procId;
+ }
+
+ // use 'pidof' command
+ public static int findProcessIdWithPidOf(String command) throws Exception {
+
+ int procId = -1;
+
+ Runtime r = Runtime.getRuntime();
+
+ Process procPs;
+
+ String baseName = new File(command).getName();
+ // fix contributed my mikos on 2010.12.10
+ procPs = r.exec(new String[]{
+ SHELL_CMD_PIDOF, baseName
+ });
+ // procPs = r.exec(SHELL_CMD_PIDOF);
+
+ BufferedReader reader = new BufferedReader(new InputStreamReader(procPs.getInputStream()));
+ String line;
+
+ while ((line = reader.readLine()) != null) {
+
+ try {
+ // this line should just be the process id
+ procId = Integer.parseInt(line.trim());
+ break;
+ } catch (NumberFormatException e) {
+ Log.e("TorServiceUtils", "unable to parse process pid: " + line, e);
+ }
+ }
+
+ return procId;
+
+ }
+
+ // use 'ps' command
+ public static int findProcessIdWithPS(String command) throws Exception {
+
+ int procId = -1;
+
+ Runtime r = Runtime.getRuntime();
+
+ Process procPs;
+
+ procPs = r.exec(SHELL_CMD_PS);
+
+ BufferedReader reader = new BufferedReader(new InputStreamReader(procPs.getInputStream()));
+ String line;
+
+ while ((line = reader.readLine()) != null) {
+ if (line.contains(' ' + command)) {
+
+ StringTokenizer st = new StringTokenizer(line, " ");
+ st.nextToken(); // proc owner
+
+ procId = Integer.parseInt(st.nextToken().trim());
+
+ break;
+ }
+ }
+
+ return procId;
+
+ }
+} \ No newline at end of file