[tor-commits] [orbot/master] Added first stab at a MoatActivity. Doesn't change bridges automatically, yet. Also, Volley needs to be proxied.

n8fr8 at torproject.org n8fr8 at torproject.org
Tue Apr 28 21:05:02 UTC 2020


commit e7cfe4651d6874f35579f724f22e83c78cec04af
Author: Benjamin Erhart <berhart at netzarchitekten.com>
Date:   Fri Apr 17 13:43:05 2020 +0200

    Added first stab at a MoatActivity. Doesn't change bridges automatically, yet. Also, Volley needs to be proxied.
---
 app/build.gradle                                   |  11 +-
 app/src/main/AndroidManifest.xml                   | 139 ++++++------
 .../ui/onboarding/BridgeWizardActivity.java        |  48 +++--
 .../android/ui/onboarding/MoatActivity.java        | 233 +++++++++++++++++++++
 app/src/main/res/layout/activity_moat.xml          |  53 +++++
 app/src/main/res/layout/content_bridge_wizard.xml  |   9 +-
 app/src/main/res/menu/moat.xml                     |  28 +++
 app/src/main/res/values/strings.xml                |   9 +-
 8 files changed, 443 insertions(+), 87 deletions(-)

diff --git a/app/build.gradle b/app/build.gradle
index 20707251..e6cf6de7 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -24,7 +24,7 @@ def getVersionName = { ->
 }
 
 android {
-   signingConfigs {
+    signingConfigs {
         release {
             if (keystorePropertiesFile.canRead()) {
                 keyAlias keystoreProperties['keyAlias']
@@ -118,15 +118,17 @@ android {
 
 dependencies {
     implementation project(':orbotservice')
-    implementation 'com.google.android.material:material:1.0.0'
+    implementation 'com.google.android.material:material:1.1.0'
     implementation 'pl.bclogic:pulsator4droid:1.0.3'
     implementation 'com.github.apl-devs:appintro:v4.2.2'
-    implementation 'com.github.javiersantos:AppUpdater:2.6.4'
+    implementation 'com.github.javiersantos:AppUpdater:2.7'
     androidTestImplementation "tools.fastlane:screengrab:1.2.0"
+    implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0'
+    implementation 'com.android.volley:volley:1.1.1'
 }
 
 // Map for the version code that gives each ABI a value.
-ext.abiCodes = ['armeabi-v7a':'1', 'arm64-v8a':'2', 'mips':'3', 'x86':'4', 'x86_64':'5']
+ext.abiCodes = ['armeabi-v7a': '1', 'arm64-v8a': '2', 'mips': '3', 'x86': '4', 'x86_64': '5']
 
 import com.android.build.OutputFile
 
@@ -141,4 +143,3 @@ android.applicationVariants.all { variant ->
         }
     }
 }
-
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 1d7a9f16..011b78c7 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -4,6 +4,14 @@
     package="org.torproject.android"
     android:installLocation="internalOnly">
 
+    <!--
+        Some Chromebooks don't support touch. Although not essential,
+        it's a good idea to explicitly include this declaration.
+    -->
+    <uses-feature
+        android:name="android.hardware.touchscreen"
+        android:required="false" />
+
     <uses-permission android:name="android.permission.INTERNET" />
     <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
     <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
@@ -11,10 +19,6 @@
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
-    <!-- Some Chromebooks don't support touch. Although not essential,
-         it's a good idea to explicitly include this declaration. -->
-    <uses-feature android:name="android.hardware.touchscreen"
-        android:required="false" />
 
     <application
         android:name=".OrbotApp"
@@ -26,8 +30,8 @@
         android:icon="@drawable/ic_launcher"
         android:label="@string/app_name"
         android:theme="@style/DefaultTheme"
-        tools:replace="android:allowBackup"
-        >
+        tools:replace="android:allowBackup">
+
         <activity
             android:name=".OrbotMainActivity"
             android:excludeFromRecents="false"
@@ -46,18 +50,17 @@
                 <data android:scheme="bridge" />
             </intent-filter>
             <intent-filter>
-                <category android:name="android.intent.category.DEFAULT" />
-
                 <action android:name="org.torproject.android.REQUEST_HS_PORT" />
+
+                <category android:name="android.intent.category.DEFAULT" />
             </intent-filter>
             <intent-filter>
-                <category android:name="android.intent.category.DEFAULT" />
-
                 <action android:name="org.torproject.android.START_TOR" />
+
+                <category android:name="android.intent.category.DEFAULT" />
             </intent-filter>
-        </activity>
+        </activity> <!-- This is for ensuring the background service still runs when/if the app is swiped away -->
 
-        <!--         This is for ensuring the background service still runs when/if the app is swiped away -->
         <activity
             android:name=".service.util.DummyActivity"
             android:allowTaskReparenting="true"
@@ -69,39 +72,72 @@
             android:noHistory="true"
             android:stateNotNeeded="true"
             android:theme="@android:style/Theme.Translucent" />
+
         <activity
             android:name=".ui.VPNEnableActivity"
             android:exported="false"
             android:label="@string/app_name" />
+
         <activity
             android:name=".settings.SettingsPreferences"
             android:label="@string/app_name" />
+
         <activity
             android:name=".ui.AppManagerActivity"
             android:label="@string/app_name"
             android:theme="@style/Theme.AppCompat" />
 
-        <service
-            android:name=".service.OrbotService"
-            android:enabled="true"
-            android:permission="android.permission.BIND_VPN_SERVICE"
-            android:stopWithTask="false"></service>
-        <service
-            android:name=".service.vpn.TorVpnService"
-            android:enabled="true"
-            android:permission="android.permission.BIND_VPN_SERVICE">
-            <intent-filter>
-                <action android:name="android.net.VpnService" />
-            </intent-filter>
-        </service>
+        <activity
+            android:name=".ui.hiddenservices.HiddenServicesActivity"
+            android:label="@string/title_activity_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"
+            android:theme="@style/DefaultTheme">
+            <meta-data
+                android:name="android.support.PARENT_ACTIVITY"
+                android:value=".OrbotMainActivity" />
+        </activity>
+
+        <activity android:name=".ui.onboarding.OnboardingActivity" />
+        <activity android:name=".ui.onboarding.BridgeWizardActivity" />
+        <activity android:name=".ui.onboarding.MoatActivity" />
+
+        <provider
+            android:name=".ui.hiddenservices.providers.HSContentProvider"
+            android:authorities="org.torproject.android.ui.hiddenservices.providers"
+            android:exported="false" />
+
+        <provider
+            android:name="androidx.core.content.FileProvider"
+            android:authorities="org.torproject.android.ui.hiddenservices.storage"
+            android:exported="false"
+            android:grantUriPermissions="true">
+            <meta-data
+                android:name="android.support.FILE_PROVIDER_PATHS"
+                android:resource="@xml/hidden_services_paths" />
+        </provider>
+
+        <provider
+            android:name=".ui.hiddenservices.providers.CookieContentProvider"
+            android:authorities="org.torproject.android.ui.hiddenservices.providers.cookie"
+            android:exported="false" />
 
         <receiver
             android:name=".service.StartTorReceiver"
-            android:exported="true">
+            android:exported="true"
+            tools:ignore="ExportedReceiver">
             <intent-filter>
                 <action android:name="org.torproject.android.intent.action.START" />
             </intent-filter>
         </receiver>
+
         <receiver
             android:name=".OnBootReceiver"
             android:enabled="true"
@@ -123,45 +159,20 @@
             </intent-filter>
         </receiver>
 
-        <activity
-            android:name=".ui.hiddenservices.HiddenServicesActivity"
-            android:label="@string/title_activity_hidden_services"
-            android:theme="@style/DefaultTheme">
-            <meta-data
-                android:name="android.support.PARENT_ACTIVITY"
-                android:value=".OrbotMainActivity" />
-        </activity>
-
-        <provider
-            android:name=".ui.hiddenservices.providers.HSContentProvider"
-            android:authorities="org.torproject.android.ui.hiddenservices.providers"
-            android:exported="false" />
-        <provider
-            android:name="androidx.core.content.FileProvider"
-            android:authorities="org.torproject.android.ui.hiddenservices.storage"
-            android:exported="false"
-            android:grantUriPermissions="true">
-            <meta-data
-                android:name="android.support.FILE_PROVIDER_PATHS"
-                android:resource="@xml/hidden_services_paths" />
-        </provider>
-
-        <activity
-            android:name=".ui.hiddenservices.ClientCookiesActivity"
-            android:label="@string/client_cookies"
-            android:theme="@style/DefaultTheme">
-            <meta-data
-                android:name="android.support.PARENT_ACTIVITY"
-                android:value=".OrbotMainActivity" />
-        </activity>
-
-        <activity android:name=".ui.onboarding.OnboardingActivity"/>
-        <activity android:name=".ui.onboarding.BridgeWizardActivity"/>
+        <service
+            android:name=".service.OrbotService"
+            android:enabled="true"
+            android:permission="android.permission.BIND_VPN_SERVICE"
+            android:stopWithTask="false" />
 
-        <provider
-            android:name=".ui.hiddenservices.providers.CookieContentProvider"
-            android:authorities="org.torproject.android.ui.hiddenservices.providers.cookie"
-            android:exported="false" />
+        <service
+            android:name=".service.vpn.TorVpnService"
+            android:enabled="true"
+            android:permission="android.permission.BIND_VPN_SERVICE">
+            <intent-filter>
+                <action android:name="android.net.VpnService" />
+            </intent-filter>
+        </service>
     </application>
 
 </manifest>
\ No newline at end of file
diff --git a/app/src/main/java/org/torproject/android/ui/onboarding/BridgeWizardActivity.java b/app/src/main/java/org/torproject/android/ui/onboarding/BridgeWizardActivity.java
index 6e54e103..5ffe79e5 100644
--- a/app/src/main/java/org/torproject/android/ui/onboarding/BridgeWizardActivity.java
+++ b/app/src/main/java/org/torproject/android/ui/onboarding/BridgeWizardActivity.java
@@ -8,13 +8,16 @@ import android.content.Intent;
 import android.net.Uri;
 import android.os.AsyncTask;
 import android.os.Bundle;
-import androidx.appcompat.app.AppCompatActivity;
-import androidx.appcompat.widget.Toolbar;
 import android.text.TextUtils;
 import android.view.MenuItem;
 import android.view.View;
 import android.widget.RadioButton;
 import android.widget.TextView;
+
+import androidx.appcompat.app.ActionBar;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.widget.Toolbar;
+
 import org.torproject.android.R;
 import org.torproject.android.service.util.Prefs;
 import org.torproject.android.settings.LocaleHelper;
@@ -36,7 +39,11 @@ public class BridgeWizardActivity extends AppCompatActivity {
         setContentView(R.layout.activity_bridge_wizard);
         Toolbar toolbar = findViewById(R.id.toolbar);
         setSupportActionBar(toolbar);
-        getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+
+        ActionBar actionBar = getSupportActionBar();
+        if (actionBar != null) {
+            actionBar.setDisplayHomeAsUpEnabled(true);
+        }
 
         tvStatus = findViewById(R.id.lbl_bridge_test_status);
         tvStatus.setVisibility(View.GONE);
@@ -83,6 +90,14 @@ public class BridgeWizardActivity extends AppCompatActivity {
             }
         });
 
+        RadioButton btnMoat = findViewById(R.id.btnMoat);
+        btnMoat.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                startActivity(new Intent(BridgeWizardActivity.this, MoatActivity.class));
+            }
+        });
+
         if (!Prefs.bridgesEnabled())
             btnDirect.setChecked(true);
         else if (Prefs.getBridgesList().equals("meek"))
@@ -128,7 +143,7 @@ public class BridgeWizardActivity extends AppCompatActivity {
                 .setPositiveButton(R.string.get_bridges_web, new Dialog.OnClickListener() {
                     @Override
                     public void onClick(DialogInterface dialog, int which) {
-                        openBrowser(URL_TOR_BRIDGES, true);
+                        openBrowser(URL_TOR_BRIDGES);
                     }
                 }).show();
     }
@@ -146,7 +161,8 @@ public class BridgeWizardActivity extends AppCompatActivity {
     /*
      * Launch the system activity for Uri viewing with the provided url
      */
-    private void openBrowser(final String browserLaunchUrl, boolean forceExternal) {
+    @SuppressWarnings("SameParameterValue")
+    private void openBrowser(final String browserLaunchUrl) {
         startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(browserLaunchUrl)));
     }
 
@@ -174,18 +190,17 @@ public class BridgeWizardActivity extends AppCompatActivity {
         @Override
         protected Boolean doInBackground(String... host) {
             // Background Code
-            boolean result = false;
-
             for (int i = 0; i < host.length; i++) {
                 String testHost = host[i];
                 i++; //move to the port
                 int testPort = Integer.parseInt(host[i]);
-                result = isHostReachable(testHost, testPort, 10000);
-                if (result)
-                    return result;
+
+                if (isHostReachable(testHost, testPort, 10000)) {
+                    return true;
+                }
             }
 
-            return result;
+            return false;
         }
 
         @Override
@@ -201,22 +216,23 @@ public class BridgeWizardActivity extends AppCompatActivity {
         }
     }
 
+    @SuppressWarnings("SameParameterValue")
     private static boolean isHostReachable(String serverAddress, int serverTCPport, int timeoutMS) {
         boolean connected = false;
-        Socket socket;
+
         try {
-            socket = new Socket();
+            Socket socket = new Socket();
             SocketAddress socketAddress = new InetSocketAddress(serverAddress, serverTCPport);
             socket.connect(socketAddress, timeoutMS);
             if (socket.isConnected()) {
                 connected = true;
                 socket.close();
             }
-        } catch (IOException e) {
+        }
+        catch (IOException e) {
             e.printStackTrace();
-        } finally {
-            socket = null;
         }
+
         return connected;
     }
 }
diff --git a/app/src/main/java/org/torproject/android/ui/onboarding/MoatActivity.java b/app/src/main/java/org/torproject/android/ui/onboarding/MoatActivity.java
new file mode 100644
index 00000000..e5ea5459
--- /dev/null
+++ b/app/src/main/java/org/torproject/android/ui/onboarding/MoatActivity.java
@@ -0,0 +1,233 @@
+package org.torproject.android.ui.onboarding;
+
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.graphics.BitmapFactory;
+import android.os.Bundle;
+import android.util.Base64;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.EditText;
+import android.widget.ImageView;
+
+import androidx.appcompat.app.ActionBar;
+import androidx.appcompat.app.AlertDialog;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.widget.Toolbar;
+
+import com.android.volley.Request;
+import com.android.volley.RequestQueue;
+import com.android.volley.Response;
+import com.android.volley.VolleyError;
+import com.android.volley.toolbox.JsonObjectRequest;
+import com.android.volley.toolbox.Volley;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.torproject.android.R;
+import org.torproject.android.service.util.Prefs;
+
+/**
+ Implements the MOAT protocol: Fetches OBFS4 bridges via Meek Azure.
+
+ The bare minimum of the communication is implemented. E.g. no check, if OBFS4 is possible or which
+ protocol version the server wants to speak. The first should be always good, as OBFS4 is the most widely
+ supported bridge type, the latter should be the same as we requested (0.1.0) anyway.
+
+ API description:
+ https://github.com/NullHypothesis/bridgedb#accessing-the-moat-interface
+ */
+public class MoatActivity extends AppCompatActivity implements View.OnClickListener {
+
+    private static String moatBaseUrl = "https://bridges.torproject.org/moat";
+
+    private ImageView mCaptchaIv;
+    private EditText mSolutionEt;
+
+    private String mChallenge;
+
+    private RequestQueue mQueue;
+
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_moat);
+
+        Toolbar toolbar = findViewById(R.id.toolbar);
+        setSupportActionBar(toolbar);
+
+        ActionBar actionBar = getSupportActionBar();
+        if (actionBar != null) {
+            actionBar.setDisplayHomeAsUpEnabled(true);
+        }
+
+        setTitle(getString(R.string.request_bridges));
+
+        mCaptchaIv = findViewById(R.id.captchaIv);
+        mSolutionEt = findViewById(R.id.solutionEt);
+
+        findViewById(R.id.requestBt).setOnClickListener(this);
+
+        mQueue = Volley.newRequestQueue(this);
+    }
+
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        super.onCreateOptionsMenu(menu);
+
+        getMenuInflater().inflate(R.menu.moat, menu);
+
+        return true;
+    }
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+
+        // Set to Meek bridge.
+        Prefs.setBridgesList("meek");
+        Prefs.putBridgesEnabled(true);
+
+        fetchCaptcha();
+    }
+
+    @Override
+    public void onClick(View view) {
+        Log.d(MoatActivity.class.toString(), "Request Bridge!");
+
+        requestBridges(mSolutionEt.getText().toString());
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        if (item.getItemId() == R.id.menu_refresh) {
+            fetchCaptcha();
+        }
+
+        return super.onOptionsItemSelected(item);
+    }
+
+    private void fetchCaptcha() {
+        JsonObjectRequest request = buildRequest("fetch",
+                "\"type\": \"client-transports\", \"supported\": [\"obfs4\"]",
+                new Response.Listener<JSONObject>() {
+                    @Override
+                    public void onResponse(JSONObject response) {
+                        try {
+                            JSONObject data = response.getJSONArray("data").getJSONObject(0);
+                            mChallenge = data.getString("challenge");
+
+                            byte[] image = Base64.decode(data.getString("image"), Base64.DEFAULT);
+                            mCaptchaIv.setImageBitmap(BitmapFactory.decodeByteArray(image, 0, image.length));
+
+                        } catch (JSONException e) {
+                            Log.d(MoatActivity.class.toString(), "Error decoding answer.");
+
+                            new AlertDialog.Builder(MoatActivity.this)
+                                    .setTitle(R.string.error)
+                                    .setMessage(e.getLocalizedMessage())
+                                    .setNegativeButton(R.string.btn_cancel, new Dialog.OnClickListener() {
+                                        @Override
+                                        public void onClick(DialogInterface dialog, int which) {
+                                            //Do nothing.
+                                        }
+                                    })
+                                    .show();
+                        }
+                    }
+                });
+
+        if (request != null) {
+            mQueue.add(request);
+        }
+    }
+
+    private void requestBridges(String solution) {
+        JsonObjectRequest request = buildRequest("check",
+                "\"id\": \"2\", \"type\": \"moat-solution\", \"transport\": \"obfs4\", \"challenge\": \""
+                        + mChallenge + "\", \"solution\": \"" + solution + "\", \"qrcode\": \"false\"",
+                new Response.Listener<JSONObject>() {
+                    @Override
+                    public void onResponse(JSONObject response) {
+                        try {
+                            JSONArray bridges = response.getJSONArray("data").getJSONObject(0).getJSONArray("bridges");
+
+                            Log.d(MoatActivity.class.toString(), "Bridges: " + bridges.toString());
+
+                            StringBuilder sb = new StringBuilder();
+
+                            for (int i = 0; i < bridges.length(); i++) {
+                                sb.append(bridges.getString(i)).append("\n");
+                            }
+
+                            Prefs.setBridgesList(sb.toString());
+                            Prefs.putBridgesEnabled(true);
+
+                            MoatActivity.this.finish();
+
+                        } catch (JSONException e) {
+                            Log.d(MoatActivity.class.toString(), "Error decoding answer: " + response.toString());
+
+                            new AlertDialog.Builder(MoatActivity.this)
+                                    .setTitle(R.string.error)
+                                    .setMessage(e.getLocalizedMessage())
+                                    .setNegativeButton(R.string.btn_cancel, new Dialog.OnClickListener() {
+                                        @Override
+                                        public void onClick(DialogInterface dialog, int which) {
+                                            //Do nothing.
+                                        }
+                                    })
+                                    .show();
+                        }
+                    }
+                });
+
+        if (request != null) {
+            mQueue.add(request);
+        }
+    }
+
+    private JsonObjectRequest buildRequest(String endpoint, String payload, Response.Listener<JSONObject> listener) {
+        JSONObject requestBody;
+
+        try {
+            requestBody = new JSONObject("{\"data\": [{\"version\": \"0.1.0\", " + payload + "}]}");
+        } catch (JSONException e) {
+            return null;
+        }
+
+        Log.d(MoatActivity.class.toString(), "Request: " + requestBody.toString());
+
+        return new JsonObjectRequest(
+                Request.Method.POST,
+                moatBaseUrl + "/" + endpoint,
+                requestBody,
+                listener,
+                new Response.ErrorListener() {
+                    @Override
+                    public void onErrorResponse(VolleyError error) {
+                        Log.d(MoatActivity.class.toString(), "Error response.");
+
+                        new AlertDialog.Builder(MoatActivity.this)
+                                .setTitle(R.string.error)
+                                .setMessage(error.getLocalizedMessage())
+                                .setNegativeButton(R.string.btn_cancel, new Dialog.OnClickListener() {
+                                    @Override
+                                    public void onClick(DialogInterface dialog, int which) {
+                                        //Do nothing.
+                                    }
+                                })
+                                .show();
+                    }
+                }
+        ) {
+            public String getBodyContentType() {
+                return "application/vnd.api+json";
+            }
+        };
+    }
+}
diff --git a/app/src/main/res/layout/activity_moat.xml b/app/src/main/res/layout/activity_moat.xml
new file mode 100644
index 00000000..fe01873c
--- /dev/null
+++ b/app/src/main/res/layout/activity_moat.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical"
+    android:background="@color/dark_purple"
+    tools:context=".ui.onboarding.MoatActivity">
+
+    <com.google.android.material.appbar.AppBarLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:theme="@style/DefaultTheme.AppBarOverlay">
+
+        <androidx.appcompat.widget.Toolbar
+            android:id="@+id/toolbar"
+            android:layout_width="match_parent"
+            android:layout_height="?attr/actionBarSize"
+            android:background="?attr/colorPrimary"
+            app:popupTheme="@style/DefaultTheme.PopupOverlay" />
+
+    </com.google.android.material.appbar.AppBarLayout>
+
+    <TextView
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_margin="12dp"
+        android:text="@string/solve_captcha_instruction" />
+
+    <ImageView
+        android:id="@+id/captchaIv"
+        android:layout_width="match_parent"
+        android:layout_height="240dp"
+        android:contentDescription="@string/captcha"
+        tools:srcCompat="@tools:sample/backgrounds/scenic" />
+
+    <EditText
+        android:id="@+id/solutionEt"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:autofillHints=""
+        android:hint="@string/enter_characters_from_image"
+        android:ems="10"
+        android:inputType="textShortMessage|text" />
+
+    <Button
+        android:id="@+id/requestBt"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:text="@string/request_bridges" />
+
+</LinearLayout>
diff --git a/app/src/main/res/layout/content_bridge_wizard.xml b/app/src/main/res/layout/content_bridge_wizard.xml
index a5b7995b..1a1e98e0 100644
--- a/app/src/main/res/layout/content_bridge_wizard.xml
+++ b/app/src/main/res/layout/content_bridge_wizard.xml
@@ -19,7 +19,7 @@
         android:textSize="16sp"
         android:textStyle="bold" />
 
-    <RadioGroup xmlns:android="http://schemas.android.com/apk/res/android"
+    <RadioGroup
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:orientation="vertical">
@@ -52,6 +52,13 @@
             android:layout_margin="12dp"
             android:text="@string/bridges_get_new" />
 
+        <RadioButton
+            android:id="@+id/btnMoat"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_margin="12dp"
+            android:text="@string/bridges_get_new"/>
+
     </RadioGroup>
 
     <TextView
diff --git a/app/src/main/res/menu/moat.xml b/app/src/main/res/menu/moat.xml
new file mode 100644
index 00000000..f32bbe11
--- /dev/null
+++ b/app/src/main/res/menu/moat.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2008 Esmertec AG.
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+-->
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:yourapp="http://schemas.android.com/apk/res-auto"
+    >
+    <item android:id="@+id/menu_refresh"
+        android:title="@string/refresh_captcha"
+        android:icon="@drawable/ic_refresh_white_24dp"
+        yourapp:showAsAction="always"
+        />
+</menu>
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index bff412a2..cc60ddfd 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -124,7 +124,7 @@
     <string name="newnym">You\'ve switched to a new Tor identity!</string>
 
     <string name="pref_open_proxy_on_all_interfaces_title">Open Proxy on All Interfaces</string>
-  <string name="pref_open_proxy_on_all_interfaces_summary">Allow Wi-Fi peers, tethered devices and anyone else who can connect to your IP, to access Tor</string>
+    <string name="pref_open_proxy_on_all_interfaces_summary">Allow Wi-Fi peers, tethered devices and anyone else who can connect to your IP, to access Tor</string>
 
     <string name="no_network_connectivity_putting_tor_to_sleep_">No network connectivity. Putting Tor to sleep…</string>
     <string name="network_connectivity_is_good_waking_tor_up_">Network connectivity is good. Waking Tor up…</string>
@@ -259,4 +259,11 @@
     <string name="app_services">App services</string>
     <string name="default_socks_http">SOCKS: - HTTP: -</string>
     <string name="refresh_apps">Refresh Apps</string>
+
+    <!-- MoatActivity -->
+    <string name="request_bridges">Request Bridges</string>
+    <string name="refresh_captcha">Refresh CAPTCHA</string>
+    <string name="solve_captcha_instruction">Solve the CAPTCHA to request bridges.</string>
+    <string name="captcha">Captcha</string>
+    <string name="enter_characters_from_image">Enter characters from image</string>
 </resources>





More information about the tor-commits mailing list