diff options
Diffstat (limited to 'OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util')
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 |