commit e7cfe4651d6874f35579f724f22e83c78cec04af Author: Benjamin Erhart berhart@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>