diff --git a/app/build.gradle b/app/build.gradle
index b9631646..cc364c78 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -30,5 +30,5 @@ dependencies {
compile project(':orbotservice')
compile 'com.android.support:support-v4:23.4.0'
compile 'com.android.support:appcompat-v7:23.4.0'
-
+ compile 'com.android.support:design:23.4.0'
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 171567ec..a713cf56 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,137 +1,178 @@
-
-
-
-
-
-
-
+ -->
-
+
+
+
+
+
+
- >
-
-
+
+
+
+ android:launchMode="singleTop">
+
-
-
+
+
+
-
-
+
+
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+ android:stopWithTask="false">
+
+
+
+
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/org/torproject/android/OrbotMainActivity.java b/app/src/main/java/org/torproject/android/OrbotMainActivity.java
index 7706c0f5..1a468d4c 100644
--- a/app/src/main/java/org/torproject/android/OrbotMainActivity.java
+++ b/app/src/main/java/org/torproject/android/OrbotMainActivity.java
@@ -4,6 +4,7 @@
package org.torproject.android;
import java.io.BufferedReader;
+import java.io.File;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
@@ -26,6 +27,11 @@ import org.torproject.android.ui.AppManager;
import org.torproject.android.ui.ImageProgressView;
import org.torproject.android.ui.PromoAppsActivity;
import org.torproject.android.ui.Rotate3dAnimation;
+import org.torproject.android.ui.hiddenservices.ClientCookiesActivity;
+import org.torproject.android.ui.hiddenservices.HiddenServicesActivity;
+import org.torproject.android.ui.hiddenservices.backup.BackupUtils;
+import org.torproject.android.ui.hiddenservices.permissions.PermissionManager;
+import org.torproject.android.ui.hiddenservices.providers.HSContentProvider;
import org.torproject.android.vpn.VPNEnableActivity;
import android.annotation.SuppressLint;
@@ -34,6 +40,8 @@ import android.app.ActivityManager.RunningServiceInfo;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.ContentValues;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
@@ -43,6 +51,7 @@ import android.content.SharedPreferences.Editor;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
+import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
@@ -80,6 +89,8 @@ import android.widget.Toast;
import com.google.zxing.integration.android.IntentIntegrator;
import com.google.zxing.integration.android.IntentResult;
+import static android.support.v4.content.FileProvider.getUriForFile;
+
public class OrbotMainActivity extends AppCompatActivity
implements OrbotConstants, OnLongClickListener, OnTouchListener {
@@ -114,8 +125,6 @@ public class OrbotMainActivity extends AppCompatActivity
private final static int REQUEST_SETTINGS = 0x9874;
private final static int REQUEST_VPN_APPS_SELECT = 8889;
- private final static boolean mIsLollipop = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP;
-
// message types for mStatusUpdateHandler
private final static int STATUS_UPDATE = 1;
private static final int MESSAGE_TRAFFIC_COUNT = 2;
@@ -140,13 +149,37 @@ public class OrbotMainActivity extends AppCompatActivity
return super.onCreateView(parent, name, context, attrs);
return null;
}
-
- /** Called when the activity is first created. */
+
+ private void migratePreferences() {
+ String hsPortString = mPrefs.getString("pref_hs_ports", "");
+ if (hsPortString.length() > 0) {
+ StringTokenizer st = new StringTokenizer(hsPortString, ",");
+ ContentResolver cr = getContentResolver();
+ while (st.hasMoreTokens()) {
+ int hsPort = Integer.parseInt(st.nextToken().split(" ")[0]);
+ ContentValues fields = new ContentValues();
+ fields.put("name", hsPort);
+ fields.put("port", hsPort);
+ fields.put("onion_port", hsPort);
+ cr.insert(HSContentProvider.CONTENT_URI, fields);
+ }
+
+ Editor pEdit = mPrefs.edit();
+ pEdit.remove("pref_hs_ports");
+ pEdit.commit();
+ }
+ }
+
+ /**
+ * Called when the activity is first created.
+ */
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mPrefs = TorServiceUtils.getSharedPrefs(getApplicationContext());
+ migratePreferences(); // Migrate old preferences
+
/* Create the widgets before registering for broadcasts to guarantee
* that the widgets exist when the status updates try to update them */
doLayout();
@@ -478,6 +511,10 @@ public class OrbotMainActivity extends AppCompatActivity
}
}
+ } else if (item.getItemId() == R.id.menu_hidden_services) {
+ startActivity(new Intent(this, HiddenServicesActivity.class));
+ } else if (item.getItemId() == R.id.menu_client_cookies) {
+ startActivity(new Intent(this, ClientCookiesActivity.class));
}
return super.onOptionsItemSelected(item);
@@ -548,192 +585,249 @@ public class OrbotMainActivity extends AppCompatActivity
Prefs.putUseVpn(enable);
if (enable) {
- if (mIsLollipop) //let the user choose the apps
+ if (PermissionManager.isLollipopOrHigher()) //let the user choose the apps
startActivityForResult(new Intent(OrbotMainActivity.this, AppManager.class), REQUEST_VPN_APPS_SELECT);
else
startActivity(new Intent(OrbotMainActivity.this, VPNEnableActivity.class));
} else
stopVpnService();
}
-
- private void enableHiddenServicePort (int hsPort) throws RemoteException, InterruptedException
- {
-
- Editor pEdit = mPrefs.edit();
-
- String hsPortString = mPrefs.getString("pref_hs_ports", "");
- String onionHostname = mPrefs.getString("pref_hs_hostname","");
-
- if (hsPortString.indexOf(hsPort+"")==-1)
- {
- if (hsPortString.length() > 0 && hsPortString.indexOf(hsPort+"")==-1)
- hsPortString += ',' + hsPort;
- else
- hsPortString = hsPort + "";
-
- pEdit.putString("pref_hs_ports", hsPortString);
- pEdit.putBoolean("pref_hs_enable", true);
-
- pEdit.commit();
- }
- if (onionHostname == null || onionHostname.length() == 0)
- {
- requestTorRereadConfig();
+ private void enableHiddenServicePort(
+ String hsName, final int hsPort, int hsRemotePort,
+ final String backupToPackage, final Uri hsKeyPath,
+ final Boolean authCookie
+ ) throws RemoteException, InterruptedException {
- new Thread () {
+ String onionHostname = null;
- public void run ()
- {
- String onionHostname = mPrefs.getString("pref_hs_hostname","");
+ if (hsName == null)
+ hsName = "hs" + hsPort;
- while (onionHostname.length() == 0)
- {
- //we need to stop and start Tor
- try {
- Thread.sleep(3000); //wait three seconds
- } catch (Exception e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- }
-
- onionHostname = mPrefs.getString("pref_hs_hostname","");
- }
-
- Intent nResult = new Intent();
- nResult.putExtra("hs_host", onionHostname);
- setResult(RESULT_OK, nResult);
- finish();
- }
- }.start();
-
- }
- else
- {
- Intent nResult = new Intent();
- nResult.putExtra("hs_host", onionHostname);
- setResult(RESULT_OK, nResult);
- finish();
- }
-
- }
-
- private synchronized void handleIntents ()
- {
- if (getIntent() == null)
- return;
-
- // Get intent, action and MIME type
- Intent intent = getIntent();
- String action = intent.getAction();
- Log.d(TAG, "handleIntents " + action);
+ if (hsRemotePort == -1)
+ hsRemotePort = hsPort;
- //String type = intent.getType();
-
- if (action == null)
- return;
-
- if (action.equals(INTENT_ACTION_REQUEST_HIDDEN_SERVICE))
- {
- final int hiddenServicePortRequest = getIntent().getIntExtra("hs_port", -1);
+ ContentValues fields = new ContentValues();
+ fields.put(HSContentProvider.HiddenService.NAME, hsName);
+ fields.put(HSContentProvider.HiddenService.PORT, hsPort);
+ fields.put(HSContentProvider.HiddenService.ONION_PORT, hsRemotePort);
+ fields.put(HSContentProvider.HiddenService.AUTH_COOKIE, authCookie);
- DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() {
-
- public void onClick(DialogInterface dialog, int which) {
- switch (which){
- case DialogInterface.BUTTON_POSITIVE:
-
- try {
- enableHiddenServicePort (hiddenServicePortRequest);
-
- } catch (RemoteException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- } catch (InterruptedException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- }
+ ContentResolver cr = getContentResolver();
-
- break;
+ Cursor row = cr.query(
+ HSContentProvider.CONTENT_URI,
+ HSContentProvider.PROJECTION,
+ HSContentProvider.HiddenService.ONION_PORT + "=" + hsPort,
+ null,
+ null
+ );
- case DialogInterface.BUTTON_NEGATIVE:
- //No button clicked
- finish();
- break;
- }
- }
- };
+ if (row == null || row.getCount() < 1) {
+ cr.insert(HSContentProvider.CONTENT_URI, fields);
+ } else {
+ onionHostname = row.getString(row.getColumnIndex(HSContentProvider.HiddenService.DOMAIN));
+ row.close();
+ }
- String requestMsg = getString(R.string.hidden_service_request, hiddenServicePortRequest);
- AlertDialog.Builder builder = new AlertDialog.Builder(this);
- builder.setMessage(requestMsg).setPositiveButton("Allow", dialogClickListener)
- .setNegativeButton("Deny", dialogClickListener).show();
-
- return; //don't null the setIntent() as we need it later
- }
- else if (action.equals(INTENT_ACTION_REQUEST_START_TOR))
- {
- autoStartFromIntent = true;
-
- startTor();
+ if (onionHostname == null || onionHostname.length() < 1) {
- //never allow backgrounds start from this type of intent start
- //app devs who want background starts, can use the service intents
- /**
- if (Prefs.allowBackgroundStarts())
- {
- Intent resultIntent;
- if (lastStatusIntent == null) {
- resultIntent = new Intent(intent);
- } else {
- resultIntent = lastStatusIntent;
- }
- resultIntent.putExtra(TorServiceConstants.EXTRA_STATUS, torStatus);
- setResult(RESULT_OK, resultIntent);
- finish();
- }*/
-
- }
- else if (action.equals(Intent.ACTION_VIEW))
- {
- String urlString = intent.getDataString();
-
- if (urlString != null)
- {
-
- if (urlString.toLowerCase().startsWith("bridge://"))
+ if (hsKeyPath != null) {
+ BackupUtils hsutils = new BackupUtils(getApplicationContext());
+ hsutils.restoreKeyBackup(hsPort, hsKeyPath);
+ }
- {
- String newBridgeValue = urlString.substring(9); //remove the bridge protocol piece
- newBridgeValue = URLDecoder.decode(newBridgeValue); //decode the value here
+ if (torStatus.equals(TorServiceConstants.STATUS_OFF)) {
+ startTor();
+ } else {
+ stopTor();
+ Toast.makeText(
+ this, R.string.start_tor_again_for_finish_the_process, Toast.LENGTH_LONG
+ ).show();
+ }
- showAlert(getString(R.string.bridges_updated),getString(R.string.restart_orbot_to_use_this_bridge_) + newBridgeValue,false);
-
- setNewBridges(newBridgeValue);
- }
- }
- }
-
- updateStatus(null);
-
- setIntent(null);
-
-
- }
-
- private void setNewBridges (String newBridgeValue)
- {
+ new Thread() {
- Prefs.setBridgesList(newBridgeValue); //set the string to a preference
- Prefs.putBridgesEnabled(true);
-
- setResult(RESULT_OK);
-
- mBtnBridges.setChecked(true);
-
- enableBridges(true);
- }
+ public void run() {
+ String hostname = null;
+ Intent nResult = new Intent();
+
+ while (hostname == null) {
+ try {
+ Thread.sleep(3000); //wait three seconds
+ } catch (Exception e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+
+ Cursor onion = getContentResolver().query(
+ HSContentProvider.CONTENT_URI,
+ HSContentProvider.PROJECTION,
+ HSContentProvider.HiddenService.ONION_PORT + "=" + hsPort,
+ null,
+ null
+ );
+
+ if (onion != null && onion.getCount() > 0) {
+ onion.moveToNext();
+ hostname = onion.getString(onion.getColumnIndex(HSContentProvider.HiddenService.DOMAIN));
+
+ if(hostname == null || hostname.length() < 1)
+ continue;
+
+ nResult.putExtra("hs_host", hostname);
+
+ if (authCookie) {
+ nResult.putExtra(
+ "hs_auth_cookie",
+ onion.getString(onion.getColumnIndex(HSContentProvider.HiddenService.AUTH_COOKIE_VALUE))
+ );
+ }
+
+ if (backupToPackage != null && backupToPackage.length() > 0) {
+ String servicePath = getFilesDir() + "/" + TorServiceConstants.HIDDEN_SERVICES_DIR + "/hs" + hsPort;
+ File hidden_service_key = new File(servicePath, "private_key");
+ Context context = getApplicationContext();
+
+ Uri contentUri = getUriForFile(
+ context,
+ "org.torproject.android.ui.hiddenservices.storage",
+ hidden_service_key
+ );
+
+ context.grantUriPermission(backupToPackage, contentUri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ nResult.setData(contentUri);
+ nResult.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ }
+
+ onion.close();
+ setResult(RESULT_OK, nResult);
+ finish();
+ }
+ }
+ }
+ }.start();
+
+ } else {
+ Intent nResult = new Intent();
+ nResult.putExtra("hs_host", onionHostname);
+ setResult(RESULT_OK, nResult);
+ finish();
+ }
+ }
+
+ private synchronized void handleIntents() {
+ if (getIntent() == null)
+ return;
+
+ // Get intent, action and MIME type
+ Intent intent = getIntent();
+ String action = intent.getAction();
+ Log.d(TAG, "handleIntents " + action);
+
+ //String type = intent.getType();
+
+ if (action == null)
+ return;
+
+ switch (action) {
+ case INTENT_ACTION_REQUEST_HIDDEN_SERVICE:
+ final int hiddenServicePort = intent.getIntExtra("hs_port", -1);
+ final int hiddenServiceRemotePort = intent.getIntExtra("hs_onion_port", -1);
+ final String hiddenServiceName = intent.getStringExtra("hs_name");
+ final String backupToPackage = intent.getStringExtra("hs_backup_to_package");
+ final Boolean authCookie = intent.getBooleanExtra("hs_auth_cookie", false);
+ final Uri mKeyUri = intent.getData();
+
+ DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() {
+
+ public void onClick(DialogInterface dialog, int which) {
+ switch (which) {
+ case DialogInterface.BUTTON_POSITIVE:
+ try {
+ enableHiddenServicePort(
+ hiddenServiceName, hiddenServicePort,
+ hiddenServiceRemotePort, backupToPackage,
+ mKeyUri, authCookie
+ );
+ } catch (RemoteException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ } catch (InterruptedException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+
+ break;
+ }
+ }
+ };
+
+ String requestMsg = getString(R.string.hidden_service_request, hiddenServicePort);
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setMessage(requestMsg).setPositiveButton("Allow", dialogClickListener)
+ .setNegativeButton("Deny", dialogClickListener).show();
+
+ return; //don't null the setIntent() as we need it later
+
+ case INTENT_ACTION_REQUEST_START_TOR:
+ autoStartFromIntent = true;
+
+ startTor();
+
+ //never allow backgrounds start from this type of intent start
+ //app devs who want background starts, can use the service intents
+ /**
+ if (Prefs.allowBackgroundStarts())
+ {
+ Intent resultIntent;
+ if (lastStatusIntent == null) {
+ resultIntent = new Intent(intent);
+ } else {
+ resultIntent = lastStatusIntent;
+ }
+ resultIntent.putExtra(TorServiceConstants.EXTRA_STATUS, torStatus);
+ setResult(RESULT_OK, resultIntent);
+ finish();
+ }*/
+
+ break;
+ case Intent.ACTION_VIEW:
+ String urlString = intent.getDataString();
+
+ if (urlString != null) {
+
+ if (urlString.toLowerCase().startsWith("bridge://"))
+
+ {
+ String newBridgeValue = urlString.substring(9); //remove the bridge protocol piece
+ newBridgeValue = URLDecoder.decode(newBridgeValue); //decode the value here
+
+ showAlert(getString(R.string.bridges_updated), getString(R.string.restart_orbot_to_use_this_bridge_) + newBridgeValue, false);
+
+ setNewBridges(newBridgeValue);
+ }
+ }
+ break;
+ }
+
+ updateStatus(null);
+
+ setIntent(null);
+
+ }
+
+ private void setNewBridges(String newBridgeValue) {
+
+ Prefs.setBridgesList(newBridgeValue); //set the string to a preference
+ Prefs.putBridgesEnabled(true);
+
+ setResult(RESULT_OK);
+
+ mBtnBridges.setChecked(true);
+
+ enableBridges(true);
+ }
/*
* Launch the system activity for Uri viewing with the provided url
@@ -1193,11 +1287,7 @@ public class OrbotMainActivity extends AppCompatActivity
if (autoStartFromIntent)
{
autoStartFromIntent = false;
- Intent resultIntent = lastStatusIntent;
-
- if (resultIntent == null)
- resultIntent = new Intent(TorServiceConstants.ACTION_START);
-
+ Intent resultIntent = lastStatusIntent;
resultIntent.putExtra(TorServiceConstants.EXTRA_STATUS, torStatus);
setResult(RESULT_OK, resultIntent);
finish();
diff --git a/app/src/main/java/org/torproject/android/settings/SettingsPreferences.java b/app/src/main/java/org/torproject/android/settings/SettingsPreferences.java
index c14cb5a4..adacba72 100644
--- a/app/src/main/java/org/torproject/android/settings/SettingsPreferences.java
+++ b/app/src/main/java/org/torproject/android/settings/SettingsPreferences.java
@@ -37,9 +37,6 @@ public class SettingsPreferences
private Preference prefTransProxyFlush = null;
private Preference prefTransProxyApps = null;
- private CheckBoxPreference prefHiddenServices = null;
- private EditTextPreference prefHiddenServicesPorts;
- private EditTextPreference prefHiddenServicesHostname;
private CheckBoxPreference prefRequestRoot = null;
private ListPreference prefLocale = null;
@@ -104,22 +101,14 @@ public class SettingsPreferences
prefTransProxyApps.setOnPreferenceClickListener(this);
prefCBTransProxy.setOnPreferenceClickListener(this);
prefcBTransProxyAll.setOnPreferenceClickListener(this);
- prefHiddenServices = (CheckBoxPreference) findPreference("pref_hs_enable");
- prefHiddenServices.setOnPreferenceClickListener(this);
- prefHiddenServicesHostname = (EditTextPreference) findPreference("pref_hs_hostname");
-
-
+
prefCBTransProxy.setEnabled(prefRequestRoot.isChecked());
prefcBTransProxyAll.setEnabled(prefCBTransProxy.isChecked());
prefcbTransTethering.setEnabled(prefCBTransProxy.isChecked());
if (prefCBTransProxy.isChecked())
prefTransProxyApps.setEnabled((!prefcBTransProxyAll.isChecked()));
-
- prefHiddenServicesPorts = (EditTextPreference) findPreference("pref_hs_ports");
- prefHiddenServicesHostname.setEnabled(prefHiddenServices.isChecked());
- prefHiddenServicesPorts.setEnabled(prefHiddenServices.isChecked());
-
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
{
prefTransProxyApps.setEnabled(true);
@@ -153,11 +142,6 @@ public class SettingsPreferences
{
startActivity(new Intent(this, AppManager.class));
- }
- else if (preference == prefHiddenServices)
- {
- prefHiddenServicesPorts.setEnabled(prefHiddenServices.isChecked());
- prefHiddenServicesHostname.setEnabled(prefHiddenServices.isChecked());
}
else
{
diff --git a/app/src/main/java/org/torproject/android/ui/hiddenservices/ClientCookiesActivity.java b/app/src/main/java/org/torproject/android/ui/hiddenservices/ClientCookiesActivity.java
new file mode 100644
index 00000000..a766a5c7
--- /dev/null
+++ b/app/src/main/java/org/torproject/android/ui/hiddenservices/ClientCookiesActivity.java
@@ -0,0 +1,199 @@
+package org.torproject.android.ui.hiddenservices;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.design.widget.FloatingActionButton;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.Toolbar;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.ListView;
+import android.widget.Toast;
+
+import com.google.zxing.integration.android.IntentIntegrator;
+import com.google.zxing.integration.android.IntentResult;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.torproject.android.R;
+import org.torproject.android.ui.hiddenservices.adapters.ClientCookiesAdapter;
+import org.torproject.android.ui.hiddenservices.dialogs.AddCookieDialog;
+import org.torproject.android.ui.hiddenservices.dialogs.CookieActionsDialog;
+import org.torproject.android.ui.hiddenservices.dialogs.SelectCookieBackupDialog;
+import org.torproject.android.ui.hiddenservices.permissions.PermissionManager;
+import org.torproject.android.ui.hiddenservices.providers.CookieContentProvider;
+
+public class ClientCookiesActivity extends AppCompatActivity {
+ public final int WRITE_EXTERNAL_STORAGE_FROM_COOKIE_ACTIONBAR = 3;
+
+ private ContentResolver mResolver;
+ private ClientCookiesAdapter mAdapter;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.layout_activity_client_cookies);
+
+ Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
+ setSupportActionBar(toolbar);
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+
+ mResolver = getContentResolver();
+
+ FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
+ fab.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ AddCookieDialog dialog = new AddCookieDialog();
+ dialog.show(getSupportFragmentManager(), "AddCookieDialog");
+ }
+ });
+
+ mAdapter = new ClientCookiesAdapter(
+ this,
+ mResolver.query(CookieContentProvider.CONTENT_URI, CookieContentProvider.PROJECTION, null, null, null)
+ , 0);
+
+ mResolver.registerContentObserver(
+ CookieContentProvider.CONTENT_URI, true, new HSObserver(new Handler())
+ );
+
+ ListView cookies = (ListView) findViewById(R.id.clien_cookies_list);
+ cookies.setAdapter(mAdapter);
+
+ cookies.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+
+ @Override
+ public void onItemClick(AdapterView> parent, View view, int position, long id) {
+ Cursor item = (Cursor) parent.getItemAtPosition(position);
+
+ Bundle arguments = new Bundle();
+ arguments.putInt(
+ "_id", item.getInt(item.getColumnIndex(CookieContentProvider.ClientCookie._ID))
+ );
+
+ arguments.putString(
+ "domain", item.getString(item.getColumnIndex(CookieContentProvider.ClientCookie.DOMAIN))
+ );
+
+ arguments.putString(
+ "auth_cookie_value", item.getString(item.getColumnIndex(CookieContentProvider.ClientCookie.AUTH_COOKIE_VALUE))
+ );
+
+ arguments.putInt(
+ "enabled", item.getInt(item.getColumnIndex(CookieContentProvider.ClientCookie.ENABLED))
+ );
+
+ CookieActionsDialog dialog = new CookieActionsDialog();
+ dialog.setArguments(arguments);
+ dialog.show(getSupportFragmentManager(), "CookieActionsDialog");
+ }
+ });
+
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.cookie_menu, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ int id = item.getItemId();
+
+ if (id == R.id.cookie_restore_backup) {
+ if (PermissionManager.isLollipopOrHigher()
+ && !PermissionManager.hasExternalWritePermission(this)) {
+ PermissionManager.requestExternalWritePermissions(this, WRITE_EXTERNAL_STORAGE_FROM_COOKIE_ACTIONBAR);
+ return true;
+ }
+
+ SelectCookieBackupDialog dialog = new SelectCookieBackupDialog();
+ dialog.show(getSupportFragmentManager(), "SelectCookieBackupDialog");
+
+ } else if (id == R.id.cookie_from_qr) {
+ IntentIntegrator integrator = new IntentIntegrator(ClientCookiesActivity.this);
+ integrator.initiateScan();
+ }
+
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode,
+ String permissions[], int[] grantResults) {
+ if (grantResults.length < 1
+ || grantResults[0] != PackageManager.PERMISSION_GRANTED) {
+ return;
+ }
+
+ switch (requestCode) {
+ case WRITE_EXTERNAL_STORAGE_FROM_COOKIE_ACTIONBAR: {
+ SelectCookieBackupDialog dialog = new SelectCookieBackupDialog();
+ dialog.show(getSupportFragmentManager(), "SelectCookieBackupDialog");
+ break;
+ }
+ case CookieActionsDialog.WRITE_EXTERNAL_STORAGE_FROM_COOKIE_ACTION_DIALOG: {
+ Toast.makeText(this, R.string.click_again_for_backup, Toast.LENGTH_LONG).show();
+ break;
+ }
+ }
+ }
+
+ @Override
+ protected void onActivityResult(int request, int response, Intent data) {
+ super.onActivityResult(request, response, data);
+
+ IntentResult scanResult = IntentIntegrator.parseActivityResult(request, response, data);
+
+ if (scanResult == null) return;
+
+ String results = scanResult.getContents();
+
+ if (results == null || results.length() < 1) return;
+
+ try {
+ JSONObject savedValues = new JSONObject(results);
+ ContentValues fields = new ContentValues();
+
+ fields.put(
+ CookieContentProvider.ClientCookie.DOMAIN,
+ savedValues.getString(CookieContentProvider.ClientCookie.DOMAIN)
+ );
+
+ fields.put(
+ CookieContentProvider.ClientCookie.AUTH_COOKIE_VALUE,
+ savedValues.getString(CookieContentProvider.ClientCookie.AUTH_COOKIE_VALUE)
+ );
+
+ mResolver.insert(CookieContentProvider.CONTENT_URI, fields);
+
+ } catch (JSONException e) {
+ e.printStackTrace();
+ Toast.makeText(this, R.string.error, Toast.LENGTH_LONG).show();
+ }
+ }
+
+ class HSObserver extends ContentObserver {
+ HSObserver(Handler handler) {
+ super(handler);
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ mAdapter.changeCursor(mResolver.query(
+ CookieContentProvider.CONTENT_URI, CookieContentProvider.PROJECTION, null, null, null
+ ));
+ }
+ }
+
+}
diff --git a/app/src/main/java/org/torproject/android/ui/hiddenservices/HiddenServicesActivity.java b/app/src/main/java/org/torproject/android/ui/hiddenservices/HiddenServicesActivity.java
new file mode 100644
index 00000000..f6445588
--- /dev/null
+++ b/app/src/main/java/org/torproject/android/ui/hiddenservices/HiddenServicesActivity.java
@@ -0,0 +1,214 @@
+package org.torproject.android.ui.hiddenservices;
+
+
+import android.content.ContentResolver;
+import android.content.pm.PackageManager;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.design.widget.FloatingActionButton;
+import android.support.v4.view.MenuItemCompat;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.Toolbar;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.ListView;
+import android.widget.Spinner;
+import android.widget.Toast;
+
+import org.torproject.android.R;
+import org.torproject.android.ui.hiddenservices.adapters.OnionListAdapter;
+import org.torproject.android.ui.hiddenservices.dialogs.HSActionsDialog;
+import org.torproject.android.ui.hiddenservices.dialogs.HSDataDialog;
+import org.torproject.android.ui.hiddenservices.dialogs.SelectHSBackupDialog;
+import org.torproject.android.ui.hiddenservices.permissions.PermissionManager;
+import org.torproject.android.ui.hiddenservices.providers.HSContentProvider;
+
+public class HiddenServicesActivity extends AppCompatActivity {
+ public final int WRITE_EXTERNAL_STORAGE_FROM_ACTIONBAR = 1;
+ private ContentResolver mResolver;
+ private OnionListAdapter mAdapter;
+ private FloatingActionButton fab;
+
+ private String mWhere = HSContentProvider.HiddenService.CREATED_BY_USER + "=1";
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.layout_hs_list_view);
+
+ Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
+ setSupportActionBar(toolbar);
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+
+ mResolver = getContentResolver();
+
+ fab = (FloatingActionButton) findViewById(R.id.fab);
+ fab.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ HSDataDialog dialog = new HSDataDialog();
+ dialog.show(getSupportFragmentManager(), "HSDataDialog");
+ }
+ });
+
+ mAdapter = new OnionListAdapter(
+ this,
+ mResolver.query(
+ HSContentProvider.CONTENT_URI, HSContentProvider.PROJECTION, mWhere, null, null
+ ),
+ 0
+ );
+
+ mResolver.registerContentObserver(
+ HSContentProvider.CONTENT_URI, true, new HSObserver(new Handler())
+ );
+
+ ListView onion_list = (ListView) findViewById(R.id.onion_list);
+ onion_list.setAdapter(mAdapter);
+
+ onion_list.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+
+ @Override
+ public void onItemClick(AdapterView> parent, View view, int position, long id) {
+ Cursor item = (Cursor) parent.getItemAtPosition(position);
+
+ Bundle arguments = new Bundle();
+ arguments.putInt(
+ "_id", item.getInt(item.getColumnIndex(HSContentProvider.HiddenService._ID))
+ );
+
+ arguments.putString(
+ "port", item.getString(item.getColumnIndex(HSContentProvider.HiddenService.PORT))
+ );
+
+ arguments.putString(
+ "onion", item.getString(item.getColumnIndex(HSContentProvider.HiddenService.DOMAIN))
+ );
+
+ arguments.putInt(
+ "auth_cookie", item.getInt(item.getColumnIndex(HSContentProvider.HiddenService.AUTH_COOKIE))
+ );
+
+ arguments.putString(
+ "auth_cookie_value", item.getString(item.getColumnIndex(HSContentProvider.HiddenService.AUTH_COOKIE_VALUE))
+ );
+
+ HSActionsDialog dialog = new HSActionsDialog();
+ dialog.setArguments(arguments);
+ dialog.show(getSupportFragmentManager(), "HSActionsDialog");
+ }
+ });
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.hs_menu, menu);
+
+ MenuItem item = menu.findItem(R.id.hs_type);
+ Spinner spinner = (Spinner) MenuItemCompat.getActionView(item);
+
+ ArrayAdapter adapter = ArrayAdapter.createFromResource(
+ this, R.array.array_hs_types, android.R.layout.simple_spinner_item);
+
+ adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+
+ spinner.setAdapter(adapter);
+
+ spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
+
+ @Override
+ public void onItemSelected(AdapterView> arg0, View v, int pos, long id) {
+ if (pos == 0) {
+ mWhere = HSContentProvider.HiddenService.CREATED_BY_USER + "=1";
+ fab.show();
+ } else {
+ mWhere = HSContentProvider.HiddenService.CREATED_BY_USER + "=0";
+ fab.hide();
+ }
+
+ mAdapter.changeCursor(mResolver.query(
+ HSContentProvider.CONTENT_URI, HSContentProvider.PROJECTION, mWhere, null, null
+ ));
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView> arg0) {
+ // Do nothing
+ }
+ });
+
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ int id = item.getItemId();
+
+ if (id == R.id.menu_restore_backup) {
+ if (PermissionManager.isLollipopOrHigher()
+ && !PermissionManager.hasExternalWritePermission(this)) {
+ PermissionManager.requestExternalWritePermissions(this, WRITE_EXTERNAL_STORAGE_FROM_ACTIONBAR);
+ return true;
+ }
+
+ SelectHSBackupDialog dialog = new SelectHSBackupDialog();
+ dialog.show(getSupportFragmentManager(), "SelectHSBackupDialog");
+ }
+
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode,
+ String permissions[], int[] grantResults) {
+ if (grantResults.length < 1
+ || grantResults[0] != PackageManager.PERMISSION_GRANTED) {
+ return;
+ }
+
+ switch (requestCode) {
+ case WRITE_EXTERNAL_STORAGE_FROM_ACTIONBAR: {
+ SelectHSBackupDialog dialog = new SelectHSBackupDialog();
+ dialog.show(getSupportFragmentManager(), "SelectHSBackupDialog");
+ break;
+ }
+ case HSActionsDialog.WRITE_EXTERNAL_STORAGE_FROM_ACTION_DIALOG: {
+ Toast.makeText(this, R.string.click_again_for_backup, Toast.LENGTH_LONG).show();
+ break;
+ }
+ }
+ }
+
+ class HSObserver extends ContentObserver {
+ HSObserver(Handler handler) {
+ super(handler);
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ mAdapter.changeCursor(mResolver.query(
+ HSContentProvider.CONTENT_URI, HSContentProvider.PROJECTION, mWhere, null, null
+ ));
+
+ if (PermissionManager.isLollipopOrHigher()) {
+ Cursor active = mResolver.query(
+ HSContentProvider.CONTENT_URI, HSContentProvider.PROJECTION, HSContentProvider.HiddenService.ENABLED + "=1", null, null
+ );
+
+ if (active == null) return;
+
+ if (active.getCount() > 0) // Call only if there running services
+ PermissionManager.requestBatteryPermmssions(HiddenServicesActivity.this, getApplicationContext());
+ else // Drop whe not needed
+ PermissionManager.requestDropBatteryPermmssions(HiddenServicesActivity.this, getApplicationContext());
+
+ active.close();
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/org/torproject/android/ui/hiddenservices/adapters/BackupAdapter.java b/app/src/main/java/org/torproject/android/ui/hiddenservices/adapters/BackupAdapter.java
new file mode 100644
index 00000000..196023b3
--- /dev/null
+++ b/app/src/main/java/org/torproject/android/ui/hiddenservices/adapters/BackupAdapter.java
@@ -0,0 +1,50 @@
+package org.torproject.android.ui.hiddenservices.adapters;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.TextView;
+
+import org.torproject.android.R;
+
+import java.io.File;
+import java.util.List;
+
+public class BackupAdapter extends ArrayAdapter {
+ private int mResource;
+
+ public BackupAdapter(Context context, int resource) {
+ super(context, resource);
+ mResource = resource;
+ }
+
+ public BackupAdapter(Context context, int resource, List zips) {
+ super(context, resource, zips);
+ mResource = resource;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+
+ View v = convertView;
+
+ if (v == null) {
+ LayoutInflater vi;
+ vi = LayoutInflater.from(getContext());
+ v = vi.inflate(mResource, null);
+ }
+
+ File p = getItem(position);
+
+ if (p != null) {
+ TextView name = (TextView) v.findViewById(R.id.backup_name);
+
+ if (name != null)
+ name.setText(p.getName());
+ }
+
+ return v;
+ }
+}
diff --git a/app/src/main/java/org/torproject/android/ui/hiddenservices/adapters/ClientCookiesAdapter.java b/app/src/main/java/org/torproject/android/ui/hiddenservices/adapters/ClientCookiesAdapter.java
new file mode 100644
index 00000000..bbc317a6
--- /dev/null
+++ b/app/src/main/java/org/torproject/android/ui/hiddenservices/adapters/ClientCookiesAdapter.java
@@ -0,0 +1,64 @@
+package org.torproject.android.ui.hiddenservices.adapters;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.support.v4.widget.CursorAdapter;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.CompoundButton;
+import android.widget.Switch;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import org.torproject.android.R;
+import org.torproject.android.ui.hiddenservices.providers.CookieContentProvider;
+
+public class ClientCookiesAdapter extends CursorAdapter {
+ private LayoutInflater cursorInflater;
+
+ public ClientCookiesAdapter(Context context, Cursor c, int flags) {
+ super(context, c, flags);
+
+ cursorInflater = (LayoutInflater) context.getSystemService(
+ Context.LAYOUT_INFLATER_SERVICE);
+ }
+
+ @Override
+ public void bindView(View view, Context context, Cursor cursor) {
+ final Context mContext = context;
+ int id = cursor.getInt(cursor.getColumnIndex(CookieContentProvider.ClientCookie._ID));
+ final String where = CookieContentProvider.ClientCookie._ID + "=" + id;
+
+ TextView domain = (TextView) view.findViewById(R.id.cookie_onion);
+ domain.setText(cursor.getString(cursor.getColumnIndex(CookieContentProvider.ClientCookie.DOMAIN)));
+
+ Switch enabled = (Switch) view.findViewById(R.id.cookie_switch);
+ enabled.setChecked(
+ cursor.getInt(cursor.getColumnIndex(CookieContentProvider.ClientCookie.ENABLED)) == 1
+ );
+
+ enabled.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ ContentResolver resolver = mContext.getContentResolver();
+ ContentValues fields = new ContentValues();
+ fields.put(CookieContentProvider.ClientCookie.ENABLED, isChecked);
+ resolver.update(
+ CookieContentProvider.CONTENT_URI, fields, where, null
+ );
+
+ Toast.makeText(
+ mContext, R.string.please_restart_Orbot_to_enable_the_changes, Toast.LENGTH_LONG
+ ).show();
+ }
+ });
+ }
+
+ @Override
+ public View newView(Context context, Cursor cursor, ViewGroup parent) {
+ return cursorInflater.inflate(R.layout.layout_client_cookie_list_item, parent, false);
+ }
+}
diff --git a/app/src/main/java/org/torproject/android/ui/hiddenservices/adapters/OnionListAdapter.java b/app/src/main/java/org/torproject/android/ui/hiddenservices/adapters/OnionListAdapter.java
new file mode 100644
index 00000000..9824c773
--- /dev/null
+++ b/app/src/main/java/org/torproject/android/ui/hiddenservices/adapters/OnionListAdapter.java
@@ -0,0 +1,68 @@
+package org.torproject.android.ui.hiddenservices.adapters;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.support.v4.widget.CursorAdapter;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.CompoundButton;
+import android.widget.Switch;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import org.torproject.android.R;
+import org.torproject.android.ui.hiddenservices.providers.HSContentProvider;
+
+public class OnionListAdapter extends CursorAdapter {
+ private LayoutInflater cursorInflater;
+
+ public OnionListAdapter(Context context, Cursor c, int flags) {
+ super(context, c, flags);
+
+ cursorInflater = (LayoutInflater) context.getSystemService(
+ Context.LAYOUT_INFLATER_SERVICE);
+ }
+
+ @Override
+ public void bindView(View view, Context context, Cursor cursor) {
+ final Context mContext = context;
+ int id = cursor.getInt(cursor.getColumnIndex(HSContentProvider.HiddenService._ID));
+ final String where = HSContentProvider.HiddenService._ID + "=" + id;
+
+ TextView port = (TextView) view.findViewById(R.id.hs_port);
+ port.setText(cursor.getString(cursor.getColumnIndex(HSContentProvider.HiddenService.PORT)));
+ TextView name = (TextView) view.findViewById(R.id.hs_name);
+ name.setText(cursor.getString(cursor.getColumnIndex(HSContentProvider.HiddenService.NAME)));
+ TextView domain = (TextView) view.findViewById(R.id.hs_onion);
+ domain.setText(cursor.getString(cursor.getColumnIndex(HSContentProvider.HiddenService.DOMAIN)));
+
+ Switch enabled = (Switch) view.findViewById(R.id.hs_switch);
+ enabled.setChecked(
+ cursor.getInt(cursor.getColumnIndex(HSContentProvider.HiddenService.ENABLED)) == 1
+ );
+
+ enabled.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ ContentResolver resolver = mContext.getContentResolver();
+ ContentValues fields = new ContentValues();
+ fields.put(HSContentProvider.HiddenService.ENABLED, isChecked);
+ resolver.update(
+ HSContentProvider.CONTENT_URI, fields, where, null
+ );
+
+ Toast.makeText(
+ mContext, R.string.please_restart_Orbot_to_enable_the_changes, Toast.LENGTH_LONG
+ ).show();
+ }
+ });
+ }
+
+ @Override
+ public View newView(Context context, Cursor cursor, ViewGroup parent) {
+ return cursorInflater.inflate(R.layout.layout_hs_list_item, parent, false);
+ }
+}
diff --git a/app/src/main/java/org/torproject/android/ui/hiddenservices/backup/BackupUtils.java b/app/src/main/java/org/torproject/android/ui/hiddenservices/backup/BackupUtils.java
new file mode 100644
index 00000000..a4790dd8
--- /dev/null
+++ b/app/src/main/java/org/torproject/android/ui/hiddenservices/backup/BackupUtils.java
@@ -0,0 +1,336 @@
+package org.torproject.android.ui.hiddenservices.backup;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.ParcelFileDescriptor;
+import android.widget.Toast;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.torproject.android.R;
+import org.torproject.android.service.TorServiceConstants;
+import org.torproject.android.ui.hiddenservices.providers.CookieContentProvider;
+import org.torproject.android.ui.hiddenservices.providers.HSContentProvider;
+import org.torproject.android.ui.hiddenservices.storage.ExternalStorage;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.MappedByteBuffer;
+import java.nio.channels.FileChannel;
+import java.nio.charset.Charset;
+
+public class BackupUtils {
+ private final String configFileName = "config.json";
+ private Context mContext;
+ private ContentResolver mResolver;
+
+ public BackupUtils(Context context) {
+ mContext = context;
+ mResolver = mContext.getContentResolver();
+ }
+
+ public String createZipBackup(Integer port) {
+ File mHSBasePath = new File(
+ mContext.getFilesDir().getAbsolutePath(),
+ TorServiceConstants.HIDDEN_SERVICES_DIR
+ );
+
+ String configFilePath = mHSBasePath + "/hs" + port + "/" + configFileName;
+ String hostnameFilePath = mHSBasePath + "/hs" + port + "/hostname";
+ String keyFilePath = mHSBasePath + "/hs" + port + "/private_key";
+
+ File storage_path = ExternalStorage.getOrCreateBackupDir();
+
+ if (storage_path == null)
+ return null;
+
+ Cursor portData = mResolver.query(
+ HSContentProvider.CONTENT_URI,
+ HSContentProvider.PROJECTION,
+ HSContentProvider.HiddenService.PORT + "=" + port,
+ null,
+ null
+ );
+
+ JSONObject config = new JSONObject();
+ try {
+ if (portData == null || portData.getCount() != 1)
+ return null;
+
+ portData.moveToNext();
+
+ config.put(
+ HSContentProvider.HiddenService.NAME,
+ portData.getString(portData.getColumnIndex(HSContentProvider.HiddenService.NAME))
+ );
+
+ config.put(
+ HSContentProvider.HiddenService.PORT,
+ portData.getInt(portData.getColumnIndex(HSContentProvider.HiddenService.PORT))
+ );
+
+ config.put(
+ HSContentProvider.HiddenService.ONION_PORT,
+ portData.getInt(portData.getColumnIndex(HSContentProvider.HiddenService.ONION_PORT))
+ );
+
+ config.put(
+ HSContentProvider.HiddenService.DOMAIN,
+ portData.getString(portData.getColumnIndex(HSContentProvider.HiddenService.DOMAIN))
+ );
+
+ config.put(
+ HSContentProvider.HiddenService.AUTH_COOKIE,
+ portData.getInt(portData.getColumnIndex(HSContentProvider.HiddenService.AUTH_COOKIE))
+ );
+
+ config.put(
+ HSContentProvider.HiddenService.AUTH_COOKIE_VALUE,
+ portData.getString(portData.getColumnIndex(HSContentProvider.HiddenService.AUTH_COOKIE_VALUE))
+ );
+
+ config.put(
+ HSContentProvider.HiddenService.CREATED_BY_USER,
+ portData.getInt(portData.getColumnIndex(HSContentProvider.HiddenService.CREATED_BY_USER))
+ );
+
+ config.put(
+ HSContentProvider.HiddenService.ENABLED,
+ portData.getInt(portData.getColumnIndex(HSContentProvider.HiddenService.ENABLED))
+ );
+ } catch (JSONException e) {
+ e.printStackTrace();
+ return null;
+ } catch (NullPointerException e) {
+ e.printStackTrace();
+ return null;
+ }
+
+ portData.close();
+
+ try {
+ FileWriter file = new FileWriter(configFilePath);
+ file.write(config.toString());
+ file.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ return null;
+ }
+
+ String zip_path = storage_path.getAbsolutePath() + "/hs" + port + ".zip";
+ String files[] = {hostnameFilePath, keyFilePath, configFilePath};
+
+ ZipIt zip = new ZipIt(files, zip_path);
+
+ if (!zip.zip())
+ return null;
+
+ return zip_path;
+ }
+
+ public void restoreZipBackup(File backup) {
+
+ File mHSBasePath = new File(
+ mContext.getFilesDir().getAbsolutePath(),
+ TorServiceConstants.HIDDEN_SERVICES_DIR
+ );
+
+ int port;
+ Cursor service;
+ String backupName = backup.getName();
+ String hsDir = backupName.substring(0, backupName.lastIndexOf('.'));
+ String configFilePath = mHSBasePath + "/" + hsDir + "/" + configFileName;
+ String jString = null;
+
+ File hsPath = new File(mHSBasePath.getAbsolutePath(), hsDir);
+ if (!hsPath.isDirectory())
+ hsPath.mkdirs();
+
+ ZipIt zip = new ZipIt(null, backup.getAbsolutePath());
+ zip.unzip(hsPath.getAbsolutePath());
+
+ File config = new File(configFilePath);
+ FileInputStream stream;
+
+ try {
+ stream = new FileInputStream(config);
+ FileChannel fc = stream.getChannel();
+ MappedByteBuffer bb = fc.map(FileChannel.MapMode.READ_ONLY, 0, fc.size());
+ jString = Charset.defaultCharset().decode(bb).toString();
+ stream.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ if (jString == null)
+ Toast.makeText(mContext, R.string.error, Toast.LENGTH_LONG).show();
+
+ try {
+ JSONObject savedValues = new JSONObject(jString);
+ ContentValues fields = new ContentValues();
+
+ fields.put(
+ HSContentProvider.HiddenService.NAME,
+ savedValues.getString(HSContentProvider.HiddenService.NAME)
+ );
+
+ fields.put(
+ HSContentProvider.HiddenService.ONION_PORT,
+ savedValues.getInt(HSContentProvider.HiddenService.ONION_PORT)
+ );
+
+ fields.put(
+ HSContentProvider.HiddenService.DOMAIN,
+ savedValues.getString(HSContentProvider.HiddenService.DOMAIN)
+ );
+
+ fields.put(
+ HSContentProvider.HiddenService.AUTH_COOKIE,
+ savedValues.getInt(HSContentProvider.HiddenService.AUTH_COOKIE)
+ );
+
+ fields.put(
+ HSContentProvider.HiddenService.CREATED_BY_USER,
+ savedValues.getInt(HSContentProvider.HiddenService.CREATED_BY_USER)
+ );
+
+ fields.put(
+ HSContentProvider.HiddenService.ENABLED,
+ savedValues.getInt(HSContentProvider.HiddenService.ENABLED)
+ );
+
+ port = savedValues.getInt(HSContentProvider.HiddenService.PORT);
+ fields.put(HSContentProvider.HiddenService.PORT, port);
+
+ service = mResolver.query(
+ HSContentProvider.CONTENT_URI,
+ HSContentProvider.PROJECTION,
+ HSContentProvider.HiddenService.PORT + "=" + port,
+ null,
+ null
+ );
+
+ if (service == null || service.getCount() == 0) {
+ mResolver.insert(HSContentProvider.CONTENT_URI, fields);
+ } else {
+ mResolver.update(
+ HSContentProvider.CONTENT_URI,
+ fields,
+ HSContentProvider.HiddenService.PORT + "=" + port,
+ null
+ );
+
+ service.close();
+ }
+
+ } catch (JSONException e) {
+ e.printStackTrace();
+ Toast.makeText(mContext, R.string.error, Toast.LENGTH_LONG).show();
+ }
+
+ Toast.makeText(mContext, R.string.backup_restored, Toast.LENGTH_LONG).show();
+ }
+
+ public void restoreKeyBackup(int hsPort, Uri hsKeyPath) {
+ File mHSBasePath = new File(
+ mContext.getFilesDir().getAbsolutePath(),
+ TorServiceConstants.HIDDEN_SERVICES_DIR
+ );
+
+ File serviceDir = new File(mHSBasePath, "hs" + hsPort);
+
+ if (!serviceDir.isDirectory())
+ serviceDir.mkdirs();
+
+ try {
+ ParcelFileDescriptor mInputPFD = mContext.getContentResolver().openFileDescriptor(hsKeyPath, "r");
+ InputStream fileStream = new FileInputStream(mInputPFD.getFileDescriptor());
+ OutputStream file = new FileOutputStream(serviceDir.getAbsolutePath() + "/private_key");
+
+ byte[] buffer = new byte[1024];
+ int length;
+ while ((length = fileStream.read(buffer)) > 0) {
+ file.write(buffer, 0, length);
+ }
+ file.close();
+
+ } catch (IOException | NullPointerException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public void restoreCookieBackup(File p) {
+ File config = new File(p.getAbsolutePath());
+ FileInputStream stream;
+ String jString = null;
+
+ try {
+ stream = new FileInputStream(config);
+ FileChannel fc = stream.getChannel();
+ MappedByteBuffer bb = fc.map(FileChannel.MapMode.READ_ONLY, 0, fc.size());
+ jString = Charset.defaultCharset().decode(bb).toString();
+ stream.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ if (jString == null)
+ Toast.makeText(mContext, R.string.error, Toast.LENGTH_LONG).show();
+
+ try {
+ JSONObject savedValues = new JSONObject(jString);
+ ContentValues fields = new ContentValues();
+
+ fields.put(
+ CookieContentProvider.ClientCookie.DOMAIN,
+ savedValues.getString(CookieContentProvider.ClientCookie.DOMAIN)
+ );
+
+ fields.put(
+ CookieContentProvider.ClientCookie.AUTH_COOKIE_VALUE,
+ savedValues.getString(CookieContentProvider.ClientCookie.AUTH_COOKIE_VALUE)
+ );
+
+ fields.put(
+ CookieContentProvider.ClientCookie.ENABLED,
+ savedValues.getInt(CookieContentProvider.ClientCookie.ENABLED)
+ );
+
+ mResolver.insert(CookieContentProvider.CONTENT_URI, fields);
+
+ } catch (JSONException e) {
+ e.printStackTrace();
+ Toast.makeText(mContext, R.string.error, Toast.LENGTH_LONG).show();
+ }
+
+ Toast.makeText(mContext, R.string.backup_restored, Toast.LENGTH_LONG).show();
+ }
+
+ public String createCookieBackup(String domain, String cookie, Integer enabled) {
+ File storage_path = ExternalStorage.getOrCreateBackupDir();
+ String backupFile = storage_path.getAbsolutePath() + '/' + domain.replace(".onion", ".json");
+
+ JSONObject backup = new JSONObject();
+ try {
+ backup.put(CookieContentProvider.ClientCookie.DOMAIN, domain);
+ backup.put(CookieContentProvider.ClientCookie.AUTH_COOKIE_VALUE, cookie);
+ backup.put(CookieContentProvider.ClientCookie.ENABLED, enabled);
+ FileWriter file = new FileWriter(backupFile);
+ file.write(backup.toString());
+ file.close();
+ } catch (JSONException | IOException e) {
+ e.printStackTrace();
+ return null;
+ }
+
+ return backupFile;
+ }
+}
diff --git a/app/src/main/java/org/torproject/android/ui/hiddenservices/backup/ZipIt.java b/app/src/main/java/org/torproject/android/ui/hiddenservices/backup/ZipIt.java
new file mode 100644
index 00000000..130b56bb
--- /dev/null
+++ b/app/src/main/java/org/torproject/android/ui/hiddenservices/backup/ZipIt.java
@@ -0,0 +1,101 @@
+package org.torproject.android.ui.hiddenservices.backup;
+
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+import java.util.zip.ZipOutputStream;
+
+public class ZipIt {
+ private static final int BUFFER = 2048;
+
+ private String[] _files;
+ private String _zipFile;
+
+ public ZipIt(@Nullable String[] files, @NonNull String zipFile) {
+ _files = files;
+ _zipFile = zipFile;
+ }
+
+ public boolean zip() {
+ try {
+ BufferedInputStream origin;
+ FileOutputStream dest = new FileOutputStream(_zipFile);
+
+ ZipOutputStream out = new ZipOutputStream(new BufferedOutputStream(dest));
+
+ byte data[] = new byte[BUFFER];
+
+ for (String _file : _files) {
+ FileInputStream fi = new FileInputStream(_file);
+ origin = new BufferedInputStream(fi, BUFFER);
+ ZipEntry entry = new ZipEntry(_file.substring(_file.lastIndexOf("/") + 1));
+ out.putNextEntry(entry);
+ int count;
+ while ((count = origin.read(data, 0, BUFFER)) != -1) {
+ out.write(data, 0, count);
+ }
+ origin.close();
+ }
+
+ out.close();
+ } catch (Exception e) {
+ return false;
+ }
+
+ return true;
+ }
+
+ public boolean unzip(String output_path) {
+ InputStream is;
+ ZipInputStream zis;
+
+ try {
+ String filename;
+ is = new FileInputStream(_zipFile);
+ zis = new ZipInputStream(new BufferedInputStream(is));
+ ZipEntry ze;
+ byte[] buffer = new byte[1024];
+ int count;
+
+ while ((ze = zis.getNextEntry()) != null) {
+ // zapis do souboru
+ filename = ze.getName();
+
+ // Need to create directories if not exists, or
+ // it will generate an Exception...
+ if (ze.isDirectory()) {
+ File fmd = new File(output_path + "/" + filename);
+ fmd.mkdirs();
+ continue;
+ }
+
+ FileOutputStream fout = new FileOutputStream(output_path + "/" + filename);
+
+ // cteni zipu a zapis
+ while ((count = zis.read(buffer)) != -1) {
+ fout.write(buffer, 0, count);
+ }
+
+ fout.close();
+ zis.closeEntry();
+ }
+
+ zis.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ return false;
+ }
+
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/torproject/android/ui/hiddenservices/database/HSDatabase.java b/app/src/main/java/org/torproject/android/ui/hiddenservices/database/HSDatabase.java
new file mode 100644
index 00000000..18bc7aa0
--- /dev/null
+++ b/app/src/main/java/org/torproject/android/ui/hiddenservices/database/HSDatabase.java
@@ -0,0 +1,47 @@
+package org.torproject.android.ui.hiddenservices.database;
+
+
+import android.content.Context;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+
+public class HSDatabase extends SQLiteOpenHelper {
+
+ public static final String HS_DATA_TABLE_NAME = "hs_data";
+ public static final String HS_CLIENT_COOKIE_TABLE_NAME = "hs_client_cookie";
+ private static final int DATABASE_VERSION = 2;
+ private static final String DATABASE_NAME = "hidden_services";
+ private static final String HS_DATA_TABLE_CREATE =
+ "CREATE TABLE " + HS_DATA_TABLE_NAME + " (" +
+ "_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
+ "name TEXT, " +
+ "domain TEXT, " +
+ "onion_port INTEGER, " +
+ "auth_cookie INTEGER DEFAULT 0, " +
+ "auth_cookie_value TEXT, " +
+ "created_by_user INTEGER DEFAULT 0, " +
+ "enabled INTEGER DEFAULT 1, " +
+ "port INTEGER);";
+
+ private static final String HS_CLIENT_COOKIE_TABLE_CREATE =
+ "CREATE TABLE " + HS_CLIENT_COOKIE_TABLE_NAME + " (" +
+ "_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
+ "domain TEXT, " +
+ "auth_cookie_value TEXT, " +
+ "enabled INTEGER DEFAULT 1);";
+
+ public HSDatabase(Context context) {
+ super(context, DATABASE_NAME, null, DATABASE_VERSION);
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ db.execSQL(HS_DATA_TABLE_CREATE);
+ db.execSQL(HS_CLIENT_COOKIE_TABLE_CREATE);
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ }
+}
+
diff --git a/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/AddCookieDialog.java b/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/AddCookieDialog.java
new file mode 100644
index 00000000..75663648
--- /dev/null
+++ b/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/AddCookieDialog.java
@@ -0,0 +1,84 @@
+package org.torproject.android.ui.hiddenservices.dialogs;
+
+
+import android.app.Dialog;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v4.app.DialogFragment;
+import android.support.v7.app.AlertDialog;
+import android.view.View;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.Toast;
+
+import org.torproject.android.R;
+import org.torproject.android.ui.hiddenservices.providers.CookieContentProvider;
+
+public class AddCookieDialog extends DialogFragment {
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+
+ final View dialog_view = getActivity().getLayoutInflater().inflate(R.layout.layout_add_client_cookie_dialog, null);
+
+ final AlertDialog addCookieDialog = new AlertDialog.Builder(getActivity())
+ .setView(dialog_view)
+ .setTitle(R.string.client_cookies)
+ .create();
+
+ Button save = (Button) dialog_view.findViewById(R.id.cookie_dialog_save);
+ save.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ String onion = ((EditText) dialog_view.findViewById(R.id.cookie_onion)).getText().toString();
+ String cookie = ((EditText) dialog_view.findViewById(R.id.cookie_value)).getText().toString();
+
+ if (checkInput(onion, cookie)) {
+ saveData(onion, cookie);
+ Toast.makeText(
+ v.getContext(), R.string.please_restart_Orbot_to_enable_the_changes, Toast.LENGTH_LONG
+ ).show();
+ addCookieDialog.dismiss();
+ }
+ }
+ });
+
+ Button cancel = (Button) dialog_view.findViewById(R.id.cookie_dialog_cancel);
+ cancel.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ addCookieDialog.cancel();
+ }
+ });
+
+ return addCookieDialog;
+ }
+
+ private boolean checkInput(String onion, String cookie) {
+
+ boolean is_set = ((onion != null && onion.length() > 0) && (cookie != null && cookie.length() > 0));
+ if (!is_set) {
+ Toast.makeText(getContext(), R.string.fields_can_t_be_empty, Toast.LENGTH_SHORT).show();
+ return false;
+ }
+
+ if (!onion.matches("([a-z0-9]{16})\\.onion")) {
+ Toast.makeText(getContext(), R.string.invalid_onion_address, Toast.LENGTH_SHORT).show();
+ return false;
+ }
+
+ return true;
+ }
+
+ private void saveData(String domain, String cookie) {
+
+ ContentValues fields = new ContentValues();
+ fields.put(CookieContentProvider.ClientCookie.DOMAIN, domain);
+ fields.put(CookieContentProvider.ClientCookie.AUTH_COOKIE_VALUE, cookie);
+
+ ContentResolver cr = getContext().getContentResolver();
+
+ cr.insert(CookieContentProvider.CONTENT_URI, fields);
+ }
+}
diff --git a/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/CookieActionsDialog.java b/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/CookieActionsDialog.java
new file mode 100644
index 00000000..7b5a2cfe
--- /dev/null
+++ b/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/CookieActionsDialog.java
@@ -0,0 +1,95 @@
+package org.torproject.android.ui.hiddenservices.dialogs;
+
+
+import android.app.Dialog;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v4.app.DialogFragment;
+import android.support.v7.app.AlertDialog;
+import android.view.View;
+import android.widget.Button;
+import android.widget.Toast;
+
+import org.torproject.android.R;
+import org.torproject.android.ui.hiddenservices.backup.BackupUtils;
+import org.torproject.android.ui.hiddenservices.permissions.PermissionManager;
+
+public class CookieActionsDialog extends DialogFragment {
+ public static final int WRITE_EXTERNAL_STORAGE_FROM_COOKIE_ACTION_DIALOG = 4;
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ final Bundle arguments = getArguments();
+
+ final View dialog_view = getActivity().getLayoutInflater().inflate(R.layout.layout_cookie_actions, null);
+ final AlertDialog actionDialog = new AlertDialog.Builder(getActivity())
+ .setView(dialog_view)
+ .setTitle(R.string.client_cookies)
+ .create();
+
+ Button backup = (Button) dialog_view.findViewById(R.id.btn_cookie_backup);
+ backup.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ Context mContext = v.getContext();
+
+ if (PermissionManager.isLollipopOrHigher()
+ && !PermissionManager.hasExternalWritePermission(mContext)) {
+
+ PermissionManager.requestExternalWritePermissions(
+ getActivity(), WRITE_EXTERNAL_STORAGE_FROM_COOKIE_ACTION_DIALOG);
+
+ return;
+ }
+
+ BackupUtils backup_utils = new BackupUtils(mContext);
+ String backupPath = backup_utils.createCookieBackup(
+ arguments.getString("domain"),
+ arguments.getString("auth_cookie_value"),
+ arguments.getInt("enabled")
+ );
+
+ if (backupPath == null || backupPath.length() < 1) {
+ Toast.makeText(mContext, R.string.error, Toast.LENGTH_LONG).show();
+ actionDialog.dismiss();
+ return;
+ }
+
+ Toast.makeText(mContext, R.string.backup_saved_at_external_storage, Toast.LENGTH_LONG).show();
+
+ Uri selectedUri = Uri.parse(backupPath.substring(0, backupPath.lastIndexOf("/")));
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setDataAndType(selectedUri, "resource/folder");
+
+ if (intent.resolveActivityInfo(mContext.getPackageManager(), 0) != null) {
+ startActivity(intent);
+ } else {
+ Toast.makeText(mContext, R.string.filemanager_not_available, Toast.LENGTH_LONG).show();
+ }
+ actionDialog.dismiss();
+ }
+ });
+
+ Button delete = (Button) dialog_view.findViewById(R.id.btn_cookie_delete);
+ delete.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ CookieDeleteDialog dialog = new CookieDeleteDialog();
+ dialog.setArguments(arguments);
+ dialog.show(getFragmentManager(), "CookieDeleteDialog");
+ actionDialog.dismiss();
+ }
+ });
+
+ Button cancel = (Button) dialog_view.findViewById(R.id.btn_cookie_cancel);
+ cancel.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ actionDialog.dismiss();
+ }
+ });
+
+ return actionDialog;
+ }
+}
diff --git a/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/CookieDeleteDialog.java b/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/CookieDeleteDialog.java
new file mode 100644
index 00000000..60c4d8e3
--- /dev/null
+++ b/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/CookieDeleteDialog.java
@@ -0,0 +1,50 @@
+package org.torproject.android.ui.hiddenservices.dialogs;
+
+
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v4.app.DialogFragment;
+import android.support.v7.app.AlertDialog;
+
+import org.torproject.android.R;
+import org.torproject.android.ui.hiddenservices.providers.CookieContentProvider;
+
+public class CookieDeleteDialog extends DialogFragment {
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ final Bundle arguments = getArguments();
+ final Context context = getContext();
+
+ DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ switch (which) {
+ case DialogInterface.BUTTON_POSITIVE:
+ // Delete from db
+ context.getContentResolver().delete(
+ CookieContentProvider.CONTENT_URI,
+ CookieContentProvider.ClientCookie._ID + "=" + arguments.getInt("_id"),
+ null
+ );
+
+ break;
+
+ case DialogInterface.BUTTON_NEGATIVE:
+ // Do nothing
+ break;
+ }
+ }
+ };
+
+ return new AlertDialog.Builder(context)
+ .setMessage(R.string.confirm_cookie_deletion)
+ .setPositiveButton(R.string.btn_okay, dialogClickListener)
+ .setNegativeButton(R.string.btn_cancel, dialogClickListener)
+ .create();
+ }
+}
diff --git a/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/HSActionsDialog.java b/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/HSActionsDialog.java
new file mode 100644
index 00000000..5be76917
--- /dev/null
+++ b/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/HSActionsDialog.java
@@ -0,0 +1,130 @@
+package org.torproject.android.ui.hiddenservices.dialogs;
+
+
+import android.app.Dialog;
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v4.app.DialogFragment;
+import android.support.v7.app.AlertDialog;
+import android.view.View;
+import android.widget.Button;
+import android.widget.Toast;
+
+import org.torproject.android.R;
+import org.torproject.android.ui.hiddenservices.backup.BackupUtils;
+import org.torproject.android.ui.hiddenservices.permissions.PermissionManager;
+
+public class HSActionsDialog extends DialogFragment {
+ public static final int WRITE_EXTERNAL_STORAGE_FROM_ACTION_DIALOG = 2;
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ final Bundle arguments = getArguments();
+
+ final View dialog_view = getActivity().getLayoutInflater().inflate(R.layout.layout_hs_actions, null);
+ final AlertDialog actionDialog = new AlertDialog.Builder(getActivity())
+ .setView(dialog_view)
+ .setTitle(R.string.hidden_services)
+ .create();
+
+ Button backup = (Button) dialog_view.findViewById(R.id.btn_hs_backup);
+ backup.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ Context mContext = v.getContext();
+
+ if (PermissionManager.isLollipopOrHigher()
+ && !PermissionManager.hasExternalWritePermission(mContext)) {
+
+ PermissionManager.requestExternalWritePermissions(
+ getActivity(), WRITE_EXTERNAL_STORAGE_FROM_ACTION_DIALOG);
+
+ return;
+ }
+
+ BackupUtils hsutils = new BackupUtils(mContext);
+ String backupPath = hsutils.createZipBackup(Integer.parseInt(arguments.getString("port")));
+
+ if (backupPath == null || backupPath.length() < 1) {
+ Toast.makeText(mContext, R.string.error, Toast.LENGTH_LONG).show();
+ actionDialog.dismiss();
+ return;
+ }
+
+ Toast.makeText(mContext, R.string.backup_saved_at_external_storage, Toast.LENGTH_LONG).show();
+
+ Uri selectedUri = Uri.parse(backupPath.substring(0, backupPath.lastIndexOf("/")));
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setDataAndType(selectedUri, "resource/folder");
+
+ if (intent.resolveActivityInfo(mContext.getPackageManager(), 0) != null) {
+ startActivity(intent);
+ } else {
+ Toast.makeText(mContext, R.string.filemanager_not_available, Toast.LENGTH_LONG).show();
+ }
+ actionDialog.dismiss();
+ }
+ });
+
+ Button copy = (Button) dialog_view.findViewById(R.id.btn_hs_clipboard);
+ copy.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ Context mContext = v.getContext();
+ ClipboardManager clipboard = (ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE);
+ ClipData clip = ClipData.newPlainText("onion", arguments.getString("onion"));
+ clipboard.setPrimaryClip(clip);
+ Toast.makeText(mContext, R.string.done, Toast.LENGTH_LONG).show();
+ actionDialog.dismiss();
+ }
+ });
+
+ Button showAuth = (Button) dialog_view.findViewById(R.id.bt_hs_show_auth);
+ showAuth.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ String auth_cookie_value = arguments.getString("auth_cookie_value");
+
+ if (arguments.getInt("auth_cookie") == 1) {
+ if (auth_cookie_value == null || auth_cookie_value.length() < 1) {
+ Toast.makeText(
+ v.getContext(), R.string.please_restart_Orbot_to_enable_the_changes, Toast.LENGTH_LONG
+ ).show();
+ } else {
+ HSCookieDialog dialog = new HSCookieDialog();
+ dialog.setArguments(arguments);
+ dialog.show(getFragmentManager(), "HSCookieDialog");
+ }
+ } else {
+ Toast.makeText(
+ v.getContext(), R.string.auth_cookie_was_not_configured, Toast.LENGTH_LONG
+ ).show();
+ }
+
+ actionDialog.dismiss();
+ }
+ });
+
+ Button delete = (Button) dialog_view.findViewById(R.id.btn_hs_delete);
+ delete.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ HSDeleteDialog dialog = new HSDeleteDialog();
+ dialog.setArguments(arguments);
+ dialog.show(getFragmentManager(), "HSDeleteDialog");
+ actionDialog.dismiss();
+ }
+ });
+
+ Button cancel = (Button) dialog_view.findViewById(R.id.btn_hs_cancel);
+ cancel.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ actionDialog.dismiss();
+ }
+ });
+
+ return actionDialog;
+ }
+}
diff --git a/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/HSCookieDialog.java b/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/HSCookieDialog.java
new file mode 100644
index 00000000..39c1b510
--- /dev/null
+++ b/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/HSCookieDialog.java
@@ -0,0 +1,81 @@
+package org.torproject.android.ui.hiddenservices.dialogs;
+
+import android.app.Dialog;
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.Context;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v4.app.DialogFragment;
+import android.support.v7.app.AlertDialog;
+import android.view.View;
+import android.widget.Button;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.google.zxing.integration.android.IntentIntegrator;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.torproject.android.R;
+import org.torproject.android.ui.hiddenservices.providers.CookieContentProvider;
+
+public class HSCookieDialog extends DialogFragment {
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ final View dialog_view = getActivity().getLayoutInflater().inflate(R.layout.layout_hs_cookie, null);
+
+ final Bundle arguments = getArguments();
+ final String auth_cookie_value = arguments.getString("auth_cookie_value");
+
+ final AlertDialog cookieDialog = new AlertDialog.Builder(getActivity())
+ .setView(dialog_view)
+ .create();
+
+ TextView cookie = (TextView) dialog_view.findViewById(R.id.hs_cookie);
+ cookie.setText(auth_cookie_value);
+
+ Button clipboard = (Button) dialog_view.findViewById(R.id.hs_cookie_to_clipboard);
+ clipboard.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ Context mContext = v.getContext();
+ ClipboardManager clipboard = (ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE);
+ ClipData clip = ClipData.newPlainText("cookie", auth_cookie_value);
+ clipboard.setPrimaryClip(clip);
+ Toast.makeText(mContext, R.string.done, Toast.LENGTH_LONG).show();
+ cookieDialog.dismiss();
+ }
+ });
+
+ Button shareQR = (Button) dialog_view.findViewById(R.id.hs_cookie_to_qr);
+ shareQR.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ try {
+ JSONObject backup = new JSONObject();
+ backup.put(CookieContentProvider.ClientCookie.DOMAIN, arguments.getString("onion"));
+ backup.put(CookieContentProvider.ClientCookie.AUTH_COOKIE_VALUE, arguments.getString("auth_cookie_value"));
+
+ IntentIntegrator integrator = new IntentIntegrator(getActivity());
+ integrator.shareText(backup.toString());
+
+ } catch (JSONException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+
+ cookieDialog.dismiss();
+ }
+ });
+
+ Button cancel = (Button) dialog_view.findViewById(R.id.hs_cookie_cancel);
+ cancel.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ cookieDialog.dismiss();
+ }
+ });
+
+ return cookieDialog;
+ }
+}
diff --git a/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/HSDataDialog.java b/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/HSDataDialog.java
new file mode 100644
index 00000000..98255604
--- /dev/null
+++ b/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/HSDataDialog.java
@@ -0,0 +1,102 @@
+package org.torproject.android.ui.hiddenservices.dialogs;
+
+
+import android.app.Dialog;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v4.app.DialogFragment;
+import android.support.v7.app.AlertDialog;
+import android.view.View;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.EditText;
+import android.widget.Toast;
+
+import org.torproject.android.R;
+import org.torproject.android.ui.hiddenservices.providers.HSContentProvider;
+
+public class HSDataDialog extends DialogFragment {
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ // Get the layout
+ final View dialog_view = getActivity().getLayoutInflater().inflate(R.layout.layout_hs_data_dialog, null);
+
+ // Use the Builder class for convenient dialog construction
+ final AlertDialog serviceDataDialog = new AlertDialog.Builder(getActivity())
+ .setView(dialog_view)
+ .setTitle(R.string.hidden_services)
+ .create();
+
+ // Buttons action
+ Button save = (Button) dialog_view.findViewById(R.id.HSDialogSave);
+ save.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ String serverName = ((EditText) dialog_view.findViewById(R.id.hsName)).getText().toString();
+ Integer localPort = Integer.parseInt(
+ ((EditText) dialog_view.findViewById(R.id.hsLocalPort)).getText().toString()
+ );
+ Integer onionPort = Integer.parseInt(
+ ((EditText) dialog_view.findViewById(R.id.hsOnionPort)).getText().toString()
+ );
+
+ Boolean authCookie = ((CheckBox) dialog_view.findViewById(R.id.hsAuth)).isChecked();
+
+ if (checkInput(serverName, localPort, onionPort)) {
+ saveData(serverName, localPort, onionPort, authCookie);
+ Toast.makeText(
+ v.getContext(), R.string.please_restart_Orbot_to_enable_the_changes, Toast.LENGTH_LONG
+ ).show();
+ serviceDataDialog.dismiss();
+ }
+ }
+ });
+
+ Button cancel = (Button) dialog_view.findViewById(R.id.HSDialogCancel);
+ cancel.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ serviceDataDialog.cancel();
+ }
+ });
+
+ return serviceDataDialog;
+ }
+
+ private boolean checkInput(String serverName, Integer local, Integer remote) {
+ boolean is_ok = true;
+ Integer error_msg = 0;
+
+ if ((local < 1 || local > 65535) || (remote < 1 || remote > 65535)) {
+ error_msg = R.string.invalid_port;
+ is_ok = false;
+ }
+
+ if (serverName == null || serverName.length() < 1) {
+ error_msg = R.string.name_can_t_be_empty;
+ is_ok = false;
+ }
+
+ if (!is_ok) {
+ Toast.makeText(getContext(), error_msg, Toast.LENGTH_SHORT).show();
+ }
+
+ return is_ok;
+ }
+
+ private void saveData(String name, Integer local, Integer remote, Boolean authCookie) {
+
+ ContentValues fields = new ContentValues();
+ fields.put(HSContentProvider.HiddenService.NAME, name);
+ fields.put(HSContentProvider.HiddenService.PORT, local);
+ fields.put(HSContentProvider.HiddenService.ONION_PORT, remote);
+ fields.put(HSContentProvider.HiddenService.AUTH_COOKIE, authCookie);
+ fields.put(HSContentProvider.HiddenService.CREATED_BY_USER, 1);
+
+ ContentResolver cr = getContext().getContentResolver();
+
+ cr.insert(HSContentProvider.CONTENT_URI, fields);
+ }
+}
diff --git a/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/HSDeleteDialog.java b/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/HSDeleteDialog.java
new file mode 100644
index 00000000..ac5df743
--- /dev/null
+++ b/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/HSDeleteDialog.java
@@ -0,0 +1,65 @@
+package org.torproject.android.ui.hiddenservices.dialogs;
+
+
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v4.app.DialogFragment;
+import android.support.v7.app.AlertDialog;
+
+import org.torproject.android.R;
+import org.torproject.android.service.TorServiceConstants;
+import org.torproject.android.ui.hiddenservices.providers.HSContentProvider;
+
+import java.io.File;
+
+public class HSDeleteDialog extends DialogFragment {
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ final Bundle arguments = getArguments();
+ final Context context = getContext();
+
+ DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ switch (which) {
+ case DialogInterface.BUTTON_POSITIVE:
+ // Delete from db
+ context.getContentResolver().delete(
+ HSContentProvider.CONTENT_URI,
+ HSContentProvider.HiddenService._ID + "=" + arguments.getInt("_id"),
+ null
+ );
+
+ // Delete from interal storage
+ String base = context.getFilesDir().getAbsolutePath() + "/" + TorServiceConstants.HIDDEN_SERVICES_DIR;
+ File dir = new File(base, "hs" + arguments.getString("port"));
+
+ if (dir.isDirectory()) {
+ String[] children = dir.list();
+ for (String aChildren : children) {
+ new File(dir, aChildren).delete();
+ }
+ dir.delete();
+ }
+
+ break;
+
+ case DialogInterface.BUTTON_NEGATIVE:
+ // Do nothing
+ break;
+ }
+ }
+ };
+
+ return new AlertDialog.Builder(context)
+ .setMessage(R.string.confirm_service_deletion)
+ .setPositiveButton(R.string.btn_okay, dialogClickListener)
+ .setNegativeButton(R.string.btn_cancel, dialogClickListener)
+ .create();
+ }
+}
diff --git a/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/SelectCookieBackupDialog.java b/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/SelectCookieBackupDialog.java
new file mode 100644
index 00000000..de5b7848
--- /dev/null
+++ b/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/SelectCookieBackupDialog.java
@@ -0,0 +1,84 @@
+package org.torproject.android.ui.hiddenservices.dialogs;
+
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v4.app.DialogFragment;
+import android.support.v7.app.AlertDialog;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.ListView;
+
+import org.torproject.android.R;
+import org.torproject.android.ui.hiddenservices.adapters.BackupAdapter;
+import org.torproject.android.ui.hiddenservices.backup.BackupUtils;
+import org.torproject.android.ui.hiddenservices.storage.ExternalStorage;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class SelectCookieBackupDialog extends DialogFragment {
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ AlertDialog.Builder cookieBackupDialog = new AlertDialog.Builder(getActivity());
+
+ cookieBackupDialog.setTitle(R.string.restore_backup);
+
+ File backupDir = ExternalStorage.getOrCreateBackupDir();
+ File[] files = null;
+
+ try {
+ files = backupDir.listFiles(new FilenameFilter() {
+ @Override
+ public boolean accept(File dir, String name) {
+ return name.toLowerCase().endsWith(".json");
+ }
+ });
+ } catch (NullPointerException e) {
+ // Silent block
+ }
+
+ if (files == null || files.length < 1) {
+ cookieBackupDialog.setMessage(R.string.create_a_backup_first);
+ cookieBackupDialog.setNegativeButton(R.string.btn_cancel, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ dialog.dismiss();
+ }
+ });
+
+ return cookieBackupDialog.create();
+ }
+
+ final View dialog_view = getActivity().getLayoutInflater().inflate(R.layout.layout_hs_backups_list, null);
+
+ cookieBackupDialog.setView(dialog_view);
+ cookieBackupDialog.setPositiveButton(R.string.btn_okay, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ dialog.dismiss();
+ }
+ });
+
+ ListView backups = (ListView) dialog_view.findViewById(R.id.listview_hs_backups);
+
+ List json_backups = new ArrayList<>();
+ Collections.addAll(json_backups, files);
+
+ backups.setAdapter(new BackupAdapter(getContext(), R.layout.layout_hs_backups_list_item, json_backups));
+ backups.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView> parent, View view, int position, long id) {
+ BackupUtils backupUtils = new BackupUtils(view.getContext().getApplicationContext());
+ File p = (File) parent.getItemAtPosition(position);
+ backupUtils.restoreCookieBackup(p);
+ }
+ });
+
+ return cookieBackupDialog.create();
+ }
+}
diff --git a/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/SelectHSBackupDialog.java b/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/SelectHSBackupDialog.java
new file mode 100644
index 00000000..a6e3bac9
--- /dev/null
+++ b/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/SelectHSBackupDialog.java
@@ -0,0 +1,84 @@
+package org.torproject.android.ui.hiddenservices.dialogs;
+
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v4.app.DialogFragment;
+import android.support.v7.app.AlertDialog;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.ListView;
+
+import org.torproject.android.R;
+import org.torproject.android.ui.hiddenservices.adapters.BackupAdapter;
+import org.torproject.android.ui.hiddenservices.backup.BackupUtils;
+import org.torproject.android.ui.hiddenservices.storage.ExternalStorage;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class SelectHSBackupDialog extends DialogFragment {
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ AlertDialog.Builder backupsDialog = new AlertDialog.Builder(getActivity());
+
+ backupsDialog.setTitle(R.string.restore_backup);
+
+ File backupDir = ExternalStorage.getOrCreateBackupDir();
+ File[] files = null;
+
+ try {
+ files = backupDir.listFiles(new FilenameFilter() {
+ @Override
+ public boolean accept(File dir, String name) {
+ return name.toLowerCase().endsWith(".zip");
+ }
+ });
+ } catch (NullPointerException e) {
+ // Silent block
+ }
+
+ if (files == null || files.length < 1) {
+ backupsDialog.setMessage(R.string.create_a_backup_first);
+ backupsDialog.setNegativeButton(R.string.btn_cancel, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ dialog.dismiss();
+ }
+ });
+
+ return backupsDialog.create();
+ }
+
+ final View dialog_view = getActivity().getLayoutInflater().inflate(R.layout.layout_hs_backups_list, null);
+
+ backupsDialog.setView(dialog_view);
+ backupsDialog.setPositiveButton(R.string.btn_okay, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ dialog.dismiss();
+ }
+ });
+
+ ListView backups = (ListView) dialog_view.findViewById(R.id.listview_hs_backups);
+
+ List zips = new ArrayList<>();
+ Collections.addAll(zips, files);
+
+ backups.setAdapter(new BackupAdapter(getContext(), R.layout.layout_hs_backups_list_item, zips));
+ backups.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView> parent, View view, int position, long id) {
+ BackupUtils backupUtils = new BackupUtils(view.getContext().getApplicationContext());
+ File p = (File) parent.getItemAtPosition(position);
+ backupUtils.restoreZipBackup(p);
+ }
+ });
+
+ return backupsDialog.create();
+ }
+}
diff --git a/app/src/main/java/org/torproject/android/ui/hiddenservices/permissions/PermissionManager.java b/app/src/main/java/org/torproject/android/ui/hiddenservices/permissions/PermissionManager.java
new file mode 100644
index 00000000..a9df5a95
--- /dev/null
+++ b/app/src/main/java/org/torproject/android/ui/hiddenservices/permissions/PermissionManager.java
@@ -0,0 +1,103 @@
+package org.torproject.android.ui.hiddenservices.permissions;
+
+
+import android.Manifest;
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Build;
+import android.os.PowerManager;
+import android.provider.Settings;
+import android.support.design.widget.Snackbar;
+import android.support.v4.app.ActivityCompat;
+import android.support.v4.app.FragmentActivity;
+import android.view.View;
+
+import org.torproject.android.R;
+
+public class PermissionManager {
+ public static int VERY_LONG_LENGTH = 6000;
+
+ public static boolean isLollipopOrHigher() {
+ return (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP);
+ }
+
+ @SuppressLint("NewApi")
+ public static boolean hasExternalWritePermission(Context context) {
+ return (context.checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED);
+ }
+
+ public static void requestExternalWritePermissions(FragmentActivity activity, int action) {
+ final int mAction = action;
+ final FragmentActivity mActivity = activity;
+
+ if (ActivityCompat.shouldShowRequestPermissionRationale
+ (mActivity, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
+ Snackbar.make(mActivity.findViewById(android.R.id.content),
+ R.string.please_grant_permissions_for_external_storage,
+ Snackbar.LENGTH_INDEFINITE).setAction(R.string.activate,
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ ActivityCompat.requestPermissions(mActivity,
+ new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
+ mAction);
+ }
+ }).show();
+ } else {
+ ActivityCompat.requestPermissions(mActivity,
+ new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
+ mAction);
+ }
+ }
+
+ @TargetApi(Build.VERSION_CODES.M)
+ public static void requestBatteryPermmssions(FragmentActivity activity, Context context) {
+ final Context mContext = context;
+ final String packageName = mContext.getPackageName();
+ PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
+
+ if (pm.isIgnoringBatteryOptimizations(packageName))
+ return;
+
+ Snackbar.make(activity.findViewById(android.R.id.content),
+ R.string.consider_disable_battery_optimizations,
+ VERY_LONG_LENGTH).setAction(R.string.disable,
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Intent intent = new Intent();
+ intent.setAction(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
+ intent.setData(Uri.parse("package:" + packageName));
+ mContext.startActivity(intent);
+ }
+ }).show();
+ }
+
+ @TargetApi(Build.VERSION_CODES.M)
+ public static void requestDropBatteryPermmssions(FragmentActivity activity, Context context) {
+ final Context mContext = context;
+
+ final String packageName = context.getPackageName();
+ PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
+
+ if (!pm.isIgnoringBatteryOptimizations(packageName))
+ return;
+
+ Snackbar.make(activity.findViewById(android.R.id.content),
+ R.string.consider_enable_battery_optimizations,
+ VERY_LONG_LENGTH).setAction(R.string.enable,
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Intent intent = new Intent();
+ intent.setAction(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS);
+ mContext.startActivity(intent);
+ }
+ }).show();
+ }
+}
+
diff --git a/app/src/main/java/org/torproject/android/ui/hiddenservices/providers/CookieContentProvider.java b/app/src/main/java/org/torproject/android/ui/hiddenservices/providers/CookieContentProvider.java
new file mode 100644
index 00000000..f420a6d8
--- /dev/null
+++ b/app/src/main/java/org/torproject/android/ui/hiddenservices/providers/CookieContentProvider.java
@@ -0,0 +1,134 @@
+package org.torproject.android.ui.hiddenservices.providers;
+
+import android.content.ContentProvider;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.UriMatcher;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.net.Uri;
+import android.provider.BaseColumns;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+import org.torproject.android.ui.hiddenservices.database.HSDatabase;
+
+
+public class CookieContentProvider extends ContentProvider {
+ public static final String[] PROJECTION = new String[]{
+ ClientCookie._ID,
+ ClientCookie.DOMAIN,
+ ClientCookie.AUTH_COOKIE_VALUE,
+ ClientCookie.ENABLED
+ };
+ private static final String AUTH = "org.torproject.android.ui.hiddenservices.providers.cookie";
+ public static final Uri CONTENT_URI =
+ Uri.parse("content://" + AUTH + "/cookie");
+ //UriMatcher
+ private static final int COOKIES = 1;
+ private static final int COOKIE_ID = 2;
+
+ private static final UriMatcher uriMatcher;
+
+ static {
+ uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
+ uriMatcher.addURI(AUTH, "hs", COOKIES);
+ uriMatcher.addURI(AUTH, "hs/#", COOKIE_ID);
+ }
+
+ private HSDatabase mServervices;
+ private Context mContext;
+
+ @Override
+ public boolean onCreate() {
+ mContext = getContext();
+ mServervices = new HSDatabase(mContext);
+ return true;
+ }
+
+ @Nullable
+ @Override
+ public Cursor query(@NonNull Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
+ String where = selection;
+ if (uriMatcher.match(uri) == COOKIE_ID) {
+ where = "_id=" + uri.getLastPathSegment();
+ }
+
+ SQLiteDatabase db = mServervices.getReadableDatabase();
+
+ return db.query(HSDatabase.HS_CLIENT_COOKIE_TABLE_NAME, projection, where,
+ selectionArgs, null, null, sortOrder);
+ }
+
+ @Nullable
+ @Override
+ public String getType(@NonNull Uri uri) {
+ int match = uriMatcher.match(uri);
+
+ switch (match) {
+ case COOKIES:
+ return "vnd.android.cursor.dir/vnd.torproject.cookies";
+ case COOKIE_ID:
+ return "vnd.android.cursor.item/vnd.torproject.cookie";
+ default:
+ return null;
+ }
+ }
+
+ @Nullable
+ @Override
+ public Uri insert(@NonNull Uri uri, ContentValues values) {
+ long regId;
+
+ SQLiteDatabase db = mServervices.getWritableDatabase();
+
+ regId = db.insert(HSDatabase.HS_CLIENT_COOKIE_TABLE_NAME, null, values);
+
+ mContext.getContentResolver().notifyChange(CONTENT_URI, null);
+
+ return ContentUris.withAppendedId(CONTENT_URI, regId);
+ }
+
+ @Override
+ public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) {
+
+ String where = selection;
+ if (uriMatcher.match(uri) == COOKIE_ID) {
+ where = "_id=" + uri.getLastPathSegment();
+ }
+
+ SQLiteDatabase db = mServervices.getWritableDatabase();
+
+ Integer rows = db.delete(HSDatabase.HS_CLIENT_COOKIE_TABLE_NAME, where, selectionArgs);
+
+ mContext.getContentResolver().notifyChange(CONTENT_URI, null);
+
+ return rows;
+
+ }
+
+ @Override
+ public int update(@NonNull Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+ SQLiteDatabase db = mServervices.getWritableDatabase();
+
+ String where = selection;
+ if (uriMatcher.match(uri) == COOKIE_ID) {
+ where = "_id=" + uri.getLastPathSegment();
+ }
+
+ Integer rows = db.update(HSDatabase.HS_CLIENT_COOKIE_TABLE_NAME, values, where, null);
+ mContext.getContentResolver().notifyChange(CONTENT_URI, null);
+
+ return rows;
+ }
+
+ public static final class ClientCookie implements BaseColumns {
+ public static final String DOMAIN = "domain";
+ public static final String AUTH_COOKIE_VALUE = "auth_cookie_value";
+ public static final String ENABLED = "enabled";
+
+ private ClientCookie() {
+ }
+ }
+}
diff --git a/app/src/main/java/org/torproject/android/ui/hiddenservices/providers/HSContentProvider.java b/app/src/main/java/org/torproject/android/ui/hiddenservices/providers/HSContentProvider.java
new file mode 100644
index 00000000..d7b558fd
--- /dev/null
+++ b/app/src/main/java/org/torproject/android/ui/hiddenservices/providers/HSContentProvider.java
@@ -0,0 +1,144 @@
+package org.torproject.android.ui.hiddenservices.providers;
+
+import android.content.ContentProvider;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.UriMatcher;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.net.Uri;
+import android.provider.BaseColumns;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+import org.torproject.android.ui.hiddenservices.database.HSDatabase;
+
+
+public class HSContentProvider extends ContentProvider {
+ public static final String[] PROJECTION = new String[]{
+ HiddenService._ID,
+ HiddenService.NAME,
+ HiddenService.PORT,
+ HiddenService.DOMAIN,
+ HiddenService.ONION_PORT,
+ HiddenService.AUTH_COOKIE,
+ HiddenService.AUTH_COOKIE_VALUE,
+ HiddenService.CREATED_BY_USER,
+ HiddenService.ENABLED
+ };
+ private static final String AUTH = "org.torproject.android.ui.hiddenservices.providers";
+ public static final Uri CONTENT_URI =
+ Uri.parse("content://" + AUTH + "/hs");
+ //UriMatcher
+ private static final int ONIONS = 1;
+ private static final int ONION_ID = 2;
+
+ private static final UriMatcher uriMatcher;
+
+ static {
+ uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
+ uriMatcher.addURI(AUTH, "hs", ONIONS);
+ uriMatcher.addURI(AUTH, "hs/#", ONION_ID);
+ }
+
+ private HSDatabase mServervices;
+ private Context mContext;
+
+ @Override
+ public boolean onCreate() {
+ mContext = getContext();
+ mServervices = new HSDatabase(mContext);
+ return true;
+ }
+
+ @Nullable
+ @Override
+ public Cursor query(@NonNull Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
+ String where = selection;
+ if (uriMatcher.match(uri) == ONION_ID) {
+ where = "_id=" + uri.getLastPathSegment();
+ }
+
+ SQLiteDatabase db = mServervices.getReadableDatabase();
+
+ return db.query(HSDatabase.HS_DATA_TABLE_NAME, projection, where,
+ selectionArgs, null, null, sortOrder);
+ }
+
+ @Nullable
+ @Override
+ public String getType(@NonNull Uri uri) {
+ int match = uriMatcher.match(uri);
+
+ switch (match) {
+ case ONIONS:
+ return "vnd.android.cursor.dir/vnd.torproject.onions";
+ case ONION_ID:
+ return "vnd.android.cursor.item/vnd.torproject.onion";
+ default:
+ return null;
+ }
+ }
+
+ @Nullable
+ @Override
+ public Uri insert(@NonNull Uri uri, ContentValues values) {
+ long regId;
+
+ SQLiteDatabase db = mServervices.getWritableDatabase();
+
+ regId = db.insert(HSDatabase.HS_DATA_TABLE_NAME, null, values);
+
+ mContext.getContentResolver().notifyChange(CONTENT_URI, null);
+
+ return ContentUris.withAppendedId(CONTENT_URI, regId);
+ }
+
+ @Override
+ public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) {
+
+ String where = selection;
+ if (uriMatcher.match(uri) == ONION_ID) {
+ where = "_id=" + uri.getLastPathSegment();
+ }
+
+ SQLiteDatabase db = mServervices.getWritableDatabase();
+
+ Integer rows = db.delete(HSDatabase.HS_DATA_TABLE_NAME, where, selectionArgs);
+
+ mContext.getContentResolver().notifyChange(CONTENT_URI, null);
+
+ return rows;
+
+ }
+
+ @Override
+ public int update(@NonNull Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+ SQLiteDatabase db = mServervices.getWritableDatabase();
+
+ String where = selection;
+ if (uriMatcher.match(uri) == ONION_ID) {
+ where = "_id=" + uri.getLastPathSegment();
+ }
+
+ Integer rows = db.update(HSDatabase.HS_DATA_TABLE_NAME, values, where, null);
+ mContext.getContentResolver().notifyChange(CONTENT_URI, null);
+
+ return rows;
+ }
+
+ public static final class HiddenService implements BaseColumns {
+ public static final String NAME = "name";
+ public static final String PORT = "port";
+ public static final String ONION_PORT = "onion_port";
+ public static final String DOMAIN = "domain";
+ public static final String AUTH_COOKIE = "auth_cookie";
+ public static final String AUTH_COOKIE_VALUE = "auth_cookie_value";
+ public static final String CREATED_BY_USER = "created_by_user";
+ public static final String ENABLED = "enabled";
+
+ private HiddenService() {
+ }
+ }
+}
diff --git a/app/src/main/java/org/torproject/android/ui/hiddenservices/storage/ExternalStorage.java b/app/src/main/java/org/torproject/android/ui/hiddenservices/storage/ExternalStorage.java
new file mode 100644
index 00000000..c50108b7
--- /dev/null
+++ b/app/src/main/java/org/torproject/android/ui/hiddenservices/storage/ExternalStorage.java
@@ -0,0 +1,34 @@
+package org.torproject.android.ui.hiddenservices.storage;
+
+import android.os.Environment;
+
+import java.io.File;
+
+public class ExternalStorage {
+ private static final String ORBOT_BACKUPS_DIR = "Orbot";
+
+ public static File getOrCreateBackupDir() {
+ if (!isExternalStorageWritable())
+ return null;
+
+ File dir = new File(Environment.getExternalStorageDirectory(), ORBOT_BACKUPS_DIR);
+
+ if (!dir.isDirectory() && !dir.mkdirs())
+ return null;
+
+ return dir;
+ }
+
+ /* Checks if external storage is available for read and write */
+ public static boolean isExternalStorageWritable() {
+ String state = Environment.getExternalStorageState();
+ return Environment.MEDIA_MOUNTED.equals(state);
+ }
+
+ /* Checks if external storage is available to at least read */
+ public static boolean isExternalStorageReadable() {
+ String state = Environment.getExternalStorageState();
+ return Environment.MEDIA_MOUNTED.equals(state) ||
+ Environment.MEDIA_MOUNTED_READ_ONLY.equals(state);
+ }
+}
diff --git a/app/src/main/res/layout/layout_activity_client_cookies.xml b/app/src/main/res/layout/layout_activity_client_cookies.xml
new file mode 100644
index 00000000..b8f0626e
--- /dev/null
+++ b/app/src/main/res/layout/layout_activity_client_cookies.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/layout_add_client_cookie_dialog.xml b/app/src/main/res/layout/layout_add_client_cookie_dialog.xml
new file mode 100644
index 00000000..4669a921
--- /dev/null
+++ b/app/src/main/res/layout/layout_add_client_cookie_dialog.xml
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/layout_client_cookie_list_item.xml b/app/src/main/res/layout/layout_client_cookie_list_item.xml
new file mode 100644
index 00000000..80eefb94
--- /dev/null
+++ b/app/src/main/res/layout/layout_client_cookie_list_item.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/layout_content_client_cookies.xml b/app/src/main/res/layout/layout_content_client_cookies.xml
new file mode 100644
index 00000000..3ee9a897
--- /dev/null
+++ b/app/src/main/res/layout/layout_content_client_cookies.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/layout_cookie_actions.xml b/app/src/main/res/layout/layout_cookie_actions.xml
new file mode 100644
index 00000000..64d8c1c6
--- /dev/null
+++ b/app/src/main/res/layout/layout_cookie_actions.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/layout_hs_actions.xml b/app/src/main/res/layout/layout_hs_actions.xml
new file mode 100644
index 00000000..0e5a6680
--- /dev/null
+++ b/app/src/main/res/layout/layout_hs_actions.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/layout_hs_backups_list.xml b/app/src/main/res/layout/layout_hs_backups_list.xml
new file mode 100644
index 00000000..521f381d
--- /dev/null
+++ b/app/src/main/res/layout/layout_hs_backups_list.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/layout_hs_backups_list_item.xml b/app/src/main/res/layout/layout_hs_backups_list_item.xml
new file mode 100644
index 00000000..6d9bfd44
--- /dev/null
+++ b/app/src/main/res/layout/layout_hs_backups_list_item.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/layout_hs_cookie.xml b/app/src/main/res/layout/layout_hs_cookie.xml
new file mode 100644
index 00000000..6d1a8935
--- /dev/null
+++ b/app/src/main/res/layout/layout_hs_cookie.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/layout_hs_data_dialog.xml b/app/src/main/res/layout/layout_hs_data_dialog.xml
new file mode 100644
index 00000000..524d69a8
--- /dev/null
+++ b/app/src/main/res/layout/layout_hs_data_dialog.xml
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/layout_hs_list_item.xml b/app/src/main/res/layout/layout_hs_list_item.xml
new file mode 100644
index 00000000..e83b8747
--- /dev/null
+++ b/app/src/main/res/layout/layout_hs_list_item.xml
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/layout_hs_list_view.xml b/app/src/main/res/layout/layout_hs_list_view.xml
new file mode 100644
index 00000000..4981877d
--- /dev/null
+++ b/app/src/main/res/layout/layout_hs_list_view.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/layout_hs_list_view_main.xml b/app/src/main/res/layout/layout_hs_list_view_main.xml
new file mode 100644
index 00000000..284a01c7
--- /dev/null
+++ b/app/src/main/res/layout/layout_hs_list_view_main.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/cookie_menu.xml b/app/src/main/res/menu/cookie_menu.xml
new file mode 100644
index 00000000..78110252
--- /dev/null
+++ b/app/src/main/res/menu/cookie_menu.xml
@@ -0,0 +1,11 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/hs_menu.xml b/app/src/main/res/menu/hs_menu.xml
new file mode 100644
index 00000000..e82a935d
--- /dev/null
+++ b/app/src/main/res/menu/hs_menu.xml
@@ -0,0 +1,14 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/orbot_main.xml b/app/src/main/res/menu/orbot_main.xml
index 54d24937..e556b76b 100644
--- a/app/src/main/res/menu/orbot_main.xml
+++ b/app/src/main/res/menu/orbot_main.xml
@@ -43,7 +43,23 @@
/>
-
+
+ -
+
+
+ 64dp
+
diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml
index 795a4198..d0f22015 100644
--- a/app/src/main/res/values/arrays.xml
+++ b/app/src/main/res/values/arrays.xml
@@ -9,5 +9,8 @@
- Get New Bridges
-
+
+ - User services
+ - App services
+
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index f96195c8..c719a5be 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -1,5 +1,4 @@
-
-
- - 65%
+ - 65%
- - 320dp
+ - 320dp
- - 320dp
+ - 320dp
- - 80%
+ - 80%
- - 100%
+ - 100%
0dp
16dip
- 0x02000000
+ 0x02000000
+
0x02000000
@@ -148,7 +148,7 @@
- - 95%
+ - 95%
64dp
@@ -358,4 +358,8 @@
-1px
+
+ 16dp
+ 16dp
+ 16dp
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 26a7aeb7..41b9e816 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -128,21 +128,21 @@
Enter Entrance Nodes
Allow Background Starts
Let any app tell Orbot to start Tor and related services
-
+
Proxy All
Proxy None
Invert Selection
Outbound Network Proxy (Optional)
-
+
Outbound Proxy Type
Protocol to use for proxy server: HTTP, HTTPS, Socks4, Socks5
Enter Proxy Type
-
+
Outbound Proxy Host
Proxy Server hostname
Enter Proxy Host
-
+
Outbound Proxy Port
Proxy Server port
Enter Proxy port
@@ -150,13 +150,12 @@
Outbound Proxy Username
Proxy Username (Optional)
Enter Proxy Username
-
+
Outbound Proxy Password
Proxy Password (Optional)
Enter Proxy Password
-
-
-
+
+
Status
Setting up full transparent proxying…
Setting up app-based transparent proxying…
@@ -227,17 +226,17 @@
Reboot your device, unable to reset Tor!
Use Default Iptables
use the built-in iptables binary instead of the one bundled with Orbot
-
+
The Tor binaries were not able to be installed or upgraded.
-
+
Always keep the icon in toolbar when Orbot is connected
Always-On Notifications
-
+
Show expanded notification with Tor exit country and IP
Expanded Notifications
-
+
Bridges enabled!
-
+
Language
Choose the locale and language for Orbot
Choose Language
@@ -251,8 +250,8 @@
No Network Auto-Sleep
Put Tor to sleep when there is no internet available
You\'ve switched to a new Tor identity!
-
- Browser
+
+ Browser
Use ChatSecure
Manage Tor
@@ -262,78 +261,123 @@
No network connectivity. Putting Tor to sleep…
Network connectivity is good. Waking Tor up…
updating settings in Tor service
-
- Tor SOCKS
+
+ Tor SOCKS
Port that Tor offers its SOCKS proxy on (default: 9050 or 0 to disable)
SOCKS Port Config
-
- Tor TransProxy Port
+
+ Tor TransProxy Port
Port that Tor offers its Transparent Proxy on (default: 9040 or 0 to disable)
TransProxy Port Config
-
-
- Tor DNS Port
+
+
+ Tor DNS Port
Port that Tor offers its DNS on (default: 5400 or 0 to disable)
DNS Port Config
-
-
- Torrc Custom Config
+
+
+ Torrc Custom Config
EXPERTS ONLY: enter direct torrc config lines
Custom Torrc
-
- Mobile Martus - Benetech Human Rights Documentation App
+
+ Mobile Martus - Benetech Human Rights Documentation App
Your Tor Public IPs:
"Please disable this app in Android->Settings->Apps if you are having problems with Orbot: "
App Conflict
-
+
Transproxy Auto-Refresh
Re-apply Transproxy rules when the network state changes
-
+
Transproxy FORCE REMOVE
Tap here to flush all transproxy network rules NOW
Transparent proxy rules flushed!
You do not have ROOT access enabled
You may need to stop and start Orbot for settings change to be enabled.
-
+
Apps
-
+
kbps
-
+
mbps
-
+
KB
-
+
MB
-
+
Bridges Updated
-
+
Please restart Orbot to enable the changes
-
+
QR Codes
-
+
If your mobile network actively blocks Tor, you can use a Bridge to access the network. SELECT one of the bridge types above to enable bridges.
-
+
Bridge Mode
-
+
Email
Web
-
+
Activate
-
+
Apps VPN Mode
-
+
You can enable all apps on your device to run through the Tor network using the VPN feature of Android.\n\n*WARNING* This is a new, experimental feature and in some cases may not start automatically, or may stop. It should NOT be used for anonymity, and ONLY used for getting through firewalls and filters.
-
+
Send Email
-
+
You can get a bridge address through email, the web or by scanning a bridge QR code. Select \'Email\' or \'Web\' below to request a bridge address.\n\nOnce you have an address, copy & paste it into the \"Bridges\" preference in Orbot\'s setting and restart.
-
+
Install Orfox
-
+
Standard Browser
-
+
NOTE: Only standard Tor bridges work on Intel X86/ATOM devices
- World (Location)
+ World (Location)
+ Hidden Services
+ Hidden Services
+ Hidden Services
+ Save
+ Ports
+ Local Port
+ Onion Port
+ Name
+ Done!
+ Invalid Port
+ Copy address to clipboard
+ Show auth cookie
+ Backup Service
+ Delete Service
+ Backup saved at external storage
+ Backup restored
+ Filemanager not available
+ Please grant permissions for external storage
+ Permission granted
+ Permission denied
+ Restore Backup
+ Create a backup first
+ Name can\'t be empty
+ Fields can\'t be empty
+ Start Tor again for finish the process
+ Confirm service deletion
+ Click again for backup
+ Service type
+ Auth cookie
+ Copy cookie to clipboard
+ Auth cookie was not configured
+ Please restart Orbot to enable the changes
+ Client cookies
+ .onion
+ Invalid .onion address
+ Read from QR
+ Backup cookie
+ Delete cookie
+ Confirm cookie deletion
+ Hosted Services
+ Share as QR
+ Disable
+ Enable
+ Consider disable battery optimizations
+ Consider enable battery optimizations
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index b3fe8af4..6089ad7e 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -1,5 +1,11 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/xml/hidden_services_paths.xml b/app/src/main/res/xml/hidden_services_paths.xml
new file mode 100644
index 00000000..073732b6
--- /dev/null
+++ b/app/src/main/res/xml/hidden_services_paths.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml
index d2b5854c..c20dc4fa 100644
--- a/app/src/main/res/xml/preferences.xml
+++ b/app/src/main/res/xml/preferences.xml
@@ -157,17 +157,6 @@ android:dialogTitle="@string/enter_ports"
/>
-
-
-
-
-
-
-
0)
- result.append(",");
-
- result.append(onionHostname);
-
-
- } catch (FileNotFoundException e) {
- logException("unable to read onion hostname file",e);
- showToolbarNotification(getString(R.string.unable_to_read_hidden_service_name), HS_NOTIFY_ID, R.drawable.ic_stat_notifyerr);
- return null;
- }
- }
- else
- {
- showToolbarNotification(getString(R.string.unable_to_read_hidden_service_name), HS_NOTIFY_ID, R.drawable.ic_stat_notifyerr);
- return null;
-
- }
- }
-
- if (result.length() > 0)
- {
- String onionHostname = result.toString();
-
- showToolbarNotification(getString(R.string.hidden_service_on) + ' ' + onionHostname, HS_NOTIFY_ID, R.drawable.ic_stat_tor);
- Editor pEdit = prefs.edit();
- pEdit.putString("pref_hs_hostname",onionHostname);
- pEdit.commit();
-
- return onionHostname;
- }
-
- }
-
- return null;
- }
-
-
private void killAllDaemons() throws Exception {
if (conn != null) {
logNotice("Using control port to shutdown Tor");
@@ -577,6 +554,14 @@ public class TorService extends Service implements TorServiceConstants, OrbotCon
fileXtables = new File(appBinHome, TorServiceConstants.IPTABLES_ASSET_KEY);
fileTorRc = new File(appBinHome, TorServiceConstants.TORRC_ASSET_KEY);
+ mHSBasePath = new File(
+ getFilesDir().getAbsolutePath(),
+ TorServiceConstants.HIDDEN_SERVICES_DIR
+ );
+
+ if (!mHSBasePath.isDirectory())
+ mHSBasePath.mkdirs();
+
mEventHandler = new TorEventHandler(this);
if (mNotificationManager == null)
@@ -809,7 +794,56 @@ public class TorService extends Service implements TorServiceConstants, OrbotCon
enableTransparentProxy();
}
- getHiddenServiceHostname ();
+ // Tor is running, update new .onion names at db
+ ContentResolver mCR = getApplicationContext().getContentResolver();
+ Cursor hidden_services = mCR.query(HS_CONTENT_URI, hsProjection, null, null, null);
+ if(hidden_services != null) {
+ try {
+ while (hidden_services.moveToNext()) {
+ String HSDomain = hidden_services.getString(hidden_services.getColumnIndex(HiddenService.DOMAIN));
+ Integer HSLocalPort = hidden_services.getInt(hidden_services.getColumnIndex(HiddenService.PORT));
+ Integer HSAuthCookie = hidden_services.getInt(hidden_services.getColumnIndex(HiddenService.AUTH_COOKIE));
+ String HSAuthCookieValue = hidden_services.getString(hidden_services.getColumnIndex(HiddenService.AUTH_COOKIE_VALUE));
+
+ // Update only new domains or restored from backup with auth cookie
+ if((HSDomain == null || HSDomain.length() < 1) || (HSAuthCookie == 1 && (HSAuthCookieValue == null || HSAuthCookieValue.length() < 1))) {
+ String hsDirPath = new File(mHSBasePath.getAbsolutePath(),"hs" + HSLocalPort).getCanonicalPath();
+ File file = new File(hsDirPath, "hostname");
+
+ if (file.exists())
+ {
+ ContentValues fields = new ContentValues();
+
+ try {
+ String onionHostname = Utils.readString(new FileInputStream(file)).trim();
+ if(HSAuthCookie == 1) {
+ String[] aux = onionHostname.split(" ");
+ onionHostname = aux[0];
+ fields.put(HiddenService.AUTH_COOKIE_VALUE, aux[1]);
+ }
+ fields.put(HiddenService.DOMAIN, onionHostname);
+ mCR.update(HS_CONTENT_URI, fields, "port=" + HSLocalPort , null);
+ } catch (FileNotFoundException e) {
+ logException("unable to read onion hostname file",e);
+ showToolbarNotification(getString(R.string.unable_to_read_hidden_service_name), HS_NOTIFY_ID, R.drawable.ic_stat_notifyerr);
+ }
+ }
+ else
+ {
+ showToolbarNotification(getString(R.string.unable_to_read_hidden_service_name), HS_NOTIFY_ID, R.drawable.ic_stat_notifyerr);
+
+ }
+ }
+ }
+
+ } catch (NumberFormatException e) {
+ Log.e(OrbotConstants.TAG,"error parsing hsport",e);
+ } catch (Exception e) {
+ Log.e(OrbotConstants.TAG,"error starting share server",e);
+ }
+
+ hidden_services.close();
+ }
} catch (Exception e) {
logException("Unable to start Tor: " + e.toString(), e);
@@ -1574,7 +1608,6 @@ public class TorService extends Service implements TorServiceConstants, OrbotCon
boolean becomeRelay = prefs.getBoolean(OrbotConstants.PREF_OR, false);
boolean ReachableAddresses = prefs.getBoolean(OrbotConstants.PREF_REACHABLE_ADDRESSES,false);
- boolean enableHiddenServices = prefs.getBoolean("pref_hs_enable", false);
boolean enableStrictNodes = prefs.getBoolean("pref_strict_nodes", false);
String entranceNodes = prefs.getString("pref_entrance_nodes", "");
@@ -1777,49 +1810,52 @@ public class TorService extends Service implements TorServiceConstants, OrbotCon
return false;
}
- if (enableHiddenServices)
- {
- logNotice("hidden services are enabled");
-
- //updateConfiguration("RendPostPeriod", "600 seconds", false); //possible feature to investigate
-
- String hsPorts = prefs.getString("pref_hs_ports","");
-
- StringTokenizer st = new StringTokenizer (hsPorts,",");
- String hsPortConfig = null;
- int hsPort = -1;
-
- while (st.hasMoreTokens())
- {
- try
- {
- hsPortConfig = st.nextToken().trim();
-
- if (hsPortConfig.indexOf(":")==-1) //setup the port to localhost if not specifed
- {
- hsPortConfig = hsPortConfig + " 127.0.0.1:" + hsPortConfig;
- }
-
- hsPort = Integer.parseInt(hsPortConfig.split(" ")[0]);
+ ContentResolver mCR = getApplicationContext().getContentResolver();
+
+ /* ---- Hidden Services ---- */
+ Cursor hidden_services = mCR.query(HS_CONTENT_URI, hsProjection, HiddenService.ENABLED + "=1", null, null);
+ if(hidden_services != null) {
+ try {
+ while (hidden_services.moveToNext()) {
+ String HSname = hidden_services.getString(hidden_services.getColumnIndex(HiddenService.NAME));
+ Integer HSLocalPort = hidden_services.getInt(hidden_services.getColumnIndex(HiddenService.PORT));
+ Integer HSOnionPort = hidden_services.getInt(hidden_services.getColumnIndex(HiddenService.ONION_PORT));
+ Integer HSAuthCookie = hidden_services.getInt(hidden_services.getColumnIndex(HiddenService.AUTH_COOKIE));
+ String hsDirPath = new File(mHSBasePath.getAbsolutePath(),"hs" + HSLocalPort).getCanonicalPath();
+
+ debug("Adding hidden service on port: " + HSLocalPort);
- String hsDirPath = new File(appCacheHome,"hs" + hsPort).getCanonicalPath();
-
- debug("Adding hidden service on port: " + hsPortConfig);
-
extraLines.append("HiddenServiceDir" + ' ' + hsDirPath).append('\n');
- extraLines.append("HiddenServicePort" + ' ' + hsPortConfig).append('\n');
-
+ extraLines.append("HiddenServicePort" + ' ' + HSOnionPort + " 127.0.0.1:" + HSLocalPort).append('\n');
- } catch (NumberFormatException e) {
- Log.e(OrbotConstants.TAG,"error parsing hsport",e);
- } catch (Exception e) {
- Log.e(OrbotConstants.TAG,"error starting share server",e);
+ if(HSAuthCookie == 1)
+ extraLines.append("HiddenServiceAuthorizeClient stealth " + HSname).append('\n');
}
+ } catch (NumberFormatException e) {
+ Log.e(OrbotConstants.TAG,"error parsing hsport",e);
+ } catch (Exception e) {
+ Log.e(OrbotConstants.TAG,"error starting share server",e);
}
-
-
- }
-
+
+ hidden_services.close();
+ }
+
+ /* ---- Client Cookies ---- */
+ Cursor client_cookies = mCR.query(COOKIE_CONTENT_URI, cookieProjection, ClientCookie.ENABLED + "=1", null, null);
+ if(client_cookies != null) {
+ try {
+ while (client_cookies.moveToNext()) {
+ String domain = client_cookies.getString(client_cookies.getColumnIndex(ClientCookie.DOMAIN));
+ String cookie = client_cookies.getString(client_cookies.getColumnIndex(ClientCookie.AUTH_COOKIE_VALUE));
+ extraLines.append("HidServAuth" + ' ' + domain + ' ' + cookie).append('\n');
+ }
+ } catch (Exception e) {
+ Log.e(OrbotConstants.TAG,"error starting share server",e);
+ }
+
+ client_cookies.close();
+ }
+
return true;
}
diff --git a/orbotservice/src/main/java/org/torproject/android/service/TorServiceConstants.java b/orbotservice/src/main/java/org/torproject/android/service/TorServiceConstants.java
index d38d9108..81bb0f43 100644
--- a/orbotservice/src/main/java/org/torproject/android/service/TorServiceConstants.java
+++ b/orbotservice/src/main/java/org/torproject/android/service/TorServiceConstants.java
@@ -161,4 +161,6 @@ public interface TorServiceConstants {
"meek_lite 0.0.2.0:3 url=https://az668014.vo.msecnd.net/ front=ajax.aspnetcdn.com"
};
+ public static final String HIDDEN_SERVICES_DIR = "hidden_services";
+
}