From 37864a9d42436081eb6132dc0c8b1e0868a15272 Mon Sep 17 00:00:00 2001 From: Adithya Abraham Philip Date: Thu, 6 Aug 2015 01:59:11 +0530 Subject: updated OrbotHelper with silent start --- .../keychain/util/ParcelableProxy.java | 7 +- .../keychain/util/orbot/OrbotHelper.java | 339 ++++++++++++++++----- .../keychain/util/orbot/TorServiceUtils.java | 26 +- 3 files changed, 293 insertions(+), 79 deletions(-) (limited to 'OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util') diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ParcelableProxy.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ParcelableProxy.java index b00b03ec7..7e788d04c 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ParcelableProxy.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ParcelableProxy.java @@ -51,8 +51,11 @@ public class ParcelableProxy implements Parcelable { if (mProxyHost == null) { return null; } - - return new Proxy(mProxyType, new InetSocketAddress(mProxyHost, mProxyPort)); + /* + * 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) { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/orbot/OrbotHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/orbot/OrbotHelper.java index 9eb92f92f..d5ee75f9b 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/orbot/OrbotHelper.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/orbot/OrbotHelper.java @@ -49,66 +49,216 @@ 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 TOR_BIN_PATH = "/data/data/org.torproject.android/app_bin/tor"; + 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; - public static boolean isOrbotRunning() { - int procId = TorServiceUtils.findProcessId(TOR_BIN_PATH); + 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(ORBOT_PACKAGE_NAME, context); + return isAppInstalled(context, ORBOT_PACKAGE_NAME); } - private static boolean isAppInstalled(String uri, Context context) { - PackageManager pm = context.getPackageManager(); - - boolean installed; + private static boolean isAppInstalled(Context context, String uri) { try { + PackageManager pm = context.getPackageManager(); pm.getPackageInfo(uri, PackageManager.GET_ACTIVITIES); - installed = true; + return true; } catch (PackageManager.NameNotFoundException e) { - installed = false; + return false; } - return installed; } /** - * hack to get around the fact that PreferenceActivity still supports only android.app.DialogFragment + * 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} * - * @return + * @param activity the {@link Activity} that gets the + * {@code START_TOR_RESULT} result + * @return whether the start request was sent to Orbot */ - 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 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 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; } - public static DialogFragment getInstallDialogFragment() { - return SupportInstallDialogFragment.newInstance(R.string.orbot_install_dialog_title, + /** + * 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); } @@ -123,13 +273,6 @@ public class OrbotHelper { middleButton); } - public static Intent getOrbotStartIntent() { - Intent intent = new Intent(ACTION_START_TOR); - intent.setPackage(ORBOT_PACKAGE_NAME); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - return intent; - } - /** * checks preferences to see if Orbot is required, and if yes, if it is installed and running * @@ -141,26 +284,23 @@ public class OrbotHelper { Preferences.ProxyPrefs proxyPrefs = Preferences.getPreferences(context).getProxyPrefs(); if (!proxyPrefs.torEnabled) { return true; - } else if (!OrbotHelper.isOrbotInstalled(context) || !OrbotHelper.isOrbotRunning()) { + } 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 runnign. Generates appropriate dialogs. + * 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 middleButtonRunnable runnable to be executed if the user clicks on the middle button - * @param proxyPrefs - * @param fragmentActivity + * @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 Runnable middleButtonRunnable, - final Runnable dialogDismissRunnable, + final DialogActions dialogActions, Preferences.ProxyPrefs proxyPrefs, - FragmentActivity fragmentActivity) { + final FragmentActivity fragmentActivity) { if (!proxyPrefs.torEnabled) { return true; @@ -172,10 +312,12 @@ public class OrbotHelper { public void handleMessage(Message msg) { switch (msg.what) { case SupportInstallDialogFragment.MESSAGE_MIDDLE_CLICKED: - middleButtonRunnable.run(); + dialogActions.onNeutralButton(); break; case SupportInstallDialogFragment.MESSAGE_DIALOG_DISMISSED: - dialogDismissRunnable.run(); + // both install and cancel buttons mean we don't go ahead with an + // operation, so it's okay to cancel + dialogActions.onCancel(); break; } } @@ -187,25 +329,39 @@ public class OrbotHelper { ).show(fragmentActivity.getSupportFragmentManager(), "OrbotHelperOrbotInstallDialog"); return false; - } else if (!OrbotHelper.isOrbotRunning()) { + } else if (!OrbotHelper.isOrbotRunning(fragmentActivity)) { - Handler ignoreTorHandler = new Handler() { + final Handler dialogHandler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { case OrbotStartDialogFragment.MESSAGE_MIDDLE_BUTTON: - middleButtonRunnable.run(); + dialogActions.onNeutralButton(); + break; + case OrbotStartDialogFragment.MESSAGE_DIALOG_CANCELLED: + dialogActions.onCancel(); break; - case OrbotStartDialogFragment.MESSAGE_DIALOG_DISMISSED: - dialogDismissRunnable.run(); + case OrbotStartDialogFragment.MESSAGE_ORBOT_STARTED: + dialogActions.onOrbotStarted(); break; } } }; - OrbotHelper.getOrbotStartDialogFragment(new Messenger(ignoreTorHandler), - middleButton) - .show(fragmentActivity.getSupportFragmentManager(), "OrbotHelperOrbotStartDialog"); + new SilentStartManager() { + + @Override + protected void onOrbotStarted() { + dialogActions.onOrbotStarted(); + } + + @Override + protected void onSilentStartDisabled() { + getOrbotStartDialogFragment(new Messenger(dialogHandler), middleButton) + .show(fragmentActivity.getSupportFragmentManager(), + "OrbotHelperOrbotStartDialog"); + } + }.startOrbotAndListen(fragmentActivity); return false; } else { @@ -213,40 +369,83 @@ public class OrbotHelper { } } - public static boolean putOrbotInRequiredState(final int middleButton, - final Runnable middleButtonRunnable, - Preferences.ProxyPrefs proxyPrefs, + public static boolean putOrbotInRequiredState(DialogActions dialogActions, FragmentActivity fragmentActivity) { - Runnable emptyRunnable = new Runnable() { + 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) { + new SilentStartManager() { + @Override - public void run() { + protected void onOrbotStarted() { + dialogActions.onOrbotStarted(); + } + @Override + protected void onSilentStartDisabled() { + requestShowOrbotStart(activity); } - }; - return putOrbotInRequiredState(middleButton, middleButtonRunnable, emptyRunnable, - proxyPrefs, fragmentActivity); + }.startOrbotAndListen(activity); } /** - * generates a standard Orbot install/enable dialog if necessary, based on proxy settings in - * preferences - * - * @param ignoreTorRunnable run when the "Ignore Tor" button is pressed - * @param fragmentActivity used to start the activ - * @return + * base class for listening to silent orbot starts. Also handles display of progress dialog. */ - public static boolean putOrbotInRequiredState(Runnable ignoreTorRunnable, - FragmentActivity fragmentActivity) { - return putOrbotInRequiredState(R.string.orbot_ignore_tor, ignoreTorRunnable, - Preferences.getPreferences(fragmentActivity).getProxyPrefs(), fragmentActivity); - } + private static abstract class SilentStartManager { - public static boolean putOrbotInRequiredState(Runnable ignoreTorRunnable, - Runnable dismissDialogRunnable, - FragmentActivity fragmentActivity) { - return putOrbotInRequiredState(R.string.orbot_ignore_tor, ignoreTorRunnable, - dismissDialogRunnable, - Preferences.getPreferences(fragmentActivity).getProxyPrefs(), - fragmentActivity); + private ProgressDialog mProgressDialog; + + public void startOrbotAndListen(Context context) { + mProgressDialog = new ProgressDialog(ThemeChanger.getDialogThemeWrapper(context)); + mProgressDialog.setMessage(context.getString(R.string.progress_starting_orbot)); + mProgressDialog.setCancelable(false); + mProgressDialog.show(); + + 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() { + mProgressDialog.dismiss(); + onOrbotStarted(); + } + }, 1000); + break; + case OrbotHelper.STATUS_STARTS_DISABLED: + context.unregisterReceiver(this); + 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); + } + + 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 index d11e80f7a..2638f8cd5 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/orbot/TorServiceUtils.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/orbot/TorServiceUtils.java @@ -49,7 +49,8 @@ package org.sufficientlysecure.keychain.util.orbot; -import org.sufficientlysecure.keychain.Constants; +import android.content.Context; + import org.sufficientlysecure.keychain.util.Log; import java.io.BufferedReader; @@ -62,24 +63,27 @@ import java.util.StringTokenizer; * This class is taken from the NetCipher library: https://github.com/guardianproject/NetCipher/ */ public class TorServiceUtils { - // various console cmds + + 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(String command) { + 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) { + if (procId == -1) procId = findProcessIdWithPS(command); - } } catch (Exception e) { try { procId = findProcessIdWithPS(command); } catch (Exception e2) { - Log.e(Constants.TAG, "Unable to get proc id for command: " + URLEncoder.encode(command), e2); + Log.e(TAG, "Unable to get proc id for command: " + URLEncoder.encode(command), e2); } } @@ -90,7 +94,9 @@ public class TorServiceUtils { public static int findProcessIdWithPidOf(String command) throws Exception { int procId = -1; + Runtime r = Runtime.getRuntime(); + Process procPs; String baseName = new File(command).getName(); @@ -115,18 +121,23 @@ public class TorServiceUtils { } 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)) { @@ -140,5 +151,6 @@ public class TorServiceUtils { } return procId; + } -} +} \ No newline at end of file -- cgit v1.2.3