updated OrbotHelper with silent start

This commit is contained in:
Adithya Abraham Philip
2015-08-06 01:59:11 +05:30
parent 7086a67c2e
commit 37864a9d42
11 changed files with 473 additions and 174 deletions

View File

@@ -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) {

View File

@@ -49,69 +49,219 @@
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;
}
/**
* 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
*
* @return
*/
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 getInstallDialogFragment() {
return SupportInstallDialogFragment.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);
@@ -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_DISMISSED:
dialogDismissRunnable.run();
case OrbotStartDialogFragment.MESSAGE_DIALOG_CANCELLED:
dialogActions.onCancel();
break;
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() {
@Override
public void run() {
}
};
return putOrbotInRequiredState(middleButton, middleButtonRunnable, emptyRunnable,
proxyPrefs, fragmentActivity);
}
/**
* 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
*/
public static boolean putOrbotInRequiredState(Runnable ignoreTorRunnable,
FragmentActivity fragmentActivity) {
return putOrbotInRequiredState(R.string.orbot_ignore_tor, ignoreTorRunnable,
Preferences.getPreferences(fragmentActivity).getProxyPrefs(), fragmentActivity);
}
public static boolean putOrbotInRequiredState(Runnable ignoreTorRunnable,
Runnable dismissDialogRunnable,
FragmentActivity fragmentActivity) {
return putOrbotInRequiredState(R.string.orbot_ignore_tor, ignoreTorRunnable,
dismissDialogRunnable,
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
protected void onOrbotStarted() {
dialogActions.onOrbotStarted();
}
@Override
protected void onSilentStartDisabled() {
requestShowOrbotStart(activity);
}
}.startOrbotAndListen(activity);
}
/**
* base class for listening to silent orbot starts. Also handles display of progress dialog.
*/
private static abstract class SilentStartManager {
private ProgressDialog mProgressDialog;
public void startOrbotAndListen(Context context) {
mProgressDialog = new ProgressDialog(ThemeChanger.getDialogThemeWrapper(context));
mProgressDialog.setMessage(context.getString(R.string.progress_starting_orbot));
mProgressDialog.setCancelable(false);
mProgressDialog.show();
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();
}
}

View File

@@ -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;
}
}
}