[tor-commits] [orbot/master] Initial work for V3 UI, DB Stuff, etc

n8fr8 at torproject.org n8fr8 at torproject.org
Wed Dec 22 21:55:06 UTC 2021


commit 7ce301042cd9a56155b764f1e18f7396bc21b8fc
Author: bim <dsnake at protonmail.com>
Date:   Sat Nov 14 20:59:02 2020 -0500

    Initial work for V3 UI, DB Stuff, etc
---
 app/src/main/AndroidManifest.xml                   |  14 +
 .../org/torproject/android/OrbotMainActivity.java  |  33 +-
 .../ui/hiddenservices/HiddenServicesActivity.java  |   6 +-
 .../hiddenservices/adapters/OnionListAdapter.java  |   7 +-
 .../ui/hiddenservices/dialogs/HSDeleteDialog.java  |   6 +-
 .../permissions/PermissionManager.java             |  14 +-
 .../providers/HSContentProvider.java               |   6 +-
 .../DeleteOnionServiceDialogFragment.java          |  41 +++
 .../NewOnionServiceDialogFragment.java             |  98 +++++
 .../OnionServiceActionsDialogFragment.java         |  93 +++++
 .../OnionServiceContentProvider.java               | 117 ++++++
 .../ui/v3onionservice/OnionServiceDatabase.java    |  38 ++
 .../ui/v3onionservice/OnionServicesActivity.java   |  98 +++++
 .../ui/v3onionservice/OnionV3ListAdapter.java      |  54 +++
 app/src/main/res/menu/orbot_main.xml               | 125 ++++---
 app/src/main/res/values/strings.xml                |   2 +
 .../java/org/torproject/android/core/DiskUtils.kt  |   8 +-
 .../torproject/android/service/OrbotService.java   | 408 +++++++--------------
 .../android/service/TorServiceConstants.java       |   2 +-
 19 files changed, 797 insertions(+), 373 deletions(-)

diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index a9db7611..7b81951f 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -92,6 +92,15 @@
                 android:value=".OrbotMainActivity" />
         </activity>
 
+        <activity
+            android:name=".ui.v3onionservice.OnionServicesActivity"
+            android:label="@string/hidden_services"
+            android:theme="@style/DefaultTheme">
+            <meta-data
+                android:name="android.support.PARENT_ACTIVITY"
+                android:value=".OrbotMainActivity" />
+        </activity>
+
         <activity
             android:name=".ui.hiddenservices.ClientCookiesActivity"
             android:label="@string/client_cookies"
@@ -123,6 +132,11 @@
             android:authorities="org.torproject.android.ui.hiddenservices.providers"
             android:exported="false" />
 
+        <provider
+            android:name=".ui.v3onionservice.OnionServiceContentProvider"
+            android:authorities="org.torproject.android.ui.v3onionservice"
+            android:exported="false" />
+
         <provider
             android:name="androidx.core.content.FileProvider"
             android:authorities="org.torproject.android.ui.hiddenservices.storage"
diff --git a/app/src/main/java/org/torproject/android/OrbotMainActivity.java b/app/src/main/java/org/torproject/android/OrbotMainActivity.java
index 1ee58106..abc87cc0 100644
--- a/app/src/main/java/org/torproject/android/OrbotMainActivity.java
+++ b/app/src/main/java/org/torproject/android/OrbotMainActivity.java
@@ -74,6 +74,7 @@ import org.torproject.android.ui.hiddenservices.permissions.PermissionManager;
 import org.torproject.android.ui.hiddenservices.providers.HSContentProvider;
 import org.torproject.android.ui.onboarding.BridgeWizardActivity;
 import org.torproject.android.ui.onboarding.OnboardingActivity;
+import org.torproject.android.ui.v3onionservice.OnionServicesActivity;
 
 import java.io.File;
 import java.io.UnsupportedEncodingException;
@@ -114,6 +115,9 @@ public class OrbotMainActivity extends AppCompatActivity implements OrbotConstan
     private static final int MESSAGE_PORTS = 3;
     private static final float ROTATE_FROM = 0.0f;
     private static final float ROTATE_TO = 360.0f * 4f;// 3.141592654f * 32.0f;
+    // this is what takes messages or values from the callback threads or other non-mainUI threads
+    // and passes them back into the main UI thread for display to the user
+    private final Handler mStatusUpdateHandler = new MainActivityStatusUpdateHandler(this);
     PulsatorLayout mPulsator;
     AlertDialog aDialog = null;
     /* Useful UI bits */
@@ -131,11 +135,6 @@ public class OrbotMainActivity extends AppCompatActivity implements OrbotConstan
     /* Some tracking bits */
     private String torStatus = null; //latest status reported from the tor service
     private Intent lastStatusIntent;  // the last ACTION_STATUS Intent received
-    private SharedPreferences mPrefs = null;
-    private boolean autoStartFromIntent = false;
-    // this is what takes messages or values from the callback threads or other non-mainUI threads
-    // and passes them back into the main UI thread for display to the user
-    private final Handler mStatusUpdateHandler = new MainActivityStatusUpdateHandler(this);
     /**
      * The state and log info from {@link OrbotService} are sent to the UI here in
      * the form of a local broadcast. Regular broadcasts can be sent by any app,
@@ -202,6 +201,8 @@ public class OrbotMainActivity extends AppCompatActivity implements OrbotConstan
             }
         }
     };
+    private SharedPreferences mPrefs = null;
+    private boolean autoStartFromIntent = false;
 
     private void migratePreferences() {
         String hsPortString = mPrefs.getString("pref_hs_ports", "");
@@ -463,6 +464,8 @@ public class OrbotMainActivity extends AppCompatActivity implements OrbotConstan
                 }
             }
 
+        } else if (item.getItemId() == R.id.menu_v3_onion_services) {
+            startActivity(new Intent(this, OnionServicesActivity.class));
         } else if (item.getItemId() == R.id.menu_hidden_services) {
             startActivity(new Intent(this, HiddenServicesActivity.class));
         } else if (item.getItemId() == R.id.menu_client_cookies) {
@@ -534,12 +537,8 @@ public class OrbotMainActivity extends AppCompatActivity implements OrbotConstan
         sendIntentToService(ACTION_START_VPN);
     }
 
-    private void enableHiddenServicePort(
-            String hsName, final int hsPort, int hsRemotePort,
-            final String backupToPackage, final Uri hsKeyPath,
-            final Boolean authCookie
-    ) {
-
+    private void enableHiddenServicePort(String hsName, final int hsPort, int hsRemotePort,
+                                         final String backupToPackage, final Uri hsKeyPath, final Boolean authCookie) {
         String onionHostname = null;
 
         if (hsName == null)
@@ -687,9 +686,9 @@ public class OrbotMainActivity extends AppCompatActivity implements OrbotConstan
                 };
 
                 String requestMsg = getString(R.string.hidden_service_request, String.valueOf(hiddenServicePort));
-                AlertDialog.Builder builder = new AlertDialog.Builder(this);
-                builder.setMessage(requestMsg).setPositiveButton("Allow", dialogClickListener)
-                        .setNegativeButton("Deny", dialogClickListener).show();
+                new AlertDialog.Builder(this).setMessage(requestMsg)
+                        .setPositiveButton(R.string.allow, dialogClickListener)
+                        .setNegativeButton(R.string.deny, dialogClickListener).show();
 
                 return; //don't null the setIntent() as we need it later
 
@@ -1103,12 +1102,14 @@ public class OrbotMainActivity extends AppCompatActivity implements OrbotConstan
                 imgStatus.startAnimation(rotation);
                 lblStatus.setText(getString(R.string.newnym));
                 break;
-            case STATUS_STARTING: return; // tor is starting up, a new identity isn't needed
+            case STATUS_STARTING:
+                return; // tor is starting up, a new identity isn't needed
             case STATUS_OFF:
             case STATUS_STOPPING:
                 startTor();
                 break;
-            default: break;
+            default:
+                break;
         }
     }
 
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
index 6c780dde..d7429181 100644
--- a/app/src/main/java/org/torproject/android/ui/hiddenservices/HiddenServicesActivity.java
+++ b/app/src/main/java/org/torproject/android/ui/hiddenservices/HiddenServicesActivity.java
@@ -61,13 +61,9 @@ public class HiddenServicesActivity extends AppCompatActivity {
         mResolver = getContentResolver();
 
         fab = findViewById(R.id.fab);
-        fab.setOnClickListener(view -> {
-            HSDataDialog dialog = new HSDataDialog();
-            dialog.show(getSupportFragmentManager(), "HSDataDialog");
-        });
+        fab.setOnClickListener(view -> new HSDataDialog().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 = findViewById(R.id.onion_list);
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
index d4e39ece..e3983cfd 100644
--- 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
@@ -25,8 +25,7 @@ public class OnionListAdapter extends CursorAdapter {
     }
 
     @Override
-    public void bindView(View view, Context context, Cursor cursor) {
-        final Context mContext = context;
+    public void bindView(View view, final Context context, Cursor cursor) {
         int id = cursor.getInt(cursor.getColumnIndex(HSContentProvider.HiddenService._ID));
         final String where = HSContentProvider.HiddenService._ID + "=" + id;
 
@@ -40,12 +39,12 @@ public class OnionListAdapter extends CursorAdapter {
         SwitchCompat enabled = view.findViewById(R.id.hs_switch);
         enabled.setChecked(cursor.getInt(cursor.getColumnIndex(HSContentProvider.HiddenService.ENABLED)) == 1);
         enabled.setOnCheckedChangeListener((buttonView, isChecked) -> {
-            ContentResolver resolver = mContext.getContentResolver();
+            ContentResolver resolver = context.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();
+            Toast.makeText(context, R.string.please_restart_Orbot_to_enable_the_changes, Toast.LENGTH_LONG).show();
         });
     }
 
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
index d26141a0..f4a96226 100644
--- 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
@@ -20,11 +20,9 @@ public class HSDeleteDialog extends DialogFragment {
     @NonNull
     @Override
     public Dialog onCreateDialog(Bundle savedInstanceState) {
-        final Bundle arguments = getArguments();
-        final Context context = getContext();
-        return new AlertDialog.Builder(context)
+        return new AlertDialog.Builder(getContext())
                 .setTitle(R.string.confirm_service_deletion)
-                .setPositiveButton(android.R.string.ok, (dialog, which) -> doDelete(arguments, context))
+                .setPositiveButton(android.R.string.ok, (dialog, which) -> doDelete(getArguments(), getContext()))
                 .setNegativeButton(android.R.string.cancel, (dialog, which) -> dialog.cancel())
                 .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
index 6d181967..52c5dc20 100644
--- 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
@@ -23,9 +23,8 @@ public class PermissionManager {
 
     @TargetApi(Build.VERSION_CODES.M)
     public static void requestBatteryPermissions(FragmentActivity activity, Context context) {
-        final Context mContext = context;
-        final String packageName = mContext.getPackageName();
-        PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
+        final String packageName = context.getPackageName();
+        PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
 
         if (pm.isIgnoringBatteryOptimizations(packageName))
             return;
@@ -37,18 +36,15 @@ public class PermissionManager {
                     Intent intent = new Intent();
                     intent.setAction(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
                     intent.setData(Uri.parse("package:" + packageName));
-                    mContext.startActivity(intent);
+                    context.startActivity(intent);
                 }).show();
     }
 
     @TargetApi(Build.VERSION_CODES.M)
     public static void requestDropBatteryPermissions(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))
+        if (!pm.isIgnoringBatteryOptimizations(context.getPackageName()))
             return;
 
         Snackbar.make(activity.findViewById(android.R.id.content),
@@ -57,7 +53,7 @@ public class PermissionManager {
                 v -> {
                     Intent intent = new Intent();
                     intent.setAction(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS);
-                    mContext.startActivity(intent);
+                    context.startActivity(intent);
                 }).show();
     }
 }
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
index 87cad0ad..79b566fd 100644
--- 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
@@ -27,8 +27,7 @@ public class HSContentProvider extends ContentProvider {
             HiddenService.ENABLED
     };
     private static final String AUTH = "org.torproject.android.ui.hiddenservices.providers";
-    public static final Uri CONTENT_URI =
-            Uri.parse("content://" + AUTH + "/hs");
+    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;
@@ -61,8 +60,7 @@ public class HSContentProvider extends ContentProvider {
 
         SQLiteDatabase db = mServervices.getReadableDatabase();
 
-        return db.query(HSDatabase.HS_DATA_TABLE_NAME, projection, where,
-                selectionArgs, null, null, sortOrder);
+        return db.query(HSDatabase.HS_DATA_TABLE_NAME, projection, where, selectionArgs, null, null, sortOrder);
     }
 
     @Nullable
diff --git a/app/src/main/java/org/torproject/android/ui/v3onionservice/DeleteOnionServiceDialogFragment.java b/app/src/main/java/org/torproject/android/ui/v3onionservice/DeleteOnionServiceDialogFragment.java
new file mode 100644
index 00000000..5028e8f3
--- /dev/null
+++ b/app/src/main/java/org/torproject/android/ui/v3onionservice/DeleteOnionServiceDialogFragment.java
@@ -0,0 +1,41 @@
+package org.torproject.android.ui.v3onionservice;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AlertDialog;
+import androidx.fragment.app.DialogFragment;
+
+import org.torproject.android.R;
+import org.torproject.android.core.DiskUtils;
+import org.torproject.android.service.TorServiceConstants;
+import org.torproject.android.ui.hiddenservices.HiddenServicesActivity;
+
+import java.io.File;
+
+public class DeleteOnionServiceDialogFragment extends DialogFragment {
+    DeleteOnionServiceDialogFragment(Bundle arguments) {
+        super();
+        setArguments(arguments);
+    }
+
+    @NonNull
+    @Override
+    public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
+        return new AlertDialog.Builder(getContext())
+                .setTitle(R.string.confirm_service_deletion)
+                .setPositiveButton(android.R.string.ok, (dialog, which) -> doDelete(getArguments(), getContext()))
+                .setNegativeButton(android.R.string.cancel, (dialog, which) -> dialog.cancel())
+                .create();
+    }
+
+    private void doDelete(Bundle arguments, Context context) {
+        context.getContentResolver().delete(OnionServiceContentProvider.CONTENT_URI, OnionServiceContentProvider.OnionService._ID + '=' + arguments.getInt(OnionServicesActivity.BUNDLE_KEY_ID), null);
+        String base = context.getFilesDir().getAbsolutePath() + "/" + TorServiceConstants.ONION_SERVICES_DIR;
+        DiskUtils.recursivelyDeleteDirectory(new File(base, "v3" + arguments.getString(OnionServicesActivity.BUNDLE_KEY_PORT)));
+    }
+
+}
diff --git a/app/src/main/java/org/torproject/android/ui/v3onionservice/NewOnionServiceDialogFragment.java b/app/src/main/java/org/torproject/android/ui/v3onionservice/NewOnionServiceDialogFragment.java
new file mode 100644
index 00000000..1929eb72
--- /dev/null
+++ b/app/src/main/java/org/torproject/android/ui/v3onionservice/NewOnionServiceDialogFragment.java
@@ -0,0 +1,98 @@
+package org.torproject.android.ui.v3onionservice;
+
+import android.app.Dialog;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.os.Bundle;
+import android.text.Editable;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.view.View;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AlertDialog;
+import androidx.fragment.app.DialogFragment;
+
+import org.torproject.android.R;
+
+public class NewOnionServiceDialogFragment extends DialogFragment {
+
+    private EditText etServer, etLocalPort, etOnionPort;
+    private TextWatcher inputValidator;
+
+    @Override
+    public void onStart() {
+        super.onStart();
+        inputValidator.afterTextChanged(null); // initially disable positive button
+    }
+
+
+    @NonNull
+    @Override
+    public Dialog onCreateDialog(Bundle savedInstanceState) {
+        final View dialogView = getActivity().getLayoutInflater().inflate(R.layout.layout_hs_data_dialog, null);
+        dialogView.findViewById(R.id.hsAuth).setVisibility(View.GONE);
+        etServer = dialogView.findViewById(R.id.hsName);
+        etLocalPort = dialogView.findViewById(R.id.hsLocalPort);
+        etOnionPort = dialogView.findViewById(R.id.hsOnionPort);
+
+        AlertDialog alertDialog = new AlertDialog.Builder(getActivity())
+                .setTitle(R.string.hidden_services)
+                .setNegativeButton(android.R.string.cancel, (dialog, which) -> dialog.cancel())
+                .setPositiveButton(R.string.save, (dialog, which) -> doSave(getContext()))
+                .setView(dialogView)
+                .create();
+
+
+        inputValidator = new TextWatcher() {
+            @Override
+            public void beforeTextChanged(CharSequence s, int start, int count, int after) { // no-op
+            }
+
+            @Override
+            public void onTextChanged(CharSequence s, int start, int before, int count) { //no-op
+            }
+
+            @Override
+            public void afterTextChanged(Editable s) {
+                Button btn = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE);
+                try {
+                    int localPort = Integer.parseInt(etLocalPort.getText().toString());
+                    int onionPort = Integer.parseInt(etOnionPort.getText().toString());
+                    btn.setEnabled(checkInput(localPort, onionPort));
+                } catch (NumberFormatException nfe) {
+                    btn.setEnabled(false);
+                }
+            }
+        };
+
+        etServer.addTextChangedListener(inputValidator);
+        etLocalPort.addTextChangedListener(inputValidator);
+        etOnionPort.addTextChangedListener(inputValidator);
+        return alertDialog;
+    }
+
+    private boolean checkInput(int local, int remote) {
+        if ((local < 1 || local > 65535) || (remote < 1 || remote > 65535)) return false;
+        return !TextUtils.isEmpty(etServer.getText().toString().trim());
+    }
+
+    private void doSave(Context context) {
+        String serverName = etServer.getText().toString().trim();
+        int localPort = Integer.parseInt(etLocalPort.getText().toString());
+        int onionPort = Integer.parseInt(etOnionPort.getText().toString());
+        ContentValues fields = new ContentValues();
+        fields.put(OnionServiceContentProvider.OnionService.NAME, serverName);
+        fields.put(OnionServiceContentProvider.OnionService.PORT, localPort);
+        fields.put(OnionServiceContentProvider.OnionService.ONION_PORT, onionPort);
+        fields.put(OnionServiceContentProvider.OnionService.CREATED_BY_USER, 1);
+        ContentResolver cr = getContext().getContentResolver();
+        cr.insert(OnionServiceContentProvider.CONTENT_URI, fields);
+        Toast.makeText(context, R.string.please_restart_Orbot_to_enable_the_changes, Toast.LENGTH_LONG).show();
+    }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/torproject/android/ui/v3onionservice/OnionServiceActionsDialogFragment.java b/app/src/main/java/org/torproject/android/ui/v3onionservice/OnionServiceActionsDialogFragment.java
new file mode 100644
index 00000000..822a45f2
--- /dev/null
+++ b/app/src/main/java/org/torproject/android/ui/v3onionservice/OnionServiceActionsDialogFragment.java
@@ -0,0 +1,93 @@
+package org.torproject.android.ui.v3onionservice;
+
+import android.app.Activity;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AlertDialog;
+import androidx.fragment.app.DialogFragment;
+
+import org.torproject.android.R;
+import org.torproject.android.core.ClipboardUtils;
+import org.torproject.android.core.DiskUtils;
+import org.torproject.android.ui.hiddenservices.backup.BackupUtils;
+
+import java.io.File;
+
+public class OnionServiceActionsDialogFragment extends DialogFragment {
+
+    private static final int REQUEST_CODE_WRITE_FILE = 343;
+
+    OnionServiceActionsDialogFragment(Bundle arguments) {
+        super();
+        setArguments(arguments);
+    }
+
+    @NonNull
+    @Override
+    public Dialog onCreateDialog(Bundle savedInstanceState) {
+        Bundle arguments = getArguments();
+        AlertDialog ad = new AlertDialog.Builder(getActivity())
+                .setItems(new CharSequence[]{
+                        getString(R.string.copy_address_to_clipboard),
+                        getString(R.string.backup_service),
+                        getString(R.string.delete_service)}, null)
+                .setNegativeButton(android.R.string.cancel, (dialog, which) -> dialog.dismiss())
+                .setTitle(R.string.hidden_services)
+                .create();
+
+        // done this way so we can startActivityForResult on backup without the dialog vanishing
+        ad.getListView().setOnItemClickListener((parent, view, position, id) -> {
+            if (position == 0) doCopy(arguments, getContext());
+            else if (position == 1) doBackup(arguments, getContext());
+            else if (position == 2)
+                new DeleteOnionServiceDialogFragment(arguments).show(getFragmentManager(), DeleteOnionServiceDialogFragment.class.getSimpleName());
+            if (position != 1) dismiss();
+        });
+        return ad;
+    }
+
+    private void doCopy(Bundle arguments, Context context) {
+        String onion = arguments.getString(OnionServicesActivity.BUNDLE_KEY_DOMAIN);
+        if (onion == null)
+            Toast.makeText(context, R.string.please_restart_Orbot_to_enable_the_changes, Toast.LENGTH_LONG).show();
+        else
+            ClipboardUtils.copyToClipboard("onion", onion, getString(R.string.done), context);
+    }
+
+    private void doBackup(Bundle arguments, Context context) {
+        String filename = "onion_service" + arguments.getInt(OnionServicesActivity.BUNDLE_KEY_PORT) + ".zip";
+        if (arguments.getString(OnionServicesActivity.BUNDLE_KEY_DOMAIN) == null) {
+            Toast.makeText(context, R.string.please_restart_Orbot_to_enable_the_changes, Toast.LENGTH_LONG).show();
+            return;
+        }
+        if (DiskUtils.supportsStorageAccessFramework()) {
+            Intent createFileIntent = DiskUtils.createWriteFileIntent(filename, "application/zip");
+            startActivityForResult(createFileIntent, REQUEST_CODE_WRITE_FILE);
+        } else { // APIs 16, 17, 18
+            attemptToWriteBackup(Uri.fromFile(new File(DiskUtils.getOrCreateLegacyBackupDir(), filename)));
+        }
+    }
+
+    @Override
+    public void onActivityResult(int requestCode, int resultCode, Intent data) {
+        if (requestCode == REQUEST_CODE_WRITE_FILE && resultCode == Activity.RESULT_OK) {
+            if (data != null) {
+                attemptToWriteBackup(data.getData());
+            }
+        }
+    }
+
+    private void attemptToWriteBackup(Uri outputFile) {
+        BackupUtils backupUtils = new BackupUtils(getContext());
+//        String backup = backupUtils.createZipBackup(port, outputFile); TODO need to break apart backup logic for v2 and v3 onions
+        Toast.makeText(getContext(), backup != null ? R.string.backup_saved_at_external_storage : R.string.error, Toast.LENGTH_LONG).show();
+        dismiss();
+    }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/torproject/android/ui/v3onionservice/OnionServiceContentProvider.java b/app/src/main/java/org/torproject/android/ui/v3onionservice/OnionServiceContentProvider.java
new file mode 100644
index 00000000..d61f7935
--- /dev/null
+++ b/app/src/main/java/org/torproject/android/ui/v3onionservice/OnionServiceContentProvider.java
@@ -0,0 +1,117 @@
+package org.torproject.android.ui.v3onionservice;
+
+import android.content.ContentProvider;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.UriMatcher;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.net.Uri;
+import android.provider.BaseColumns;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+public class OnionServiceContentProvider extends ContentProvider {
+
+    public static final String[] PROJECTION = {
+            OnionService._ID,
+            OnionService.NAME,
+            OnionService.PORT,
+            OnionService.DOMAIN,
+            OnionService.ONION_PORT,
+            OnionService.CREATED_BY_USER,
+            OnionService.ENABLED
+    };
+
+    private static final int ONIONS = 1, ONION_ID = 2;
+    private static final String AUTH = "org.torproject.android.ui.v3onionservice";
+    public static final Uri CONTENT_URI = Uri.parse("content://" + AUTH + "/v3");
+    private static final UriMatcher uriMatcher;
+
+    static {
+        uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
+        uriMatcher.addURI(AUTH, "v3", ONIONS);
+        uriMatcher.addURI(AUTH, "v3/#", ONION_ID);
+    }
+
+    private OnionServiceDatabase mDatabase;
+
+    @Override
+    public boolean onCreate() {
+        mDatabase = new OnionServiceDatabase(getContext());
+        return true;
+    }
+
+    @Nullable
+    @Override
+    public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
+        String where = selection;
+        if (uriMatcher.match(uri) == ONION_ID) {
+            where = "_id=" + uri.getLastPathSegment();
+        }
+
+        SQLiteDatabase db = mDatabase.getReadableDatabase();
+        return db.query(OnionServiceDatabase.ONION_SERVICE_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, @Nullable ContentValues values) {
+        SQLiteDatabase db = mDatabase.getWritableDatabase();
+        long regId = db.insert(OnionServiceDatabase.ONION_SERVICE_TABLE_NAME, null, values);
+        getContext().getContentResolver().notifyChange(CONTENT_URI, null);
+        return ContentUris.withAppendedId(CONTENT_URI, regId);
+    }
+
+    @Override
+    public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
+        if (uriMatcher.match(uri) == ONION_ID) {
+            selection = "_id=" + uri.getLastPathSegment();
+        }
+        SQLiteDatabase db = mDatabase.getWritableDatabase();
+        int rows = db.delete(OnionServiceDatabase.ONION_SERVICE_TABLE_NAME, selection, selectionArgs);
+        getContext().getContentResolver().notifyChange(CONTENT_URI, null);
+        return rows;
+    }
+
+    @Override
+    public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
+        SQLiteDatabase db = mDatabase.getWritableDatabase();
+        String where = selection;
+        if (uriMatcher.match(uri) == ONION_ID) {
+            where = "_id=" + uri.getLastPathSegment();
+        }
+
+        int rows = db.update(OnionServiceDatabase.ONION_SERVICE_TABLE_NAME, values, where, null);
+        getContext().getContentResolver().notifyChange(CONTENT_URI, null);
+        return rows;
+    }
+
+    public static final class OnionService 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 CREATED_BY_USER = "created_by_user";
+        public static final String ENABLED = "enabled";
+
+        private OnionService() { // no-op
+        }
+    }
+
+}
diff --git a/app/src/main/java/org/torproject/android/ui/v3onionservice/OnionServiceDatabase.java b/app/src/main/java/org/torproject/android/ui/v3onionservice/OnionServiceDatabase.java
new file mode 100644
index 00000000..f2a812e8
--- /dev/null
+++ b/app/src/main/java/org/torproject/android/ui/v3onionservice/OnionServiceDatabase.java
@@ -0,0 +1,38 @@
+package org.torproject.android.ui.v3onionservice;
+
+import android.content.Context;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+
+public class OnionServiceDatabase extends SQLiteOpenHelper {
+
+    static final String DATABASE_NAME = "onion_service",
+            ONION_SERVICE_TABLE_NAME = "onion_services";
+    private static final int DATABASE_VERSION = 1;
+
+    private static final String ONION_SERVICES_CREATE_SQL =
+            "CREATE TABLE " + ONION_SERVICE_TABLE_NAME + " (" +
+                    "_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
+                    "name TEXT, " +
+                    "domain TEXT, " +
+                    "onion_port INTEGER, " +
+                    "created_by_user INTEGER DEFAULT 0, " +
+                    "enabled INTEGER DEFAULT 1, " +
+                    "port INTEGER);";
+
+    public OnionServiceDatabase(Context context) {
+        super(context, DATABASE_NAME, null, DATABASE_VERSION);
+    }
+
+    @Override
+    public void onCreate(SQLiteDatabase db) {
+        db.execSQL(ONION_SERVICES_CREATE_SQL);
+    }
+
+    @Override
+    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+
+    }
+
+
+}
diff --git a/app/src/main/java/org/torproject/android/ui/v3onionservice/OnionServicesActivity.java b/app/src/main/java/org/torproject/android/ui/v3onionservice/OnionServicesActivity.java
new file mode 100644
index 00000000..3be04731
--- /dev/null
+++ b/app/src/main/java/org/torproject/android/ui/v3onionservice/OnionServicesActivity.java
@@ -0,0 +1,98 @@
+package org.torproject.android.ui.v3onionservice;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.os.Handler;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.widget.ListView;
+import android.widget.RadioButton;
+
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.widget.Toolbar;
+
+import com.google.android.material.floatingactionbutton.FloatingActionButton;
+
+import org.torproject.android.R;
+import org.torproject.android.core.DiskUtils;
+import org.torproject.android.core.LocaleHelper;
+
+public class OnionServicesActivity extends AppCompatActivity {
+
+    static final String BUNDLE_KEY_ID = "id", BUNDLE_KEY_PORT = "port", BUNDLE_KEY_DOMAIN = "domain";
+    private static final String WHERE_SELECTION_CLAUSE = OnionServiceContentProvider.OnionService.CREATED_BY_USER + "=1";
+    private RadioButton radioShowUserServices, radioShowAppServices;
+    private FloatingActionButton fab;
+    private ContentResolver mContentResolver;
+    private OnionV3ListAdapter mAdapter;
+
+    @Override
+    public void onCreate(Bundle bundle) {
+        super.onCreate(bundle);
+        setContentView(R.layout.layout_hs_list_view);
+
+        Toolbar toolbar = findViewById(R.id.toolbar);
+        setSupportActionBar(toolbar);
+        getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+
+        fab = findViewById(R.id.fab);
+        fab.setOnClickListener(v -> new NewOnionServiceDialogFragment().show(getSupportFragmentManager(), "NewOnionServiceDialogFragment"));
+
+        mContentResolver = getContentResolver();
+        mAdapter = new OnionV3ListAdapter(this, mContentResolver.query(OnionServiceContentProvider.CONTENT_URI, OnionServiceContentProvider.PROJECTION, WHERE_SELECTION_CLAUSE, null, null), 0);
+        mContentResolver.registerContentObserver(OnionServiceContentProvider.CONTENT_URI, true, new OnionServiceObserver(new Handler()));
+
+        ListView onionList = findViewById(R.id.onion_list);
+        onionList.setAdapter(mAdapter);
+        onionList.setOnItemClickListener((parent, view, position, id) -> {
+            Cursor item = (Cursor) parent.getItemAtPosition(position);
+            Bundle arguments = new Bundle();
+            arguments.putInt(BUNDLE_KEY_ID, item.getInt(item.getColumnIndex(OnionServiceContentProvider.OnionService._ID)));
+            arguments.putString(BUNDLE_KEY_PORT, item.getString(item.getColumnIndex(OnionServiceContentProvider.OnionService.PORT)));
+            arguments.putString(BUNDLE_KEY_DOMAIN, item.getString(item.getColumnIndex(OnionServiceContentProvider.OnionService.DOMAIN)));
+            OnionServiceActionsDialogFragment dialog = new OnionServiceActionsDialogFragment(arguments);
+            dialog.show(getSupportFragmentManager(), OnionServiceActionsDialogFragment.class.getSimpleName());
+        });
+    }
+
+    @Override
+    protected void attachBaseContext(Context base) {
+        super.attachBaseContext(LocaleHelper.onAttach(base));
+    }
+
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        getMenuInflater().inflate(R.menu.hs_menu, menu);
+        return true;
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        if (item.getItemId() == R.id.menu_restore_backup) {
+            if (DiskUtils.supportsStorageAccessFramework()) {
+
+            } else { // 16, 17, 18
+
+            }
+        }
+        return super.onOptionsItemSelected(item);
+    }
+
+    private class OnionServiceObserver extends ContentObserver {
+
+        OnionServiceObserver(Handler handler) {
+            super(handler);
+        }
+
+        @Override
+        public void onChange(boolean selfChange) {
+            mAdapter.changeCursor(mContentResolver.query(OnionServiceContentProvider.CONTENT_URI, OnionServiceContentProvider.PROJECTION, WHERE_SELECTION_CLAUSE, null, null));
+            // todo battery optimization stuff if lollipop or higher and running onion services
+        }
+    }
+
+
+}
diff --git a/app/src/main/java/org/torproject/android/ui/v3onionservice/OnionV3ListAdapter.java b/app/src/main/java/org/torproject/android/ui/v3onionservice/OnionV3ListAdapter.java
new file mode 100644
index 00000000..c0489d3c
--- /dev/null
+++ b/app/src/main/java/org/torproject/android/ui/v3onionservice/OnionV3ListAdapter.java
@@ -0,0 +1,54 @@
+package org.torproject.android.ui.v3onionservice;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.CursorAdapter;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.appcompat.widget.SwitchCompat;
+
+import org.torproject.android.R;
+import org.torproject.android.ui.hiddenservices.providers.HSContentProvider;
+
+public class OnionV3ListAdapter extends CursorAdapter {
+
+    private final LayoutInflater mLayoutInflater;
+
+    OnionV3ListAdapter(Context context, Cursor cursor, int flags) {
+        super(context, cursor, flags);
+        mLayoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+    }
+
+    @Override
+    public View newView(Context context, Cursor cursor, ViewGroup parent) {
+        return mLayoutInflater.inflate(R.layout.layout_hs_list_item, parent, false);
+    }
+
+    @Override
+    public void bindView(View view, Context context, Cursor cursor) {
+        int id = cursor.getInt(cursor.getColumnIndex(OnionServiceContentProvider.OnionService._ID));
+        final String where = OnionServiceContentProvider.OnionService._ID + "=" + id;
+        TextView port = view.findViewById(R.id.hs_port);
+        port.setText(cursor.getString(cursor.getColumnIndex(OnionServiceContentProvider.OnionService.PORT)));
+        TextView name = view.findViewById(R.id.hs_name);
+        name.setText(cursor.getString(cursor.getColumnIndex(OnionServiceContentProvider.OnionService.NAME)));
+        TextView domain = view.findViewById(R.id.hs_onion);
+        domain.setText(cursor.getString(cursor.getColumnIndex(OnionServiceContentProvider.OnionService.DOMAIN)));
+
+        SwitchCompat enabled = view.findViewById(R.id.hs_switch);
+        enabled.setChecked(cursor.getInt(cursor.getColumnIndex(OnionServiceContentProvider.OnionService.ENABLED)) == 1);
+        enabled.setOnCheckedChangeListener((buttonView, isChecked) -> {
+            ContentResolver resolver = context.getContentResolver();
+            ContentValues fields = new ContentValues();
+            fields.put(OnionServiceContentProvider.OnionService.ENABLED, isChecked);
+            resolver.update(OnionServiceContentProvider.CONTENT_URI, fields, where, null);
+            Toast.makeText(context, R.string.please_restart_Orbot_to_enable_the_changes, Toast.LENGTH_LONG).show();
+        });
+    }
+}
diff --git a/app/src/main/res/menu/orbot_main.xml b/app/src/main/res/menu/orbot_main.xml
index 6359ebdc..2fb0ad4e 100644
--- a/app/src/main/res/menu/orbot_main.xml
+++ b/app/src/main/res/menu/orbot_main.xml
@@ -1,5 +1,4 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
+<?xml version="1.0" encoding="utf-8"?><!--
 /*
  * Copyright (C) 2008 Esmertec AG.
  * Copyright (C) 2008 The Android Open Source Project
@@ -18,77 +17,83 @@
  */
 -->
 <menu xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:yourapp="http://schemas.android.com/apk/res-auto"
-    >
+    xmlns:yourapp="http://schemas.android.com/apk/res-auto">
 
 
-    <item android:id="@+id/menu_newnym"
-        android:title="@string/menu_new_identity"
+    <item
+        android:id="@+id/menu_newnym"
         android:icon="@drawable/ic_refresh_white_24dp"
-        yourapp:showAsAction="always"
-        />
+        android:title="@string/menu_new_identity"
+        yourapp:showAsAction="always" />
 
 
-    <item android:id="@+id/menu_settings"
+    <item
+        android:id="@+id/menu_settings"
+        android:icon="@drawable/ic_action_settings"
         android:title="@string/menu_settings"
-         android:icon="@drawable/ic_action_settings"
-         yourapp:showAsAction="never"
-         />
+        yourapp:showAsAction="never" />
 
- <item
+    <item
         android:title="@string/menu_qr"
-     yourapp:showAsAction="never"
-         >
-  <menu>
-  <item android:id="@+id/menu_scan"
-        android:title="@string/menu_scan"
-      yourapp:showAsAction="never"
-         />
-  
-  <item android:id="@+id/menu_share_bridge"
-        android:title="@string/menu_share_bridge"
-      yourapp:showAsAction="never"
-         />
-  </menu>
- </item>
+        yourapp:showAsAction="never">
+        <menu>
+            <item
+                android:id="@+id/menu_scan"
+                android:title="@string/menu_scan"
+                yourapp:showAsAction="never" />
+
+            <item
+                android:id="@+id/menu_share_bridge"
+                android:title="@string/menu_share_bridge"
+                yourapp:showAsAction="never" />
+        </menu>
+    </item>
 
     <item
         android:title="@string/menu_hidden_services"
         yourapp:showAsAction="never">
-      <menu>
-        <item android:id="@+id/menu_hidden_services"
-              android:title="@string/hosted_services"
-            yourapp:showAsAction="never"
-               />
-
-        <item android:id="@+id/menu_client_cookies"
-              android:title="@string/client_cookies"
-            yourapp:showAsAction="never"
-               />
-      </menu>
+        <menu>
+
+            <item
+                android:id="@+id/menu_v3_onion_services"
+                android:title="@string/hidden_services"
+                yourapp:showAsAction="never" />
+
+            <item
+                android:id="@+id/menu_hidden_services"
+                android:title="@string/hosted_services"
+                yourapp:showAsAction="never" />
+
+            <item
+                android:id="@+id/menu_client_cookies"
+                android:title="@string/client_cookies"
+                yourapp:showAsAction="never" />
+        </menu>
     </item>
-        
- <!--
-  <item android:id="@+id/menu_promo_apps"
-        android:title="@string/menu_promo_apps"
-         android:icon="@drawable/ic_menu_goto"
-      yourapp:showAsAction="never"
-         
-         />
-         -->
- 
-  <item android:id="@+id/menu_about"
+
+    <!--
+     <item android:id="@+id/menu_promo_apps"
+           android:title="@string/menu_promo_apps"
+            android:icon="@drawable/ic_menu_goto"
+         yourapp:showAsAction="never"
+
+            />
+            -->
+
+    <item
+        android:id="@+id/menu_about"
+        android:icon="@drawable/ic_menu_about"
         android:title="@string/menu_about"
-         android:icon="@drawable/ic_menu_about"
-      yourapp:showAsAction="never"
-         
-         />
- 
-  <item android:id="@+id/menu_exit"
+        yourapp:showAsAction="never"
+
+        />
+
+    <item
+        android:id="@+id/menu_exit"
+        android:icon="@drawable/ic_menu_exit"
         android:title="@string/menu_exit"
-         android:icon="@drawable/ic_menu_exit"
-      yourapp:showAsAction="never"
-         
-         />
- 
+        yourapp:showAsAction="never"
+
+        />
+
 </menu>
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index c9fc00a6..0501adf8 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -101,6 +101,8 @@
     <string name="obfsproxy_version">Obfs4proxy: https://github.com/Yawning/obfs4</string>
     <string name="openssl_version">OpenSSL: http://www.openssl.org</string>
     <string name="hidden_service_request">An app wants to open onion server port %1$s to the Tor network. This is safe if you trust the app.</string>
+    <string name="allow">Allow</string>
+    <string name="deny">Deny</string>
     <string name="found_existing_tor_process">found existing Tor process…</string>
     <string name="something_bad_happened">Something bad happened. Check the log</string>
     <string name="unable_to_read_hidden_service_name">unable to read onion service name</string>
diff --git a/appcore/src/main/java/org/torproject/android/core/DiskUtils.kt b/appcore/src/main/java/org/torproject/android/core/DiskUtils.kt
index 7e741695..be74aa4e 100644
--- a/appcore/src/main/java/org/torproject/android/core/DiskUtils.kt
+++ b/appcore/src/main/java/org/torproject/android/core/DiskUtils.kt
@@ -68,9 +68,15 @@ object DiskUtils {
     @JvmStatic
     fun getOrCreateLegacyBackupDir(): File? {
         if (Environment.MEDIA_MOUNTED != Environment.getExternalStorageState()) return null
-        val dir = File(Environment.getExternalStorageDirectory(), "Orbot")
+        val dir = File(Environment.getExternalStorageDirectory(), )
         return if (!dir.isDirectory && !dir.mkdirs()) null else dir
     }
 
+    @JvmStatic
+    fun recursivelyDeleteDirectory(directory: File) : Boolean {
+        val contents = directory.listFiles()
+        contents?.forEach { recursivelyDeleteDirectory(it) }
+        return directory.delete()
+    }
 
 }
\ No newline at end of file
diff --git a/orbotservice/src/main/java/org/torproject/android/service/OrbotService.java b/orbotservice/src/main/java/org/torproject/android/service/OrbotService.java
index 36dcb0ae..10acf5de 100644
--- a/orbotservice/src/main/java/org/torproject/android/service/OrbotService.java
+++ b/orbotservice/src/main/java/org/torproject/android/service/OrbotService.java
@@ -39,7 +39,6 @@ import androidx.localbroadcastmanager.content.LocalBroadcastManager;
 
 import com.jaredrummler.android.shell.CommandResult;
 
-import net.freehaven.tor.control.ConfigEntry;
 import net.freehaven.tor.control.TorControlCommands;
 import net.freehaven.tor.control.TorControlConnection;
 
@@ -72,7 +71,6 @@ import java.util.Collections;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.Iterator;
-import java.util.List;
 import java.util.Random;
 import java.util.StringTokenizer;
 import java.util.concurrent.ExecutorService;
@@ -84,14 +82,36 @@ import IPtProxy.IPtProxy;
 public class OrbotService extends VpnService implements TorServiceConstants, OrbotConstants {
 
     public final static String BINARY_TOR_VERSION = org.torproject.android.binary.TorServiceConstants.BINARY_TOR_VERSION;
-    private final static int CONTROL_SOCKET_TIMEOUT = 60000;
     static final int NOTIFY_ID = 1;
+    private final static int CONTROL_SOCKET_TIMEOUT = 60000;
     private static final int ERROR_NOTIFY_ID = 3;
     private static final int HS_NOTIFY_ID = 4;
-    private static final Uri HS_CONTENT_URI = Uri.parse("content://org.torproject.android.ui.hiddenservices.providers/hs");
+    private static final Uri V2_HS_CONTENT_URI = Uri.parse("content://org.torproject.android.ui.hiddenservices.providers/hs");
+    private static final Uri V3_ONION_SERVICES_CONTENT_URI = Uri.parse("content://org.torproject.android.ui.v3onionservice/v3");
     private static final Uri COOKIE_CONTENT_URI = Uri.parse("content://org.torproject.android.ui.hiddenservices.providers.cookie/cookie");
     private final static String NOTIFICATION_CHANNEL_ID = "orbot_channel_1";
-    private final static String RESET_STRING = "=\"\"";
+    private static final String[] LEGACY_V2_ONION_SERVICE_PROJECTION = new String[]{
+            OnionService._ID,
+            OnionService.NAME,
+            OnionService.DOMAIN,
+            OnionService.PORT,
+            OnionService.AUTH_COOKIE,
+            OnionService.AUTH_COOKIE_VALUE,
+            OnionService.ONION_PORT,
+            OnionService.ENABLED};
+    private static final String[] V3_ONION_SERVICE_PROJECTION = new String[]{
+            OnionService._ID,
+            OnionService.NAME,
+            OnionService.DOMAIN,
+            OnionService.PORT,
+            OnionService.ONION_PORT,
+            OnionService.ENABLED,
+    };
+    private static final String[] LEGACY_COOKIE_PROJECTION = new String[]{
+            ClientCookie._ID,
+            ClientCookie.DOMAIN,
+            ClientCookie.AUTH_COOKIE_VALUE,
+            ClientCookie.ENABLED};
     public static int mPortSOCKS = -1;
     public static int mPortHTTP = -1;
     public static int mPortDns = TOR_DNS_PORT_DEFAULT;
@@ -100,6 +120,7 @@ public class OrbotService extends VpnService implements TorServiceConstants, Orb
     public static File appCacheHome;
     public static File fileTor;
     public static File fileTorRc;
+    private final ExecutorService mExecutor = Executors.newCachedThreadPool();
     boolean mIsLollipop = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP;
     TorEventHandler mEventHandler;
     OrbotVpnManager mVpnManager;
@@ -110,29 +131,12 @@ public class OrbotService extends VpnService implements TorServiceConstants, Orb
     private String mCurrentStatus = STATUS_OFF;
     private TorControlConnection conn = null;
     private int mLastProcessId = -1;
-    private ArrayList<String> configBuffer = null;
-    private ArrayList<String> resetBuffer = null;
     private File fileControlPort, filePid;
     private NotificationManager mNotificationManager = null;
     private NotificationCompat.Builder mNotifyBuilder;
     private boolean mNotificationShowing = false;
-    private final ExecutorService mExecutor = Executors.newCachedThreadPool();
-    private File mHSBasePath;
+    private File mHSBasePath, mV3OnionBasePath;
     private ArrayList<Bridge> alBridges = null;
-    private final String[] hsProjection = new String[]{
-            HiddenService._ID,
-            HiddenService.NAME,
-            HiddenService.DOMAIN,
-            HiddenService.PORT,
-            HiddenService.AUTH_COOKIE,
-            HiddenService.AUTH_COOKIE_VALUE,
-            HiddenService.ONION_PORT,
-            HiddenService.ENABLED};
-    private final String[] cookieProjection = new String[]{
-            ClientCookie._ID,
-            ClientCookie.DOMAIN,
-            ClientCookie.AUTH_COOKIE_VALUE,
-            ClientCookie.ENABLED};
 
     /**
      * @param bridgeList bridges that were manually entered into Orbot settings
@@ -144,13 +148,16 @@ public class OrbotService extends VpnService implements TorServiceConstants, Orb
         return bridgeList.split("\\n");
     }
 
-    public void debug(String msg) {
+    private static boolean useIPtProxy() {
+        String bridgeList = Prefs.getBridgesList();
+        return bridgeList.contains("obfs3") || bridgeList.contains("obfs4") || bridgeList.contains("meek");
+    }
 
+    public void debug(String msg) {
         Log.d(OrbotConstants.TAG, msg);
 
         if (Prefs.useDebugLogging()) {
             sendCallbackLogMessage(msg);
-
         }
     }
 
@@ -264,7 +271,6 @@ public class OrbotService extends VpnService implements TorServiceConstants, Orb
     }
 
     public int onStartCommand(Intent intent, int flags, int startId) {
-
         showToolbarNotification("", NOTIFY_ID, R.drawable.ic_stat_tor);
 
         if (intent != null)
@@ -347,7 +353,6 @@ public class OrbotService extends VpnService implements TorServiceConstants, Orb
 
     /**
      * if someone stops during startup, we may have to wait for the conn port to be setup, so we can properly shutdown tor
-     * @throws Exception
      */
     private void stopTorDaemon(boolean waitForConnection) throws Exception {
 
@@ -372,7 +377,10 @@ public class OrbotService extends VpnService implements TorServiceConstants, Orb
             if (!waitForConnection)
                 break;
 
-            try { Thread.sleep(3000);}catch (Exception e){}
+            try {
+                Thread.sleep(3000);
+            } catch (Exception e) {
+            }
         }
     }
 
@@ -384,13 +392,6 @@ public class OrbotService extends VpnService implements TorServiceConstants, Orb
         } catch (IOException e) {
             e.printStackTrace();
         }
-        /**
-         // if that fails, try again using native utils
-         try {
-         killProcess(fileTor, "-1"); // this is -HUP
-         } catch (Exception e) {
-         e.printStackTrace();
-         }**/
     }
 
     protected void logNotice(String msg) {
@@ -439,14 +440,14 @@ public class OrbotService extends VpnService implements TorServiceConstants, Orb
             fileControlPort = new File(getFilesDir(), TOR_CONTROL_PORT_FILE);
             filePid = new File(getFilesDir(), TOR_PID_FILE);
 
-            mHSBasePath = new File(
-                    getFilesDir().getAbsolutePath(),
-                    TorServiceConstants.HIDDEN_SERVICES_DIR
-            );
-
+            mHSBasePath = new File(getFilesDir().getAbsolutePath(), TorServiceConstants.HIDDEN_SERVICES_DIR);
             if (!mHSBasePath.isDirectory())
                 mHSBasePath.mkdirs();
 
+            mV3OnionBasePath = new File(getFilesDir().getAbsolutePath(), TorServiceConstants.ONION_SERVICES_DIR);
+            if (!mV3OnionBasePath.isDirectory())
+                mV3OnionBasePath.mkdirs();
+
             mEventHandler = new TorEventHandler(this);
 
             if (mNotificationManager == null) {
@@ -501,12 +502,12 @@ public class OrbotService extends VpnService implements TorServiceConstants, Orb
 
     private boolean pluggableTransportInstall() {
 
-        File fileCacheDir = new File(getCacheDir(),"pt");
+        File fileCacheDir = new File(getCacheDir(), "pt");
         if (!fileCacheDir.exists())
             fileCacheDir.mkdir();
         IPtProxy.setStateLocation(fileCacheDir.getAbsolutePath());
         String fileTestState = IPtProxy.getStateLocation();
-        debug ("IPtProxy state: " + fileTestState);
+        debug("IPtProxy state: " + fileTestState);
 
         return false;
     }
@@ -657,13 +658,10 @@ public class OrbotService extends VpnService implements TorServiceConstants, Orb
                 if (isPortUsed) //the specified port is not available, so let Tor find one instead
                     port++;
             }
-
-
             return port + "";
         }
 
         return portString;
-
     }
 
     public boolean updateTorConfigCustom(File fileTorRcCustom, String extraLines) throws IOException, TimeoutException {
@@ -710,8 +708,6 @@ public class OrbotService extends VpnService implements TorServiceConstants, Orb
      * The entire process for starting tor and related services is run from this method.
      */
     private void startTor() {
-
-
         try {
 
             // STATUS_STARTING is set in onCreate()
@@ -734,9 +730,8 @@ public class OrbotService extends VpnService implements TorServiceConstants, Orb
                     String torProcId = conn.getInfo("process/pid");
                     if (!TextUtils.isEmpty(torProcId))
                         mLastProcessId = Integer.parseInt(torProcId);
-                }
-                else {
-                    if (fileControlPort!=null && fileControlPort.exists())
+                } else {
+                    if (fileControlPort != null && fileControlPort.exists())
                         findExistingTorDaemon();
 
                 }
@@ -765,9 +760,14 @@ public class OrbotService extends VpnService implements TorServiceConstants, Orb
 
             if (success) {
                 try {
-                    updateOnionNames();
+                    updateLegacyV2OnionNames();
+                } catch (SecurityException se) {
+                    logNotice("unable to upload legacy v2 onion names");
+                }
+                try {
+                    updateV3OnionNames();
                 } catch (SecurityException se) {
-                    logNotice("unable to upload onion names");
+                    logNotice("unable to upload v3 onion names");
                 }
             }
 
@@ -777,21 +777,48 @@ public class OrbotService extends VpnService implements TorServiceConstants, Orb
             showToolbarNotification(
                     getString(R.string.unable_to_start_tor) + ": " + e.getMessage(),
                     ERROR_NOTIFY_ID, R.drawable.ic_stat_notifyerr);
+        }
+    }
 
+    private void updateV3OnionNames() throws SecurityException {
+        ContentResolver contentResolver = getApplicationContext().getContentResolver();
+        Cursor onionServices = contentResolver.query(V3_ONION_SERVICES_CONTENT_URI, null, null, null, null);
+        if (onionServices != null) {
+            try {
+                while (onionServices.moveToNext()) {
+                    String domain = onionServices.getString(onionServices.getColumnIndex(OnionService.DOMAIN));
+                    int localPort = onionServices.getInt(onionServices.getColumnIndex(OnionService.PORT));
+
+                    if (domain == null || TextUtils.isEmpty(domain)) {
+                        String v3OnionDirPath = new File(mV3OnionBasePath.getAbsolutePath(), "v3" + localPort).getCanonicalPath();
+                        File hostname = new File(v3OnionDirPath, "hostname");
+                        if (hostname.exists()) {
+                            domain = Utils.readString(new FileInputStream(hostname)).trim();
+                            ContentValues fields = new ContentValues();
+                            fields.put(OnionService.DOMAIN, domain);
+                            contentResolver.update(V3_ONION_SERVICES_CONTENT_URI, fields, "port=" + localPort, null);
+                        }
+                    }
+
+                }
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+            onionServices.close();
         }
     }
 
-    private void updateOnionNames() throws SecurityException {
+    private void updateLegacyV2OnionNames() throws SecurityException {
         // 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);
+        Cursor hidden_services = mCR.query(V2_HS_CONTENT_URI, LEGACY_V2_ONION_SERVICE_PROJECTION, 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));
+                    String HSDomain = hidden_services.getString(hidden_services.getColumnIndex(OnionService.DOMAIN));
+                    Integer HSLocalPort = hidden_services.getInt(hidden_services.getColumnIndex(OnionService.PORT));
+                    Integer HSAuthCookie = hidden_services.getInt(hidden_services.getColumnIndex(OnionService.AUTH_COOKIE));
+                    String HSAuthCookieValue = hidden_services.getString(hidden_services.getColumnIndex(OnionService.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))) {
@@ -806,10 +833,10 @@ public class OrbotService extends VpnService implements TorServiceConstants, Orb
                                 if (HSAuthCookie == 1) {
                                     String[] aux = onionHostname.split(" ");
                                     onionHostname = aux[0];
-                                    fields.put(HiddenService.AUTH_COOKIE_VALUE, aux[1]);
+                                    fields.put(OnionService.AUTH_COOKIE_VALUE, aux[1]);
                                 }
-                                fields.put(HiddenService.DOMAIN, onionHostname);
-                                mCR.update(HS_CONTENT_URI, fields, "port=" + HSLocalPort, null);
+                                fields.put(OnionService.DOMAIN, onionHostname);
+                                mCR.update(V2_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);
@@ -832,8 +859,6 @@ public class OrbotService extends VpnService implements TorServiceConstants, Orb
     }
 
     private boolean runTorShellCmd() throws Exception {
-        boolean result = true;
-
         File fileTorrcCustom = updateTorrcCustomFile();
 
         //make sure Tor exists and we can execute it
@@ -868,9 +893,7 @@ public class OrbotService extends VpnService implements TorServiceConstants, Orb
                 exitCode = exec(torCmdString, false);
             } catch (Exception e) {
                 logNotice("Tor was unable to start: " + e.getMessage());
-
                 throw new Exception("Tor was unable to start: " + e.getMessage());
-
             }
 
             if (exitCode != 0) {
@@ -885,13 +908,11 @@ public class OrbotService extends VpnService implements TorServiceConstants, Orb
                 logNotice(getString(R.string.couldn_t_start_tor_process_) + "; exit=" + exitCode);
                 throw new Exception(getString(R.string.couldn_t_start_tor_process_) + "; exit=" + exitCode);
             } else {
-
                 logNotice("Tor started; process id=" + mLastProcessId);
-                result = true;
             }
         }
 
-        return result;
+        return true;
     }
 
     protected void exec(Runnable runn) {
@@ -918,23 +939,17 @@ public class OrbotService extends VpnService implements TorServiceConstants, Orb
 
         while (conn == null && attempt++ < maxTries && (mCurrentStatus != STATUS_OFF)) {
             try {
-
                 controlPort = getControlPort();
-
                 if (controlPort != -1) {
                     logNotice(getString(R.string.connecting_to_control_port) + controlPort);
-
-
                     break;
                 }
 
             } catch (Exception ce) {
                 conn = null;
                 //    logException( "Error connecting to Tor local control port: " + ce.getMessage(),ce);
-
             }
 
-
             try {
                 //    logNotice("waiting...");
                 Thread.sleep(2000);
@@ -942,27 +957,19 @@ public class OrbotService extends VpnService implements TorServiceConstants, Orb
             }
         }
 
-
         if (controlPort != -1) {
             Socket torConnSocket = new Socket(IP_LOCALHOST, controlPort);
             torConnSocket.setSoTimeout(CONTROL_SOCKET_TIMEOUT);
-
             conn = new TorControlConnection(torConnSocket);
-
             conn.launchThread(true);//is daemon
-
         }
 
         if (conn != null) {
-
             logNotice("SUCCESS connected to Tor control port.");
 
             File fileCookie = new File(appCacheHome, TOR_CONTROL_COOKIE);
 
             if (fileCookie.exists()) {
-
-                // We extend NullEventHandler so that we don't need to provide empty
-                // implementations for all the events we don't care about.
                 logNotice("adding control port event handler");
 
                 conn.setEventHandler(mEventHandler);
@@ -1019,7 +1026,6 @@ public class OrbotService extends VpnService implements TorServiceConstants, Orb
                 }
 
                 sendCallbackPorts(mPortSOCKS, mPortHTTP, mPortDns, mPortTrans);
-
                 setTorNetworkEnabled(true);
 
                 return Integer.parseInt(torProcId);
@@ -1066,20 +1072,6 @@ public class OrbotService extends VpnService implements TorServiceConstants, Orb
         return result;
     }
 
-    /**
-     * Returns the port number that the HTTP proxy is running on
-     */
-    public int getHTTPPort() {
-        return mPortHTTP;
-    }
-
-    /**
-     * Returns the port number that the HTTP proxy is running on
-     */
-    public int getSOCKSPort() {
-        return mPortSOCKS;
-    }
-
     public String getInfo(String key) {
         try {
             if (conn != null) {
@@ -1092,71 +1084,11 @@ public class OrbotService extends VpnService implements TorServiceConstants, Orb
         return null;
     }
 
-    public String getConfiguration(String name) {
-        try {
-            if (conn != null) {
-                StringBuffer result = new StringBuffer();
-
-                List<ConfigEntry> listCe = conn.getConf(name);
-
-                Iterator<ConfigEntry> itCe = listCe.iterator();
-                ConfigEntry ce;
-
-
-                while (itCe.hasNext()) {
-                    ce = itCe.next();
-
-                    result.append(ce.key);
-                    result.append(' ');
-                    result.append(ce.value);
-                    result.append('\n');
-                }
-
-                return result.toString();
-            }
-        } catch (Exception ioe) {
-
-            logException("Unable to get Tor configuration: " + ioe.getMessage(), ioe);
-        }
-
-        return null;
-    }
-
-    /**
-     * Set configuration
-     **/
-    public boolean updateConfiguration(String name, String value, boolean saveToDisk) {
-
-
-        if (configBuffer == null)
-            configBuffer = new ArrayList<>();
-
-        if (resetBuffer == null)
-            resetBuffer = new ArrayList<>();
-
-        if (value == null || value.length() == 0) {
-            resetBuffer.add(name + RESET_STRING);
-
-        } else {
-
-            String sbConf = name +
-                    ' ' +
-                    value;
-            configBuffer.add(sbConf);
-        }
-
-        return false;
-    }
-
-
     public void setTorNetworkEnabled(final boolean isEnabled) throws IOException {
-
-        //it is possible to not have a connection yet, and someone might try to newnym
-        if (conn != null) {
+        if (conn != null) { // it is possible to not have a connection yet, and someone might try to newnym
             new Thread() {
                 public void run() {
                     try {
-
                         final String newValue = isEnabled ? "0" : "1";
                         conn.setConf("DisableNetwork", newValue);
                     } catch (Exception ioe) {
@@ -1165,7 +1097,6 @@ public class OrbotService extends VpnService implements TorServiceConstants, Orb
                 }
             }.start();
         }
-
     }
 
     public void sendSignalActive() {
@@ -1179,8 +1110,7 @@ public class OrbotService extends VpnService implements TorServiceConstants, Orb
     }
 
     public void newIdentity() {
-        //it is possible to not have a connection yet, and someone might try to newnym
-        if (conn != null) {
+        if (conn != null) { // it is possible to not have a connection yet, and someone might try to newnym
             new Thread() {
                 public void run() {
                     try {
@@ -1199,49 +1129,6 @@ public class OrbotService extends VpnService implements TorServiceConstants, Orb
         }
     }
 
-    public boolean saveConfiguration() {
-        try {
-            if (conn != null) {
-
-                if (resetBuffer != null && resetBuffer.size() > 0) {
-                    for (String value : configBuffer) {
-
-                        //   debug("removing torrc conf: " + value);
-
-
-                    }
-
-                    // conn.resetConf(resetBuffer);
-                    resetBuffer = null;
-                }
-
-                if (configBuffer != null && configBuffer.size() > 0) {
-
-                    for (String value : configBuffer) {
-
-                        debug("Setting torrc conf: " + value);
-
-
-                    }
-
-                    conn.setConf(configBuffer);
-
-                    configBuffer = null;
-                }
-
-                // Flush the configuration to disk.
-                //this is doing bad things right now NF 22/07/10
-                //conn.saveConf();
-
-                return true;
-            }
-        } catch (Exception ioe) {
-            logException("Unable to update Tor configuration: " + ioe.getMessage(), ioe);
-        }
-
-        return false;
-    }
-
     protected void sendCallbackBandwidth(long upload, long download, long written, long read) {
         Intent intent = new Intent(LOCAL_ACTION_BANDWIDTH);
 
@@ -1255,11 +1142,8 @@ public class OrbotService extends VpnService implements TorServiceConstants, Orb
     }
 
     private void sendCallbackLogMessage(final String logMessage) {
-
         mHandler.post(() -> {
-
-            Intent intent = new Intent(LOCAL_ACTION_LOG);
-            // You can also include some extra data.
+            Intent intent = new Intent(LOCAL_ACTION_LOG); // You can also include some extra data.
             intent.putExtra(LOCAL_EXTRA_LOG, logMessage);
             intent.putExtra(EXTRA_STATUS, mCurrentStatus);
 
@@ -1269,9 +1153,7 @@ public class OrbotService extends VpnService implements TorServiceConstants, Orb
     }
 
     private void sendCallbackPorts(int socksPort, int httpPort, int dnsPort, int transPort) {
-
-        Intent intent = new Intent(LOCAL_ACTION_PORTS);
-        // You can also include some extra data.
+        Intent intent = new Intent(LOCAL_ACTION_PORTS); // You can also include some extra data.
         intent.putExtra(EXTRA_SOCKS_PROXY_PORT, socksPort);
         intent.putExtra(EXTRA_HTTP_PROXY_PORT, httpPort);
         intent.putExtra(EXTRA_DNS_PORT, dnsPort);
@@ -1355,26 +1237,19 @@ public class OrbotService extends VpnService implements TorServiceConstants, Orb
 
     private StringBuffer processSettingsImpl(StringBuffer extraLines) throws IOException {
         logNotice(getString(R.string.updating_settings_in_tor_service));
-
         SharedPreferences prefs = Prefs.getSharedPrefs(getApplicationContext());
 
         boolean useBridges = Prefs.bridgesEnabled();
-
         boolean becomeRelay = prefs.getBoolean(OrbotConstants.PREF_OR, false);
         boolean ReachableAddresses = prefs.getBoolean(OrbotConstants.PREF_REACHABLE_ADDRESSES, false);
-
         boolean enableStrictNodes = prefs.getBoolean("pref_strict_nodes", false);
         String entranceNodes = prefs.getString("pref_entrance_nodes", "");
         String exitNodes = prefs.getString("pref_exit_nodes", "");
         String excludeNodes = prefs.getString("pref_exclude_nodes", "");
 
         if (!useBridges) {
-
             extraLines.append("UseBridges 0").append('\n');
-
-            if (Prefs.useVpn()) //set the proxy here if we aren't using a bridge
-            {
-
+            if (Prefs.useVpn()) { //set the proxy here if we aren't using a bridge
                 if (!mIsLollipop) {
                     String proxyType = "socks5";
                     extraLines.append(proxyType + "Proxy" + ' ' + OrbotVpnManager.sSocksProxyLocalhost + ':' + OrbotVpnManager.sSocksProxyServerPort).append('\n');
@@ -1400,15 +1275,12 @@ public class OrbotService extends VpnService implements TorServiceConstants, Orb
 
                         } else if (proxyPass != null)
                             extraLines.append(proxyType + "ProxyAuthenticator" + ' ' + proxyUser + ':' + proxyPort).append('\n');
-
-
                     }
                 }
             }
         } else {
 
             loadBridgeDefaults();
-
             extraLines.append("UseBridges 1").append('\n');
             //    extraLines.append("UpdateBridgesFromAuthority 1").append('\n');
 
@@ -1445,12 +1317,9 @@ public class OrbotService extends VpnService implements TorServiceConstants, Orb
                 extraLines.append("Bridge ");
                 extraLines.append(bridgeLine);
                 extraLines.append("\n");
-
             }
-
         }
 
-
         //only apply GeoIP if you need it
         File fileGeoIP = new File(appBinHome, GEOIP_ASSET_KEY);
         File fileGeoIP6 = new File(appBinHome, GEOIP6_ASSET_KEY);
@@ -1473,16 +1342,12 @@ public class OrbotService extends VpnService implements TorServiceConstants, Orb
 
         try {
             if (ReachableAddresses) {
-                String ReachableAddressesPorts =
-                        prefs.getString(OrbotConstants.PREF_REACHABLE_ADDRESSES_PORTS, "*:80,*:443");
-
+                String ReachableAddressesPorts = prefs.getString(OrbotConstants.PREF_REACHABLE_ADDRESSES_PORTS, "*:80,*:443");
                 extraLines.append("ReachableAddresses" + ' ' + ReachableAddressesPorts).append('\n');
-
             }
 
         } catch (Exception e) {
             showToolbarNotification(getString(R.string.your_reachableaddresses_settings_caused_an_exception_), ERROR_NOTIFY_ID, R.drawable.ic_stat_notifyerr);
-
             return null;
         }
 
@@ -1490,7 +1355,6 @@ public class OrbotService extends VpnService implements TorServiceConstants, Orb
             if (becomeRelay && (!useBridges) && (!ReachableAddresses)) {
                 int ORPort = Integer.parseInt(prefs.getString(OrbotConstants.PREF_OR_PORT, "9001"));
                 String nickname = prefs.getString(OrbotConstants.PREF_OR_NICKNAME, "Orbot");
-
                 String dnsFile = writeDNSFile();
 
                 extraLines.append("ServerDNSResolvConfFile" + ' ' + dnsFile).append('\n');
@@ -1501,33 +1365,56 @@ public class OrbotService extends VpnService implements TorServiceConstants, Orb
             }
         } catch (Exception e) {
             showToolbarNotification(getString(R.string.your_relay_settings_caused_an_exception_), ERROR_NOTIFY_ID, R.drawable.ic_stat_notifyerr);
-
-
             return null;
         }
 
-        ContentResolver mCR = getApplicationContext().getContentResolver();
+        ContentResolver contentResolver = getApplicationContext().getContentResolver();
+        addV3OnionServicesToTorrc(extraLines, contentResolver);
+//        addV2HiddenServicesToTorrc(extraLines, contentResolver);
+//        addV2ClientCookiesToTorrc(extraLines, contentResolver);
+        return extraLines;
+    }
 
+    private void addV3OnionServicesToTorrc(StringBuffer torrc, ContentResolver contentResolver) {
         try {
-            /* ---- Hidden Services ---- */
-            Cursor hidden_services = mCR.query(HS_CONTENT_URI, hsProjection, HiddenService.ENABLED + "=1", null, null);
+            Cursor onionServices = contentResolver.query(V3_ONION_SERVICES_CONTENT_URI, V3_ONION_SERVICE_PROJECTION, OnionService.ENABLED + "=1", null, null);
+            if (onionServices != null) {
+                while (onionServices.moveToNext()) {
+                    int localPort = onionServices.getInt(onionServices.getColumnIndex(OnionService.PORT));
+                    int onionPort = onionServices.getInt(onionServices.getColumnIndex(OnionService.ONION_PORT));
+                    String v3DirPath = new File(mV3OnionBasePath.getAbsolutePath(), "v3" + localPort).getCanonicalPath();
+                    torrc.append("HiddenServiceDir ").append(v3DirPath).append("\n");
+                    torrc.append("HiddenServiceVersion 3").append("\n");
+                    torrc.append("HiddenServicePort ").append(onionPort).append(" 127.0.0.1:").append(localPort).append("\n");
+                }
+                onionServices.close();
+            }
+        } catch (Exception e) {
+            Log.e(TAG, e.getLocalizedMessage());
+        }
+    }
+
+    // todo needs modifications to set hidden service version back after doing v3 stuff...
+    private void addV2HiddenServicesToTorrc(StringBuffer torrc, ContentResolver contentResolver) {
+        try {
+            Cursor hidden_services = contentResolver.query(V2_HS_CONTENT_URI, LEGACY_V2_ONION_SERVICE_PROJECTION, OnionService.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 HSname = hidden_services.getString(hidden_services.getColumnIndex(OnionService.NAME));
+                        int HSLocalPort = hidden_services.getInt(hidden_services.getColumnIndex(OnionService.PORT));
+                        int HSOnionPort = hidden_services.getInt(hidden_services.getColumnIndex(OnionService.ONION_PORT));
+                        int HSAuthCookie = hidden_services.getInt(hidden_services.getColumnIndex(OnionService.AUTH_COOKIE));
                         String hsDirPath = new File(mHSBasePath.getAbsolutePath(), "hs" + HSLocalPort).getCanonicalPath();
 
                         debug("Adding hidden service on port: " + HSLocalPort);
 
-                        extraLines.append("HiddenServiceDir" + ' ' + hsDirPath).append('\n');
-                        extraLines.append("HiddenServicePort" + ' ' + HSOnionPort + " 127.0.0.1:" + HSLocalPort).append('\n');
-                        extraLines.append("HiddenServiceVersion 2").append('\n');
+                        torrc.append("HiddenServiceDir" + ' ' + hsDirPath).append('\n');
+                        torrc.append("HiddenServicePort" + ' ' + HSOnionPort + " 127.0.0.1:" + HSLocalPort).append('\n');
+                        torrc.append("HiddenServiceVersion 2").append('\n');
 
                         if (HSAuthCookie == 1)
-                            extraLines.append("HiddenServiceAuthorizeClient stealth " + HSname).append('\n');
+                            torrc.append("HiddenServiceAuthorizeClient stealth " + HSname).append('\n');
                     }
                 } catch (NumberFormatException e) {
                     Log.e(OrbotConstants.TAG, "error parsing hsport", e);
@@ -1539,28 +1426,25 @@ public class OrbotService extends VpnService implements TorServiceConstants, Orb
             }
         } catch (SecurityException se) {
         }
+    }
 
+    private void addV2ClientCookiesToTorrc(StringBuffer torrc, ContentResolver contentResolver) {
         try {
-
-            /* ---- Client Cookies ---- */
-            Cursor client_cookies = mCR.query(COOKIE_CONTENT_URI, cookieProjection, ClientCookie.ENABLED + "=1", null, null);
+            Cursor client_cookies = contentResolver.query(COOKIE_CONTENT_URI, LEGACY_COOKIE_PROJECTION, 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');
+                        torrc.append("HidServAuth" + ' ' + domain + ' ' + cookie).append('\n');
                     }
                 } catch (Exception e) {
                     Log.e(OrbotConstants.TAG, "error starting share server", e);
                 }
-
                 client_cookies.close();
             }
         } catch (SecurityException se) {
         }
-
-        return extraLines;
     }
 
     //using Google DNS for now as the public DNS server
@@ -1614,7 +1498,6 @@ public class OrbotService extends VpnService implements TorServiceConstants, Orb
     @Override
     public IBinder onBind(Intent intent) {
         Log.e(TAG, "onBind");
-        //do nothing here
         return super.onBind(intent); // invoking super class will call onRevoke() when appropriate
     }
 
@@ -1676,8 +1559,7 @@ public class OrbotService extends VpnService implements TorServiceConstants, Orb
             alBridges = new ArrayList<>();
 
             try {
-                BufferedReader in =
-                        new BufferedReader(new InputStreamReader(getResources().openRawResource(R.raw.bridges), "UTF-8"));
+                BufferedReader in = new BufferedReader(new InputStreamReader(getResources().openRawResource(R.raw.bridges), "UTF-8"));
                 String str;
 
                 while ((str = in.readLine()) != null) {
@@ -1692,9 +1574,7 @@ public class OrbotService extends VpnService implements TorServiceConstants, Orb
                         sbConfig.append(st.nextToken()).append(' ');
 
                     b.config = sbConfig.toString().trim();
-
                     alBridges.add(b);
-
                 }
 
                 in.close();
@@ -1702,7 +1582,6 @@ public class OrbotService extends VpnService implements TorServiceConstants, Orb
                 e.printStackTrace();
             }
         }
-
     }
 
     private void getBridges(String type, StringBuffer extraLines) {
@@ -1728,20 +1607,18 @@ public class OrbotService extends VpnService implements TorServiceConstants, Orb
                     break;
             }
         }
-
     }
 
-    public static final class HiddenService implements BaseColumns {
+    public static final class OnionService 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() {
+        private OnionService() {
         }
     }
 
@@ -1768,7 +1645,6 @@ public class OrbotService extends VpnService implements TorServiceConstants, Orb
         }
 
         public void run() {
-
             String action = mIntent.getAction();
 
             if (!TextUtils.isEmpty(action)) {
@@ -1804,11 +1680,8 @@ public class OrbotService extends VpnService implements TorServiceConstants, Orb
                     if (mVpnManager != null && (!mVpnManager.isStarted())) {
                         //start VPN here
                         Intent vpnIntent = VpnService.prepare(OrbotService.this);
-                        if (vpnIntent == null) //then we can run the VPN
-                        {
+                        if (vpnIntent == null) { //then we can run the VPN
                             mVpnManager.handleIntent(new Builder(), mIntent);
-
-
                         }
                     }
 
@@ -1828,9 +1701,7 @@ public class OrbotService extends VpnService implements TorServiceConstants, Orb
                 } else if (action.equals(CMD_ACTIVE)) {
                     sendSignalActive();
                 } else if (action.equals(CMD_SET_EXIT)) {
-
                     setExitNode(mIntent.getStringExtra("exit"));
-
                 } else {
                     Log.w(OrbotConstants.TAG, "unhandled OrbotService Intent: " + action);
                 }
@@ -1852,5 +1723,4 @@ public class OrbotService extends VpnService implements TorServiceConstants, Orb
             }
         }
     }
-
 }
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 f1769687..915f149d 100644
--- a/orbotservice/src/main/java/org/torproject/android/service/TorServiceConstants.java
+++ b/orbotservice/src/main/java/org/torproject/android/service/TorServiceConstants.java
@@ -112,6 +112,6 @@ public interface TorServiceConstants {
     String OBFSCLIENT_ASSET_KEY = "obfs4proxy";
 
     String HIDDEN_SERVICES_DIR = "hidden_services";
-
+    String ONION_SERVICES_DIR = "v3_onion_services";
 
 }





More information about the tor-commits mailing list