[tor-commits] [tor-browser/tor-browser-60.5.1esr-8.5-1] Bug 28329 - Part 4. Add new Tor Bootstrapping and configuration screens

gk at torproject.org gk at torproject.org
Fri Mar 15 21:48:09 UTC 2019


commit 87d6c829225d271ba94fa21672b157085dbf1ad9
Author: Matthew Finkel <Matthew.Finkel at gmail.com>
Date:   Thu Mar 14 02:03:26 2019 +0000

    Bug 28329 - Part 4. Add new Tor Bootstrapping and configuration screens
---
 .../android/app/src/main/res/layout/gecko_app.xml  |   5 +
 .../preference_tor_network_bridge_summary.xml      |  25 +
 .../preference_tor_network_bridges_enabled.xml     |  85 ++
 ...eference_tor_network_bridges_enabled_switch.xml |  15 +
 .../preference_tor_network_provide_bridge.xml      |  89 ++
 .../preference_tor_network_select_bridge_type.xml  | 128 +++
 .../app/src/main/res/layout/tor_bootstrap.xml      |  86 ++
 .../layout/tor_bootstrap_animation_container.xml   |  20 +
 .../app/src/main/res/layout/tor_bootstrap_log.xml  |  37 +
 .../main/res/xml/preferences_tor_network_main.xml  |  15 +
 .../xml/preferences_tor_network_provide_bridge.xml |  27 +
 .../preferences_tor_network_select_bridge_type.xml |  17 +
 mobile/android/base/AndroidManifest.xml.in         |   5 +
 .../base/java/org/mozilla/gecko/BrowserApp.java    |  44 +-
 .../TorBootstrapAnimationContainer.java            |  82 ++
 .../gecko/torbootstrap/TorBootstrapLogPanel.java   |  54 ++
 .../gecko/torbootstrap/TorBootstrapLogger.java     |  17 +
 .../gecko/torbootstrap/TorBootstrapPager.java      | 160 ++++
 .../torbootstrap/TorBootstrapPagerConfig.java      |  87 ++
 .../gecko/torbootstrap/TorBootstrapPanel.java      | 428 ++++++++++
 .../gecko/torbootstrap/TorLogEventListener.java    | 128 +++
 .../mozilla/gecko/torbootstrap/TorPreferences.java | 950 +++++++++++++++++++++
 22 files changed, 2501 insertions(+), 3 deletions(-)

diff --git a/mobile/android/app/src/main/res/layout/gecko_app.xml b/mobile/android/app/src/main/res/layout/gecko_app.xml
index ca25841695a7..9a5d03f79409 100644
--- a/mobile/android/app/src/main/res/layout/gecko_app.xml
+++ b/mobile/android/app/src/main/res/layout/gecko_app.xml
@@ -68,6 +68,11 @@
                           android:layout_width="match_parent"
                           android:layout_height="match_parent"/>
 
+                <ViewStub android:id="@+id/tor_bootstrap_pager_stub"
+                          android:layout="@layout/tor_bootstrap_animation_container"
+                          android:layout_width="match_parent"
+                          android:layout_height="match_parent"/>
+
             </FrameLayout>
 
             <View android:id="@+id/doorhanger_overlay"
diff --git a/mobile/android/app/src/main/res/layout/preference_tor_network_bridge_summary.xml b/mobile/android/app/src/main/res/layout/preference_tor_network_bridge_summary.xml
new file mode 100644
index 000000000000..d99b3c9543b0
--- /dev/null
+++ b/mobile/android/app/src/main/res/layout/preference_tor_network_bridge_summary.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/.  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+              android:layout_width="match_parent"
+              android:layout_height="wrap_content"
+              android:minHeight="?android:attr/listPreferredItemHeight"
+              android:gravity="center_vertical" >
+    <TextView xmlns:android="http://schemas.android.com/apk/res/android"
+        android:id="@+id/tor_network_bridge_summary"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:paddingTop="30sp"
+        android:paddingBottom="30sp"
+        android:paddingLeft="20sp"
+        android:paddingRight="20sp"
+        android:textSize="16sp"
+        android:fontFamily="Roboto-Regular"
+        android:textColor="#DE000000"
+        android:lineSpacingMultiplier="1.43"
+        android:text="@string/pref_category_tor_bridge_summary" />
+</LinearLayout>
diff --git a/mobile/android/app/src/main/res/layout/preference_tor_network_bridges_enabled.xml b/mobile/android/app/src/main/res/layout/preference_tor_network_bridges_enabled.xml
new file mode 100644
index 000000000000..8d8e4f320ba7
--- /dev/null
+++ b/mobile/android/app/src/main/res/layout/preference_tor_network_bridges_enabled.xml
@@ -0,0 +1,85 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2006 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.
+-->
+<!-- Layout for a Preference in a PreferenceActivity. The
+     Preference is able to place a specific widget for its particular
+     type in the "widget_frame" layout.
+     This is a modified version of the default Android Preference layout,
+     See: https://android.googlesource.com/platform/frameworks/base/+/refs/heads/pie-release/core/res/res/layout/preference.xml
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:minHeight="?android:attr/listPreferredItemHeight"
+    android:gravity="center_vertical"
+    android:paddingEnd="?android:attr/scrollbarSize"
+    android:orientation="vertical"
+    android:background="?android:attr/selectableItemBackground" >
+    <TextView xmlns:android="http://schemas.android.com/apk/res/android"
+        android:id="@+id/tor_network_configuration_summary"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:paddingTop="30sp"
+        android:paddingBottom="30sp"
+        android:paddingLeft="20sp"
+        android:paddingRight="20sp"
+        android:textSize="16sp"
+        android:fontFamily="Roboto-Regular"
+        android:textColor="#DE000000"
+        android:lineSpacingMultiplier="1.43"
+        android:text="@string/pref_category_tor_network_summary" />
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:gravity="center_vertical" >
+        <ImageView
+            android:id="@+android:id/icon"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center"
+            />
+        <RelativeLayout
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="15dp"
+            android:layout_marginEnd="6dp"
+            android:layout_marginTop="6dp"
+            android:layout_marginBottom="12dp"
+            android:layout_weight="1">
+            <TextView android:id="@+android:id/title"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:singleLine="true"
+                android:fontFamily="Roboto-Regular"
+                android:textSize="20sp"
+                android:textColor="#DE000000"
+                android:ellipsize="marquee"
+                android:fadingEdge="horizontal" />
+            <TextView android:id="@+android:id/summary"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_below="@android:id/title"
+                android:layout_alignStart="@android:id/title"
+                android:fontFamily="Roboto-Regular"
+                android:textSize="16sp"
+                android:textColorLink="#8000FF"
+                android:clickable="true"
+                android:focusable="false"
+                android:maxLines="2" />
+        </RelativeLayout>
+        <!-- Preference should place its actual preference widget here. -->
+        <LinearLayout android:id="@+android:id/widget_frame"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:gravity="center_vertical" />
+    </LinearLayout>
+</LinearLayout>
diff --git a/mobile/android/app/src/main/res/layout/preference_tor_network_bridges_enabled_switch.xml b/mobile/android/app/src/main/res/layout/preference_tor_network_bridges_enabled_switch.xml
new file mode 100644
index 000000000000..3ab276f0916c
--- /dev/null
+++ b/mobile/android/app/src/main/res/layout/preference_tor_network_bridges_enabled_switch.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/.  -->
+
+<Switch xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+android:id/switch_widget"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:focusable="false"
+    android:clickable="true"
+    android:thumbTint="@color/tor_bridges_enabled_colors"
+    android:trackTint="@color/tor_bridges_enabled_colors"
+    android:background="@null" />
diff --git a/mobile/android/app/src/main/res/layout/preference_tor_network_provide_bridge.xml b/mobile/android/app/src/main/res/layout/preference_tor_network_provide_bridge.xml
new file mode 100644
index 000000000000..9e72b44ae734
--- /dev/null
+++ b/mobile/android/app/src/main/res/layout/preference_tor_network_provide_bridge.xml
@@ -0,0 +1,89 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/.  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+              android:layout_width="match_parent"
+              android:layout_height="wrap_content"
+              android:minHeight="?android:attr/listPreferredItemHeight"
+              android:orientation="vertical"
+              android:paddingEnd="?android:attr/scrollbarSize"
+              android:gravity="center_vertical"
+              android:background="?android:attr/selectableItemBackground" >
+    <LinearLayout
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:paddingLeft="16sp"
+        android:paddingRight="16sp"
+        android:orientation="vertical" >
+        <TextView android:id="@+android:id/title"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:fontFamily="Roboto-Regular"
+            android:textSize="20sp"
+            android:textColor="#DE000000"
+            android:singleLine="true"
+            android:ellipsize="marquee"
+            android:fadingEdge="horizontal" />
+        <TextView android:id="@+android:id/summary"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:paddingBottom="30sp"
+            android:fontFamily="Roboto-Regular"
+            android:textSize="16sp"
+            android:maxLines="4" />
+        <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+                      android:layout_width="match_parent"
+                      android:layout_height="wrap_content"
+                      android:minHeight="?android:attr/listPreferredItemHeight"
+                      android:gravity="center_vertical"
+                      android:orientation="vertical" >
+            <EditText
+                android:id="@+id/tor_network_provide_bridge1"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginBottom="10dp"
+                android:inputType="text"
+                android:textSize="20sp"
+                android:fontFamily="Roboto-Regular"
+                android:paddingTop="22sp"
+                android:paddingLeft="16sp"
+                android:paddingBottom="22sp"
+                android:background="#DCDCDC"
+                android:hint="@string/pref_tor_bridges_provide_manual_address_port_placeholder" />
+            <EditText
+                android:id="@+id/tor_network_provide_bridge2"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginBottom="10dp"
+                android:inputType="text"
+                android:textSize="20sp"
+                android:fontFamily="Roboto-Regular"
+                android:paddingTop="22sp"
+                android:paddingLeft="16sp"
+                android:paddingBottom="22sp"
+                android:background="#DCDCDC"
+                android:hint="@string/pref_tor_bridges_provide_manual_address_port_placeholder" />
+            <EditText
+                android:id="@+id/tor_network_provide_bridge3"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:inputType="text"
+                android:textSize="20sp"
+                android:fontFamily="Roboto-Regular"
+                android:paddingTop="22sp"
+                android:paddingLeft="16sp"
+                android:paddingBottom="22sp"
+                android:background="#DCDCDC"
+                android:hint="@string/pref_tor_bridges_provide_manual_address_port_placeholder" />
+        </LinearLayout>
+    </LinearLayout>
+    <LinearLayout android:id="@+android:id/widget_frame"
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        android:gravity="center_vertical"
+        android:orientation="vertical">
+    </LinearLayout>
+</LinearLayout>
diff --git a/mobile/android/app/src/main/res/layout/preference_tor_network_select_bridge_type.xml b/mobile/android/app/src/main/res/layout/preference_tor_network_select_bridge_type.xml
new file mode 100644
index 000000000000..2c1632bb8268
--- /dev/null
+++ b/mobile/android/app/src/main/res/layout/preference_tor_network_select_bridge_type.xml
@@ -0,0 +1,128 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/.  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+              android:layout_width="match_parent"
+              android:layout_height="wrap_content"
+              android:minHeight="?android:attr/listPreferredItemHeight"
+              android:orientation="vertical"
+              android:paddingEnd="?android:attr/scrollbarSize"
+              android:gravity="center_vertical"
+              android:background="?android:attr/selectableItemBackground" >
+
+    <!-- Include the Bridge description -->
+    <include layout="@layout/preference_tor_network_bridge_summary" />
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:paddingLeft="20sp"
+        android:orientation="vertical" >
+        <LinearLayout
+            android:id="@+id/title_and_summary"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="vertical" >
+            <TextView android:id="@+android:id/title"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:fontFamily="Roboto-Regular"
+                android:textSize="20sp"
+                android:textColor="#DE000000"
+                android:singleLine="true"
+                android:ellipsize="marquee"
+                android:fadingEdge="horizontal" />
+            <TextView android:id="@+android:id/summary"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginBottom="40dp"
+                android:fontFamily="Roboto-Regular"
+                android:textSize="16sp"
+                android:maxLines="4" />
+        </LinearLayout>
+        <include layout="@xml/separator" />
+        <RadioGroup
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="vertical"
+            android:paddingLeft="20sp"
+            android:visibility="gone"
+            android:layoutDirection="rtl"
+            android:id="@+id/pref_radio_group_builtin_bridges_type">
+            <RadioButton android:id="@+id/radio_pref_bridges_obfs4"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="10sp"
+                android:layout_marginBottom="10sp"
+                android:buttonTint="@color/tor_bridges_select_builtin"
+                android:fontFamily="Roboto-Regular"
+                android:textColor="#DE000000"
+                android:textSize="16sp"
+                android:text="@string/pref_bridges_type_obfs4"/>
+            <include layout="@xml/separator" />
+            <RadioButton android:id="@+id/radio_pref_bridges_meek_azure"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="10sp"
+                android:layout_marginBottom="10sp"
+                android:buttonTint="@color/tor_bridges_select_builtin"
+                android:fontFamily="Roboto-Regular"
+                android:textColor="#DE000000"
+                android:textSize="16sp"
+                android:text="@string/pref_bridges_type_meek_azure"/>
+            <include layout="@xml/separator" />
+            <RadioButton android:id="@+id/radio_pref_bridges_obfs3"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="10sp"
+                android:layout_marginBottom="10sp"
+                android:buttonTint="@color/tor_bridges_select_builtin"
+                android:fontFamily="Roboto-Regular"
+                android:textColor="#DE000000"
+                android:textSize="16sp"
+                android:text="@string/pref_bridges_type_obfs3"/>
+            <include layout="@xml/separator" />
+        </RadioGroup>
+    </LinearLayout>
+
+    <LinearLayout android:id="@+android:id/widget_frame"
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        android:gravity="center_vertical"
+        android:orientation="vertical">
+    </LinearLayout>
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:clickable="true"
+        android:paddingTop="20sp"
+        android:paddingLeft="20sp"
+        android:id="@+id/tor_network_provide_a_bridge"
+        android:orientation="vertical">
+        <TextView
+            android:id="@+id/tor_network_provide_a_bridge_title"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:fontFamily="Roboto-Regular"
+            android:textSize="20sp"
+            android:textColor="#DE000000"
+            android:text="@string/pref_tor_bridges_provide_manual_button_title"
+            android:singleLine="true"
+            android:ellipsize="marquee"
+            android:fadingEdge="horizontal" />
+        <TextView
+            android:id="@+id/tor_network_provide_a_bridge_summary"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginBottom="30dp"
+            android:fontFamily="Roboto-Regular"
+            android:textSize="16sp"
+            android:text="@string/pref_tor_bridges_provide_manual_summary"
+            android:maxLines="4" />
+        <include layout="@xml/separator" />
+    </LinearLayout>
+</LinearLayout>
diff --git a/mobile/android/app/src/main/res/layout/tor_bootstrap.xml b/mobile/android/app/src/main/res/layout/tor_bootstrap.xml
new file mode 100644
index 000000000000..ce2b1c910a44
--- /dev/null
+++ b/mobile/android/app/src/main/res/layout/tor_bootstrap.xml
@@ -0,0 +1,86 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/.  -->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+                xmlns:app="http://schemas.android.com/apk/res-auto"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:background="@color/tor_bootstrap_background">
+
+        <ImageView android:id="@+id/tor_bootstrap_settings_gear"
+                   app:srcCompat="@drawable/ic_settings_24px"
+                   android:tint="#ffffffff"
+                   android:layout_width="wrap_content"
+                   android:layout_height="wrap_content"
+                   android:layout_marginTop="25dp"
+                   android:layout_marginRight="20dp"
+                   android:layout_alignParentRight="true" />
+
+        <!-- These three elements are rendered in reverse order -->
+        <TextView android:id="@+id/tor_bootstrap_swipe_log"
+                  android:layout_width="match_parent"
+                  android:layout_height="wrap_content"
+                  android:width="301dp"
+                  android:height="24dp"
+                  android:layout_marginBottom="20dp"
+                  android:layout_centerHorizontal="true"
+                  android:layout_alignParentBottom="true"
+                  android:gravity="center"
+                  android:visibility="invisible"
+                  android:textSize="14sp"
+                  android:fontFamily="Roboto-Regular"
+                  android:textColor="#FFFFFFFF"
+                  android:lineSpacingMultiplier="1.71"
+                  android:text="@string/tor_bootstrap_swipe_for_logs"/>
+
+        <Button android:id="@+id/tor_bootstrap_connect"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginBottom="7dp"
+                android:width="144dp"
+                android:height="48dp"
+                android:textSize="14sp"
+                android:layout_above="@id/tor_bootstrap_swipe_log"
+                android:layout_centerHorizontal="true"
+                android:background="@drawable/rounded_corners"
+                android:fontFamily="Roboto-Medium"
+                android:textColor="@color/tor_bootstrap_background"
+                android:lineSpacingMultiplier="1.14"
+                android:text="@string/tor_bootstrap_connect" />
+
+        <TextView android:id="@+id/tor_bootstrap_last_status_message"
+                  android:layout_width="match_parent"
+                  android:layout_height="wrap_content"
+                  android:width="301dp"
+                  android:height="24dp"
+                  android:layout_marginBottom="40dp"
+                  android:layout_above="@id/tor_bootstrap_connect"
+                  android:layout_centerHorizontal="true"
+                  android:gravity="center"
+                  android:singleLine="true"
+                  android:textSize="14sp"
+                  android:fontFamily="RobotoMono-Regular"
+                  android:textColor="@android:color/white"
+                  android:lineSpacingMultiplier="2"
+                  android:visibility="invisible" />
+
+        <!-- Keep the src synchronized with TorBootstrapPanel::stopBootstrapping() -->
+        <ImageView android:id="@+id/tor_bootstrap_onion"
+                   app:srcCompat="@drawable/tor_spinning_onion"
+                   android:scaleType="fitCenter"
+                   android:tint="#ffffffff"
+                   android:layout_height="wrap_content"
+                   android:layout_width="match_parent"
+                   android:layout_marginTop="130dp"
+                   android:layout_marginBottom="37dp"
+                   android:layout_marginRight="95dp"
+                   android:layout_marginLeft="95dp"
+                   android:layout_centerHorizontal="true"
+                   android:layout_below="@id/tor_bootstrap_settings_gear"
+                   android:layout_above="@id/tor_bootstrap_last_status_message"
+                   android:paddingLeft="20dp"
+                   android:paddingRight="20dp"/>
+</RelativeLayout>
diff --git a/mobile/android/app/src/main/res/layout/tor_bootstrap_animation_container.xml b/mobile/android/app/src/main/res/layout/tor_bootstrap_animation_container.xml
new file mode 100644
index 000000000000..04dfeb0f3509
--- /dev/null
+++ b/mobile/android/app/src/main/res/layout/tor_bootstrap_animation_container.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/.  -->
+
+<org.mozilla.gecko.torbootstrap.TorBootstrapAnimationContainer xmlns:android="http://schemas.android.com/apk/res/android"
+             xmlns:gecko="http://schemas.android.com/apk/res-auto"
+             android:layout_height="match_parent"
+             android:layout_width="match_parent"
+             android:background="@color/tor_bootstrap_background">
+
+    <org.mozilla.gecko.torbootstrap.TorBootstrapPager
+                    android:id="@+id/tor_bootstrap_pager"
+                    android:layout_width="match_parent"
+                    android:layout_height="match_parent"
+                    android:background="@color/tor_bootstrap_background">
+
+    </org.mozilla.gecko.torbootstrap.TorBootstrapPager>
+</org.mozilla.gecko.torbootstrap.TorBootstrapAnimationContainer>
diff --git a/mobile/android/app/src/main/res/layout/tor_bootstrap_log.xml b/mobile/android/app/src/main/res/layout/tor_bootstrap_log.xml
new file mode 100644
index 000000000000..c2f02d658d50
--- /dev/null
+++ b/mobile/android/app/src/main/res/layout/tor_bootstrap_log.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/.  -->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+                xmlns:app="http://schemas.android.com/apk/res-auto"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:background="@color/tor_bootstrap_background">
+
+
+     <ImageView android:id="@+id/tor_bootstrap_settings_gear"
+                app:srcCompat="@drawable/ic_settings_24px"
+                android:tint="#ffffffff"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="25dp"
+                android:layout_marginRight="20dp"
+                android:layout_alignParentRight="true" />
+
+     <!-- Encapsulate the TextView within the ScrollView so the view is scrollable -->
+     <ScrollView android:layout_height="match_parent"
+                 android:layout_width="match_parent"
+                 android:layout_below="@id/tor_bootstrap_settings_gear" >
+         <TextView android:id="@+id/tor_bootstrap_last_status_message"
+                   android:layout_width="match_parent"
+                   android:layout_height="wrap_content"
+                   android:textColor="@android:color/white"
+                   android:fontFamily="RobotoMono-Regular"
+                   android:textSize="14sp"
+                   android:textIsSelectable="true"
+                   android:layout_marginLeft="20dp"
+                   android:layout_marginRight="20dp" />
+     </ScrollView>
+</RelativeLayout>
diff --git a/mobile/android/app/src/main/res/xml/preferences_tor_network_main.xml b/mobile/android/app/src/main/res/xml/preferences_tor_network_main.xml
new file mode 100644
index 000000000000..c397bd7c1fc9
--- /dev/null
+++ b/mobile/android/app/src/main/res/xml/preferences_tor_network_main.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
+                  xmlns:gecko="http://schemas.android.com/apk/res-auto"
+                  android:enabled="true">
+    <SwitchPreference android:key="android.not_a_preference.tor.bridges.enabled"
+                      android:title="@string/pref_choice_tor_bridges_enabled_title"
+                      android:summaryOff="@string/pref_choice_tor_bridges_enabled_summary"
+                      android:selectable="false"
+                      android:layout="@layout/preference_tor_network_bridges_enabled"
+                      android:widgetLayout="@layout/preference_tor_network_bridges_enabled_switch" />
+</PreferenceScreen>
diff --git a/mobile/android/app/src/main/res/xml/preferences_tor_network_provide_bridge.xml b/mobile/android/app/src/main/res/xml/preferences_tor_network_provide_bridge.xml
new file mode 100644
index 000000000000..e8346f4fec63
--- /dev/null
+++ b/mobile/android/app/src/main/res/xml/preferences_tor_network_provide_bridge.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
+                  xmlns:gecko="http://schemas.android.com/apk/res-auto"
+                  android:enabled="true">
+
+
+    <!-- Ideally, this preference would not be needed. We would move the
+        summary into the tor.bridges.provide preference. However, there is
+        a bug in the layout where typing in the text field isn't shown until
+        the user presses the back button. This only occurs when the EditText
+        View is under the first ViewGroup under the ListView. -->
+    <Preference
+            android:layout="@layout/preference_tor_network_bridge_summary"
+            android:selectable="false"
+            android:shouldDisableView="false"
+            android:enabled="false"/>
+
+    <Preference
+            android:key="android.not_a_preference.tor.bridges.provide"
+            android:layout="@layout/preference_tor_network_provide_bridge"
+            android:title="@string/pref_tor_bridges_provide_manual_text_title"
+            android:summary="@string/pref_tor_bridges_provide_manual_summary" />
+</PreferenceScreen>
diff --git a/mobile/android/app/src/main/res/xml/preferences_tor_network_select_bridge_type.xml b/mobile/android/app/src/main/res/xml/preferences_tor_network_select_bridge_type.xml
new file mode 100644
index 000000000000..0bcc18c38997
--- /dev/null
+++ b/mobile/android/app/src/main/res/xml/preferences_tor_network_select_bridge_type.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
+                  xmlns:gecko="http://schemas.android.com/apk/res-auto"
+                  android:enabled="true">
+
+    <Preference
+            android:key="android.not_a_preference.tor.bridges.type"
+            android:layout="@layout/preference_tor_network_select_bridge_type"
+            android:title="@string/pref_tor_bridges_provide_select_text_title"
+            android:summary="@string/pref_choice_tor_bridges_enabled_summary"
+            android:selectable="false"/>
+
+</PreferenceScreen>
diff --git a/mobile/android/base/AndroidManifest.xml.in b/mobile/android/base/AndroidManifest.xml.in
index 72b1e16925b7..01b23d22a19b 100644
--- a/mobile/android/base/AndroidManifest.xml.in
+++ b/mobile/android/base/AndroidManifest.xml.in
@@ -501,5 +501,10 @@
             android:stopWithTask="true">
         </service>
 
+        <activity android:name="org.mozilla.gecko.torbootstrap.TorPreferences"
+                  android:theme="@style/Gecko.Preferences"
+                  android:configChanges="orientation|screenSize|locale|layoutDirection"
+                  android:excludeFromRecents="true"/>
+
     </application>
 </manifest>
diff --git a/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java b/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
index 464b4054c9ff..f8af42f09b5f 100644
--- a/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
@@ -158,6 +158,7 @@ import org.mozilla.gecko.toolbar.AutocompleteHandler;
 import org.mozilla.gecko.toolbar.BrowserToolbar;
 import org.mozilla.gecko.toolbar.BrowserToolbar.TabEditingState;
 import org.mozilla.gecko.toolbar.PwaConfirm;
+import org.mozilla.gecko.torbootstrap.TorBootstrapAnimationContainer;
 import org.mozilla.gecko.trackingprotection.TrackingProtectionPrompt;
 import org.mozilla.gecko.updater.PostUpdateHandler;
 import org.mozilla.gecko.updater.UpdateServiceHelper;
@@ -266,6 +267,7 @@ public class BrowserApp extends GeckoApp
     private TabStripInterface mTabStrip;
     private AnimatedProgressBar mProgressView;
     private FirstrunAnimationContainer mFirstrunAnimationContainer;
+    private TorBootstrapAnimationContainer mTorBootstrapAnimationContainer;
     private HomeScreen mHomeScreen;
     private TabsPanel mTabsPanel;
 
@@ -486,7 +488,7 @@ public class BrowserApp extends GeckoApp
                     mVideoPlayer.stop();
                 }
 
-                if (Tabs.getInstance().isSelectedTab(tab) && mDynamicToolbar.isEnabled()) {
+                if (Tabs.getInstance().isSelectedTab(tab) && mDynamicToolbar.isEnabled() && !isTorBootstrapVisible()) {
                     final VisibilityTransition transition = (tab.getShouldShowToolbarWithoutAnimationOnFirstSelection()) ?
                             VisibilityTransition.IMMEDIATE : VisibilityTransition.ANIMATE;
                     mDynamicToolbar.setVisible(true, transition);
@@ -496,7 +498,7 @@ public class BrowserApp extends GeckoApp
                 }
                 // fall through
             case LOCATION_CHANGE:
-                if (Tabs.getInstance().isSelectedTab(tab)) {
+                if (Tabs.getInstance().isSelectedTab(tab) && !isTorBootstrapVisible()) {
                     updateHomePagerForTab(tab);
                 }
 
@@ -509,7 +511,7 @@ public class BrowserApp extends GeckoApp
                 if (Tabs.getInstance().isSelectedTab(tab)) {
                     invalidateOptionsMenu();
 
-                    if (mDynamicToolbar.isEnabled()) {
+                    if (mDynamicToolbar.isEnabled() && !isTorBootstrapVisible()) {
                         mDynamicToolbar.setVisible(true, VisibilityTransition.ANIMATE);
                     }
                 }
@@ -1359,6 +1361,10 @@ public class BrowserApp extends GeckoApp
         final SafeIntent intent = new SafeIntent(getIntent());
 
         if (!IntentUtils.getIsInAutomationFromEnvironment(intent)) {
+            if (mTorNeedsStart) {
+                showTorBootstrapPager();
+            }
+
             // We can't show the first run experience until Gecko has finished initialization (bug 1077583).
             checkFirstrun(this, intent);
         }
@@ -2756,6 +2762,11 @@ public class BrowserApp extends GeckoApp
                 && mHomeScreenContainer != null && mHomeScreenContainer.getVisibility() == View.VISIBLE);
     }
 
+    private boolean isTorBootstrapVisible() {
+        return (mTorBootstrapAnimationContainer != null && mTorBootstrapAnimationContainer.isVisible()
+                && mHomeScreenContainer != null && mHomeScreenContainer.getVisibility() == View.VISIBLE);
+    }
+
     /**
      * Enters editing mode with the current tab's URL. There might be no
      * tabs loaded by the time the user enters editing mode e.g. just after
@@ -3107,6 +3118,33 @@ public class BrowserApp extends GeckoApp
         }
     }
 
+    private void showTorBootstrapPager() {
+
+        if (mTorBootstrapAnimationContainer == null) {
+            // We can't use toggleToolbarChrome() because that uses INVISIBLE, but we need GONE
+            mBrowserChrome.setVisibility(View.GONE);
+            final ViewStub torBootstrapPagerStub = (ViewStub) findViewById(R.id.tor_bootstrap_pager_stub);
+            mTorBootstrapAnimationContainer = (TorBootstrapAnimationContainer) torBootstrapPagerStub.inflate();
+            mTorBootstrapAnimationContainer.load(this, getSupportFragmentManager());
+            mTorBootstrapAnimationContainer.registerOnFinishListener(new TorBootstrapAnimationContainer.OnFinishListener() {
+                @Override
+                public void onFinish() {
+                    // Show the chrome again
+                    toggleToolbarChrome(true);
+                    // When the content loaded in the background (such as about:tor),
+                    // it was loaded while mBrowserChrome was GONE. We should refresh the
+                    // height now so the page is rendered correctly.
+                    Tabs.getInstance().getSelectedTab().doReload(false);
+
+                    // If we finished, then Tor bootstrapped 100%
+                    mTorNeedsStart = false;
+                }
+            });
+        }
+
+        mHomeScreenContainer.setVisibility(View.VISIBLE);
+    }
+
     private void showFirstrunPager() {
 
         if (mFirstrunAnimationContainer == null) {
diff --git a/mobile/android/base/java/org/mozilla/gecko/torbootstrap/TorBootstrapAnimationContainer.java b/mobile/android/base/java/org/mozilla/gecko/torbootstrap/TorBootstrapAnimationContainer.java
new file mode 100644
index 000000000000..188e03df0092
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/torbootstrap/TorBootstrapAnimationContainer.java
@@ -0,0 +1,82 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.torbootstrap;
+
+import android.app.Activity;
+import android.content.Context;
+import android.support.v4.app.FragmentManager;
+import android.util.AttributeSet;
+
+import android.view.View;
+import android.widget.LinearLayout;
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.firstrun.FirstrunAnimationContainer;
+
+/**
+ * A container for the bootstrapping flow.
+ *
+ * Mostly a modified version of FirstrunAnimationContainer
+ */
+public class TorBootstrapAnimationContainer extends FirstrunAnimationContainer {
+
+    public static interface OnFinishListener {
+        public void onFinish();
+    }
+
+    private TorBootstrapPager pager;
+    private boolean visible;
+
+    // Provides a callback so BrowserApp can execute an action
+    // when the bootstrapping is complete and the bootstrapping
+    // screen closes.
+    private OnFinishListener onFinishListener;
+
+    public TorBootstrapAnimationContainer(Context context) {
+        this(context, null);
+    }
+    public TorBootstrapAnimationContainer(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public void load(Activity activity, FragmentManager fm) {
+        visible = true;
+        pager = findViewById(R.id.tor_bootstrap_pager);
+        pager.load(activity, fm, new OnFinishListener() {
+            @Override
+            public void onFinish() {
+                hide();
+            }
+        });
+    }
+
+    public void hide() {
+        visible = false;
+        if (onFinishListener != null) {
+            onFinishListener.onFinish();
+        }
+        animateHide();
+    }
+
+    private void animateHide() {
+        final Animator alphaAnimator = ObjectAnimator.ofFloat(this, "alpha", 0);
+        alphaAnimator.setDuration(150);
+        alphaAnimator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                TorBootstrapAnimationContainer.this.setVisibility(View.GONE);
+            }
+        });
+
+        alphaAnimator.start();
+    }
+
+    public void registerOnFinishListener(OnFinishListener listener) {
+        onFinishListener = listener;
+    }
+}
diff --git a/mobile/android/base/java/org/mozilla/gecko/torbootstrap/TorBootstrapLogPanel.java b/mobile/android/base/java/org/mozilla/gecko/torbootstrap/TorBootstrapLogPanel.java
new file mode 100644
index 000000000000..18d827cec216
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/torbootstrap/TorBootstrapLogPanel.java
@@ -0,0 +1,54 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.torbootstrap;
+
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+import org.mozilla.gecko.R;
+
+/**
+ * Simple subclass of TorBootstrapPanel specifically for showing
+ * Tor and Orbot log entries.
+ */
+public class TorBootstrapLogPanel extends TorBootstrapPanel {
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstance) {
+        mRoot = (ViewGroup) inflater.inflate(R.layout.tor_bootstrap_log, container, false);
+
+        if (mRoot == null) {
+            Log.w(LOGTAG, "Inflating R.layout.tor_bootstrap returned null");
+            return null;
+        }
+
+        TorLogEventListener.addLogger(this);
+
+        return mRoot;
+    }
+
+    @Override
+    public void onViewCreated(View view, Bundle savedInstance) {
+        super.onViewCreated(view, savedInstance);
+        // Inherited from the super class
+        configureGearCogClickHandler();
+    }
+
+    // TODO Add a button for Go-to-bottom
+    @Override
+    public void updateStatus(String torServiceMsg, String newTorStatus) {
+        if (torServiceMsg == null) {
+            return;
+        }
+        TextView torLog = (TextView) mRoot.findViewById(R.id.tor_bootstrap_last_status_message);
+        torLog.append("- " + torServiceMsg + "\n");
+    }
+}
diff --git a/mobile/android/base/java/org/mozilla/gecko/torbootstrap/TorBootstrapLogger.java b/mobile/android/base/java/org/mozilla/gecko/torbootstrap/TorBootstrapLogger.java
new file mode 100644
index 000000000000..24c9321beb63
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/torbootstrap/TorBootstrapLogger.java
@@ -0,0 +1,17 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.torbootstrap;
+
+import android.app.Activity;
+
+// Simple interface for a logger.
+//
+// The current implementers are TorBootstrapPanel and
+// TorBootstrapLogPanel.
+public interface TorBootstrapLogger {
+    public void updateStatus(String torServiceMsg, String newTorStatus);
+    public Activity getActivity();
+}
diff --git a/mobile/android/base/java/org/mozilla/gecko/torbootstrap/TorBootstrapPager.java b/mobile/android/base/java/org/mozilla/gecko/torbootstrap/TorBootstrapPager.java
new file mode 100644
index 000000000000..b780810f14ab
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/torbootstrap/TorBootstrapPager.java
@@ -0,0 +1,160 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.torbootstrap;
+
+import android.app.Activity;
+import android.content.Context;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentManager;
+import android.support.v4.app.FragmentPagerAdapter;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.animation.Animator;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+
+import org.mozilla.gecko.firstrun.FirstrunPager;
+
+import java.util.List;
+
+/**
+ * ViewPager containing our bootstrapping pages.
+ *
+ * Based on FirstrunPager for simplicity
+ */
+public class TorBootstrapPager extends FirstrunPager {
+
+    private Context context;
+    private Activity mActivity;
+    protected TorBootstrapPanel.PagerNavigation pagerNavigation;
+
+    public TorBootstrapPager(Context context) {
+        this(context, null);
+    }
+
+    public TorBootstrapPager(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        this.context = context;
+    }
+
+    @Override
+    public void addView(View child, int index, ViewGroup.LayoutParams params) {
+        super.addView(child, index, params);
+    }
+
+    // Load the default (hard-coded) panels from TorBootstrapPagerConfig
+    // Mostly copied from super
+    public void load(Activity activity, FragmentManager fm, final TorBootstrapAnimationContainer.OnFinishListener onFinishListener) {
+        mActivity = activity;
+        final List<TorBootstrapPagerConfig.TorBootstrapPanelConfig> panels = TorBootstrapPagerConfig.getDefaultBootstrapPanel();
+
+        setAdapter(new ViewPagerAdapter(fm, panels));
+        this.pagerNavigation = new TorBootstrapPanel.PagerNavigation() {
+            @Override
+            public void next() {
+                // No-op implementation.
+            }
+
+            @Override
+            public void finish() {
+                if (onFinishListener != null) {
+                    onFinishListener.onFinish();
+                }
+            }
+        };
+
+        animateLoad();
+    }
+
+    // Copied from super
+    private void animateLoad() {
+        setTranslationY(500);
+        setAlpha(0);
+
+        final Animator translateAnimator = ObjectAnimator.ofFloat(this, "translationY", 0);
+        translateAnimator.setDuration(400);
+
+        final Animator alphaAnimator = ObjectAnimator.ofFloat(this, "alpha", 1);
+        alphaAnimator.setStartDelay(200);
+        alphaAnimator.setDuration(600);
+
+        final AnimatorSet set = new AnimatorSet();
+        set.playTogether(alphaAnimator, translateAnimator);
+        set.setStartDelay(400);
+
+        set.start();
+    }
+
+    // Provide an interface for inter-panel communication allowing
+    // the logging panel to stop the bootstrapping animation on the
+    // main panel.
+    public interface TorBootstrapController {
+        void startBootstrapping();
+        void stopBootstrapping();
+    }
+
+    // Mostly copied from FirstrunPager
+    protected class ViewPagerAdapter extends FragmentPagerAdapter implements TorBootstrapController {
+        private final List<TorBootstrapPagerConfig.TorBootstrapPanelConfig> panels;
+        private final Fragment[] fragments;
+
+        public ViewPagerAdapter(FragmentManager fm, List<TorBootstrapPagerConfig.TorBootstrapPanelConfig> panels) {
+            super(fm);
+            this.panels = panels;
+            this.fragments = new Fragment[panels.size()];
+        }
+
+        @Override
+        public Fragment getItem(int i) {
+            Fragment fragment = fragments[i];
+            if (fragment == null) {
+                TorBootstrapPagerConfig.TorBootstrapPanelConfig panelConfig = panels.get(i);
+                // We know the class is within the "org.mozilla.gecko.torbootstrap" package namespace
+                fragment = Fragment.instantiate(mActivity.getApplicationContext(), panelConfig.getClassname(), panelConfig.getArgs());
+                ((TorBootstrapPanel) fragment).setPagerNavigation(pagerNavigation);
+                ((TorBootstrapPanel) fragment).setContext(mActivity);
+                ((TorBootstrapPanel) fragment).setBootstrapController(this);
+                fragments[i] = fragment;
+            }
+            return fragment;
+        }
+
+        @Override
+        public int getCount() {
+            return panels.size();
+        }
+
+        @Override
+        public CharSequence getPageTitle(int i) {
+            return context.getString(panels.get(i).getTitleRes()).toUpperCase();
+        }
+
+        public void startBootstrapping() {
+            if (fragments.length == 0) {
+                return;
+            }
+
+            TorBootstrapPanel mainPanel = (TorBootstrapPanel) getItem(0);
+            if (mainPanel == null) {
+                return;
+            }
+            mainPanel.startBootstrapping();
+        }
+
+        public void stopBootstrapping() {
+            if (fragments.length == 0) {
+                return;
+            }
+
+            TorBootstrapPanel mainPanel = (TorBootstrapPanel) getItem(0);
+            if (mainPanel == null) {
+                return;
+            }
+            mainPanel.stopBootstrapping();
+        }
+    }
+}
diff --git a/mobile/android/base/java/org/mozilla/gecko/torbootstrap/TorBootstrapPagerConfig.java b/mobile/android/base/java/org/mozilla/gecko/torbootstrap/TorBootstrapPagerConfig.java
new file mode 100644
index 000000000000..7eb5f77fe8ca
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/torbootstrap/TorBootstrapPagerConfig.java
@@ -0,0 +1,87 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.torbootstrap;
+
+import android.os.Bundle;
+import android.util.Log;
+import org.mozilla.gecko.GeckoSharedPrefs;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.Telemetry;
+import org.mozilla.gecko.TelemetryContract;
+import org.mozilla.gecko.Experiments;
+
+import java.util.LinkedList;
+import java.util.List;
+
+public class TorBootstrapPagerConfig {
+    public static final String LOGTAG = "TorBootstrapPagerConfig";
+
+    public static final String KEY_IMAGE = "imageRes";
+    public static final String KEY_TEXT = "textRes";
+    public static final String KEY_SUBTEXT = "subtextRes";
+    public static final String KEY_CTATEXT = "ctatextRes";
+
+    public static List<TorBootstrapPanelConfig> getDefaultConnectPanel() {
+         final List<TorBootstrapPanelConfig> panels = new LinkedList<>();
+         panels.add(SimplePanelConfigs.connectPanelConfig);
+
+         return panels;
+    }
+
+    public static List<TorBootstrapPanelConfig> getDefaultBootstrapPanel() {
+         final List<TorBootstrapPanelConfig> panels = new LinkedList<>();
+         panels.add(SimplePanelConfigs.bootstrapPanelConfig);
+         panels.add(SimplePanelConfigs.torLogPanelConfig);
+
+         return panels;
+    }
+
+    public static class TorBootstrapPanelConfig {
+
+        private String classname;
+        private int titleRes;
+        private Bundle args;
+
+        public TorBootstrapPanelConfig(String resource, int titleRes) {
+            this(resource, titleRes, -1, -1, -1, true);
+        }
+
+        public TorBootstrapPanelConfig(String classname, int titleRes, int imageRes, int textRes, int subtextRes) {
+            this(classname, titleRes, imageRes, textRes, subtextRes, false);
+        }
+
+        private TorBootstrapPanelConfig(String classname, int titleRes, int imageRes, int textRes, int subtextRes, boolean isCustom) {
+            this.classname = classname;
+            this.titleRes = titleRes;
+
+            if (!isCustom) {
+                this.args = new Bundle();
+                this.args.putInt(KEY_IMAGE, imageRes);
+                this.args.putInt(KEY_TEXT, textRes);
+                this.args.putInt(KEY_SUBTEXT, subtextRes);
+            }
+        }
+
+        public String getClassname() {
+            return this.classname;
+        }
+
+        public int getTitleRes() {
+            return this.titleRes;
+        }
+
+        public Bundle getArgs() {
+            return args;
+        }
+    }
+
+    private static class SimplePanelConfigs {
+        public static final TorBootstrapPanelConfig connectPanelConfig = new TorBootstrapPanelConfig(TorBootstrapPanel.class.getName(), R.string.firstrun_panel_title_welcome);
+        public static final TorBootstrapPanelConfig bootstrapPanelConfig = new TorBootstrapPanelConfig(TorBootstrapPanel.class.getName(), R.string.firstrun_panel_title_welcome);
+        public static final TorBootstrapPanelConfig torLogPanelConfig = new TorBootstrapPanelConfig(TorBootstrapLogPanel.class.getName(), R.string.firstrun_panel_title_privacy);
+
+    }
+}
diff --git a/mobile/android/base/java/org/mozilla/gecko/torbootstrap/TorBootstrapPanel.java b/mobile/android/base/java/org/mozilla/gecko/torbootstrap/TorBootstrapPanel.java
new file mode 100644
index 000000000000..584c0fc3cdde
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/torbootstrap/TorBootstrapPanel.java
@@ -0,0 +1,428 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.torbootstrap;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.graphics.drawable.Animatable2;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.support.v4.content.LocalBroadcastManager;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.TextView;
+import android.util.Log;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.Telemetry;
+import org.mozilla.gecko.TelemetryContract;
+import org.mozilla.gecko.firstrun.FirstrunPanel;
+
+import org.torproject.android.service.OrbotConstants;
+import org.torproject.android.service.TorService;
+import org.torproject.android.service.TorServiceConstants;
+import org.torproject.android.service.util.TorServiceUtils;
+
+
+/**
+ * Tor Bootstrap panel (fragment/screen)
+ *
+ * This is based on the Firstrun Panel for simplicity.
+ */
+public class TorBootstrapPanel extends FirstrunPanel implements TorBootstrapLogger {
+
+    protected static final String LOGTAG = "TorBootstrap";
+
+    protected ViewGroup mRoot;
+    protected Activity mActContext;
+    protected TorBootstrapPager.TorBootstrapController mBootstrapController;
+
+    // These are used by the background AlphaChanging thread for dynamically changing
+    // the alpha value of the Onion during bootstrap.
+    private int mOnionCurrentAlpha = 255;
+    // This is either +1 or -1, depending on the direction of the change.
+    private int mOnionCurrentAlphaDirection = -1;
+    private Object mOnionAlphaChangerLock = new Object();
+    private boolean mOnionAlphaChangerRunning = false;
+
+    // Runnable for changing the alpha of the Onion image every 100 milliseconds.
+    // It gradually increases and then decreases the alpha in the background and
+    // then applies the new alpha on the UI thread.
+    private Thread mChangeOnionAlphaThread = null;
+    final private class ChangeOnionAlphaRunnable implements Runnable {
+        @Override
+        public void run() {
+            while (true) {
+                 synchronized(mOnionAlphaChangerLock) {
+                     if (!mOnionAlphaChangerRunning) {
+                         // Null the reference for this thread when we exit
+                         mChangeOnionAlphaThread = null;
+                         return;
+                     }
+                 }
+
+                 // Choose the new value here, mOnionCurrentAlpha is set in setOnionAlphaValue()
+                 // Increase by 5 if mOnionCurrentAlphaDirection is positive, and decrease by
+                 // 5 if mOnionCurrentAlphaDirection is negative.
+                 final int newAlpha = mOnionCurrentAlpha + mOnionCurrentAlphaDirection*5;
+                 getActivity().runOnUiThread(new Runnable() {
+                      public void run() {
+                          setOnionAlphaValue(newAlpha);
+                      }
+                 });
+
+                 try {
+                     Thread.sleep(100);
+                 } catch (InterruptedException e) {}
+            }
+        }
+    }
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstance) {
+        mRoot = (ViewGroup) inflater.inflate(R.layout.tor_bootstrap, container, false);
+        if (mRoot == null) {
+            Log.w(LOGTAG, "Inflating R.layout.tor_bootstrap returned null");
+            return null;
+        }
+
+        Button connectButton = mRoot.findViewById(R.id.tor_bootstrap_connect);
+        if (connectButton == null) {
+            Log.w(LOGTAG, "Finding the Connect button failed. Did the ID change?");
+            return null;
+        }
+
+        connectButton.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                startBootstrapping();
+            }
+        });
+
+        // This should be declared in the xml layout, however there is a bug
+        // preventing this (the XML attribute isn't actually defined in the
+        // SDK).
+        // https://issuetracker.google.com/issues/37036728
+        connectButton.setClipToOutline(true);
+
+        configureGearCogClickHandler();
+
+        TorLogEventListener.addLogger(this);
+
+        return mRoot;
+    }
+
+    private void setOnionAlphaValue(int newAlpha) {
+        ImageView onionImg = (ImageView) mRoot.findViewById(R.id.tor_bootstrap_onion);
+        if (onionImg == null) {
+            return;
+        }
+
+        if (newAlpha > 255) {
+            // Cap this at 255 and change direction of animation
+            newAlpha = 255;
+
+            synchronized(mOnionAlphaChangerLock) {
+                mOnionCurrentAlphaDirection = -1;
+            }
+        } else if (newAlpha < 0) {
+            // Lower-bound this at 0 and change direction of animation
+            newAlpha = 0;
+
+            synchronized(mOnionAlphaChangerLock) {
+                mOnionCurrentAlphaDirection = 1;
+            }
+        }
+        onionImg.setImageAlpha(newAlpha);
+        mOnionCurrentAlpha = newAlpha;
+    }
+
+    public void updateStatus(String torServiceMsg, String newTorStatus) {
+        final String noticePrefix = "NOTICE: ";
+
+        if (torServiceMsg == null) {
+            return;
+        }
+
+        TextView torLog = (TextView) mRoot.findViewById(R.id.tor_bootstrap_last_status_message);
+        if (torLog == null) {
+            Log.w(LOGTAG, "updateStatus: torLog is null?");
+        }
+        // Only show Notice-level log messages on this panel
+        if (torServiceMsg.startsWith(noticePrefix)) {
+            // Drop the prefix
+            String msg = torServiceMsg.substring(noticePrefix.length());
+            torLog.setText(msg);
+        } else if (torServiceMsg.toLowerCase().contains("error")) {
+            torLog.setText(R.string.tor_notify_user_about_error);
+
+            // This may be a false-positive, but if we encountered an error within
+            // the OrbotService then there's likely nothing the user can do. This
+            // isn't persistent, so if they restart the app the button will be
+            // visible again.
+            Button connectButton = mRoot.findViewById(R.id.tor_bootstrap_connect);
+            if (connectButton == null) {
+                Log.w(LOGTAG, "updateStatus: Finding the Connect button failed. Did the ID change?");
+            } else {
+                TextView swipeLeftLog = (TextView) mRoot.findViewById(R.id.tor_bootstrap_swipe_log);
+                if (swipeLeftLog == null) {
+                    Log.w(LOGTAG, "updateStatus: swipeLeftLog is null?");
+                }
+
+                // Abuse this by showing the log message despite not bootstrapping
+                toggleVisibleElements(true, torLog, connectButton, swipeLeftLog);
+            }
+        }
+
+        // Return to the browser when we reach 100% bootstrapped
+        if (torServiceMsg.contains(TorServiceConstants.TOR_CONTROL_PORT_MSG_BOOTSTRAP_DONE)) {
+            // Inform the background AlphaChanging thread it should terminate
+            synchronized(mOnionAlphaChangerLock) {
+                mOnionAlphaChangerRunning = false;
+            }
+            close();
+        }
+    }
+
+    public void setContext(Activity ctx) {
+        mActContext = ctx;
+    }
+
+    // Save the TorBootstrapController.
+    // This method won't be used by the main TorBootstrapPanel (|this|), but
+    // it will be used by its childen.
+    public void setBootstrapController(TorBootstrapPager.TorBootstrapController bootstrapController) {
+        mBootstrapController = bootstrapController;
+    }
+
+    private void startTorService() {
+        Intent torService = new Intent(getActivity(), TorService.class);
+        torService.setAction(TorServiceConstants.ACTION_START);
+        getActivity().startService(torService);
+    }
+
+    private void stopTorService() {
+        // First, stop the current bootstrapping process (if it's in progress)
+        // TODO Ideally, we'd DisableNetwork here, but that's not available.
+        Intent torService = new Intent(getActivity(), TorService.class);
+        getActivity().stopService(torService);
+    }
+
+    // Setup OnClick handler for the settings gear/cog
+    protected void configureGearCogClickHandler() {
+        if (mRoot == null) {
+            Log.w(LOGTAG, "configureGearCogClickHandler: mRoot is null?");
+            return;
+        }
+
+        final ImageView gearSettingsImage = mRoot.findViewById(R.id.tor_bootstrap_settings_gear);
+        if (gearSettingsImage == null) {
+            Log.w(LOGTAG, "configureGearCogClickHandler: gearSettingsImage is null?");
+            return;
+        }
+
+        gearSettingsImage.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                // The existance of the connect button is an indicator of the user
+                // interacting with the main bootstrapping screen or the loggin screen.
+                Button connectButton = mRoot.findViewById(R.id.tor_bootstrap_connect);
+                if (connectButton == null) {
+                    Log.w(LOGTAG, "gearSettingsImage onClick: Finding the Connect button failed, proxying request.");
+
+                    // If there isn't a connect button on this screen, then proxy the
+                    // stopBootstrapping() request via the TorBootstrapController (which
+                    // is the underlying PagerAdapter).
+                    mBootstrapController.stopBootstrapping();
+                } else {
+                    stopBootstrapping();
+                }
+                // Open Tor Network Settings preferences screen
+                Intent intent = new Intent(mActContext, TorPreferences.class);
+                mActContext.startActivity(intent);
+            }
+        });
+    }
+
+    private void toggleVisibleElements(boolean bootstrapping, TextView lastStatus, Button connect, TextView swipeLeft) {
+        final int connectVisible = bootstrapping ? View.INVISIBLE : View.VISIBLE;
+        final int infoTextVisible = bootstrapping ? View.VISIBLE : View.INVISIBLE;
+
+        if (connect != null) {
+            connect.setVisibility(connectVisible);
+        }
+        if (lastStatus != null) {
+            lastStatus.setVisibility(infoTextVisible);
+        }
+        if (swipeLeft != null) {
+            swipeLeft.setVisibility(infoTextVisible);
+        }
+    }
+
+    private void startBackgroundAlphaChangingThread() {
+        // If it is non-null, then this is a bug because the thread should null this reference when
+        // it terminates.
+        if (mChangeOnionAlphaThread != null) {
+            if (mChangeOnionAlphaThread.getState() == Thread.State.TERMINATED) {
+                // The thread likely terminated unexpectedly, null the reference.
+                // The thread should set this itself.
+                Log.i(LOGTAG, "mChangeOnionAlphaThread.getState(): is terminated");
+                mChangeOnionAlphaThread = null;
+            } else {
+                // Don't null the reference in this case because then we'll start another
+                // background thread. We are currently in an unknown state, simply set
+                // the Running flag as false.
+                Log.w(LOGTAG, "We're in an unexpected state. mChangeOnionAlphaThread.getState(): " + mChangeOnionAlphaThread.getState());
+
+                synchronized(mOnionAlphaChangerLock) {
+                    mOnionAlphaChangerRunning = false;
+                }
+            }
+        }
+
+        // If the background thread is not currently running, then start it.
+        if (mChangeOnionAlphaThread == null) {
+            mChangeOnionAlphaThread = new Thread(new ChangeOnionAlphaRunnable());
+            if (mChangeOnionAlphaThread == null) {
+                Log.w(LOGTAG, "Instantiating a new ChangeOnionAlphaRunnable Thread failed.");
+            } else if (mChangeOnionAlphaThread.getState() == Thread.State.NEW) {
+                Log.i(LOGTAG, "Starting mChangeOnionAlphaThread");
+
+                // Synchronization across threads should not be necessary because there
+                // shouldn't be any other threads relying on mOnionAlphaChangerRunning.
+                // We do this purely for safety.
+                synchronized(mOnionAlphaChangerLock) {
+                    mOnionAlphaChangerRunning = true;
+                }
+
+                mChangeOnionAlphaThread.start();
+            }
+        }
+    }
+
+    public void startBootstrapping() {
+        if (mRoot == null) {
+            Log.w(LOGTAG, "startBootstrapping: mRoot is null?");
+            return;
+        }
+        // We're starting bootstrap, transition into the bootstrapping-tor-panel
+        Button connectButton = mRoot.findViewById(R.id.tor_bootstrap_connect);
+        if (connectButton == null) {
+            Log.w(LOGTAG, "startBootstrapping: connectButton is null?");
+            return;
+        }
+
+        ImageView onionImg = (ImageView) mRoot.findViewById(R.id.tor_bootstrap_onion);
+
+        // Replace the current non-animated image with the animation
+        onionImg.setImageResource(R.drawable.tor_spinning_onion);
+
+        Drawable drawableOnion = onionImg.getDrawable();
+        if (Build.VERSION.SDK_INT >= 23 && drawableOnion instanceof Animatable2) {
+            Animatable2 spinningOnion = (Animatable2) drawableOnion;
+            // Begin spinning
+            spinningOnion.start();
+        } else {
+            Log.i(LOGTAG, "Animatable2 is not supported (or bad inheritance), version: " + Build.VERSION.SDK_INT);
+        }
+
+        mOnionCurrentAlpha = 255;
+        // The onion should have 100% alpha, begin decreasing it.
+        mOnionCurrentAlphaDirection = -1;
+        startBackgroundAlphaChangingThread();
+
+        TextView torStatus = (TextView) mRoot.findViewById(R.id.tor_bootstrap_last_status_message);
+        if (torStatus == null) {
+            Log.w(LOGTAG, "startBootstrapping: torStatus is null?");
+            return;
+        }
+
+        TextView swipeLeftLog = (TextView) mRoot.findViewById(R.id.tor_bootstrap_swipe_log);
+        if (swipeLeftLog == null) {
+            Log.w(LOGTAG, "startBootstrapping: swipeLeftLog is null?");
+            return;
+        }
+
+        torStatus.setText(getString(R.string.tor_bootstrap_starting_status));
+
+        toggleVisibleElements(true, torStatus, connectButton, swipeLeftLog);
+        startTorService();
+    }
+
+    // This is public because this Pager may call this method if another Panel requests it.
+    public void stopBootstrapping() {
+        if (mRoot == null) {
+            Log.w(LOGTAG, "stopBootstrapping: mRoot is null?");
+            return;
+        }
+        // Transition from the animated bootstrapping panel to
+        // the static "Connect" panel
+        Button connectButton = mRoot.findViewById(R.id.tor_bootstrap_connect);
+        if (connectButton == null) {
+            Log.w(LOGTAG, "stopBootstrapping: connectButton is null?");
+            return;
+        }
+
+        ImageView onionImg = (ImageView) mRoot.findViewById(R.id.tor_bootstrap_onion);
+        if (onionImg == null) {
+            Log.w(LOGTAG, "stopBootstrapping: onionImg is null?");
+            return;
+        }
+
+        // Inform the background AlphaChanging thread it should terminate.
+        synchronized(mOnionAlphaChangerLock) {
+            mOnionAlphaChangerRunning = false;
+        }
+
+        Drawable drawableOnion = onionImg.getDrawable();
+
+        // If the connect button wasn't pressed previously, then this object is
+        // not an animation (it is most likely a BitmapDrawable). Only manipulate
+        // it when it is an Animatable2.
+        if (Build.VERSION.SDK_INT >= 23 && drawableOnion instanceof Animatable2) {
+            Animatable2 spinningOnion = (Animatable2) drawableOnion;
+            // spinningOnion is null if we didn't previously call startBootstrapping.
+            // If we reach here and spinningOnion is null, then there is likely a bug
+            // because stopBootstrapping() is called only when the user selects the
+            // gear button and we should only reach this block if the user pressed the
+            // connect button (thus creating and enabling the animation) and then
+            // pressing the gear button. Therefore, if the drawableOnion is an
+            // Animatable2, then spinningOnion should be non-null.
+            if (spinningOnion != null) {
+                spinningOnion.stop();
+
+                onionImg.setImageResource(R.drawable.tor_spinning_onion);
+            }
+        } else {
+            Log.i(LOGTAG, "Animatable2 is not supported (or bad inheritance), version: " + Build.VERSION.SDK_INT);
+        }
+
+        // Reset the onion's alpha value.
+        onionImg.setImageAlpha(255);
+
+        TextView torStatus = (TextView) mRoot.findViewById(R.id.tor_bootstrap_last_status_message);
+        if (torStatus == null) {
+            Log.w(LOGTAG, "stopBootstrapping: torStatus is null?");
+            return;
+        }
+
+        TextView swipeLeftLog = (TextView) mRoot.findViewById(R.id.tor_bootstrap_swipe_log);
+        if (swipeLeftLog == null) {
+            Log.w(LOGTAG, "stopBootstrapping: swipeLeftLog is null?");
+            return;
+        }
+
+        // Reset the displayed message
+        torStatus.setText("");
+
+        toggleVisibleElements(false, torStatus, connectButton, swipeLeftLog);
+        stopTorService();
+    }
+}
diff --git a/mobile/android/base/java/org/mozilla/gecko/torbootstrap/TorLogEventListener.java b/mobile/android/base/java/org/mozilla/gecko/torbootstrap/TorLogEventListener.java
new file mode 100644
index 000000000000..6218763475e5
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/torbootstrap/TorLogEventListener.java
@@ -0,0 +1,128 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.torbootstrap;
+
+import android.app.Activity;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Handler;
+import android.os.Message;
+import android.support.v4.content.LocalBroadcastManager;
+
+import org.torproject.android.service.OrbotConstants;
+import org.torproject.android.service.TorService;
+import org.torproject.android.service.TorServiceConstants;
+import org.torproject.android.service.util.TorServiceUtils;
+
+import java.util.Vector;
+
+
+/**
+ * This is simply a container for capturing the log events and proxying them
+ * to the TorBootstrapLogger implementers (TorBootstrapPanel and TorBootstrapLogPanel now).
+ *
+ * This should be in BrowserApp, but that class/Activity is already too large,
+ * so this should be easier to reason about.
+ */
+public class TorLogEventListener {
+
+    private static Vector<TorBootstrapLogger> mLoggers;
+
+    private TorLogEventListener instance;
+    private static boolean isInitialized = false;
+
+    public TorLogEventListener getInstance(Context context) {
+        if (instance == null) {
+            instance = new TorLogEventListener();
+        }
+        return instance;
+    }
+
+    private synchronized static void initialize(Context context) {
+        LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(context);
+        lbm.registerReceiver(mLocalBroadcastReceiver,
+                new IntentFilter(TorServiceConstants.ACTION_STATUS));
+        lbm.registerReceiver(mLocalBroadcastReceiver,
+                new IntentFilter(TorServiceConstants.LOCAL_ACTION_LOG));
+
+        isInitialized = true;
+        // There should be at least two Loggers: TorBootstrapPanel
+        // and TorBootstrapLogPanel
+        mLoggers = new Vector<TorBootstrapLogger>(2);
+    }
+
+    public synchronized static void addLogger(TorBootstrapLogger logger) {
+        if (!isInitialized) {
+            // This is an assumption we're making. All Loggers are a subclass
+            // of an Activity.
+            Activity activity = logger.getActivity();
+            initialize(activity);
+        }
+
+        if (mLoggers.contains(logger)) {
+            return;
+        }
+        mLoggers.add(logger);
+    }
+
+    public synchronized static void deleteLogger(TorBootstrapLogger logger) {
+        mLoggers.remove(logger);
+    }
+
+    /**
+     * The state and log info from {@link TorService} are sent to the UI here in
+     * the form of a local broadcast. Regular broadcasts can be sent by any app,
+     * so local ones are used here so other apps cannot interfere with Orbot's
+     * operation.
+     *
+     * Copied from Orbot - OrbotMainActivity.java
+     */
+    private static BroadcastReceiver mLocalBroadcastReceiver = new BroadcastReceiver() {
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            String action = intent.getAction();
+            if (action == null) {
+                return;
+            }
+
+            // This is only defined for log updates
+            if (!action.equals(TorServiceConstants.LOCAL_ACTION_LOG) &&
+                !action.equals(TorServiceConstants.ACTION_STATUS)) {
+                return;
+            }
+
+            Message msg = mStatusUpdateHandler.obtainMessage();
+
+            if (action.equals(TorServiceConstants.LOCAL_ACTION_LOG)) {
+                msg.obj = intent.getStringExtra(TorServiceConstants.LOCAL_EXTRA_LOG);
+            }
+
+            msg.getData().putString("status",
+                                    intent.getStringExtra(TorServiceConstants.EXTRA_STATUS));
+            mStatusUpdateHandler.sendMessage(msg);
+        }
+    };
+
+
+    // 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 static Handler mStatusUpdateHandler = new Handler() {
+
+        @Override
+        public void handleMessage(final Message msg) {
+           String newTorStatus = msg.getData().getString("status");
+           String log = (String)msg.obj;
+
+           for (TorBootstrapLogger l : mLoggers) {
+               l.updateStatus(log, newTorStatus);
+           }
+           super.handleMessage(msg);
+        }
+    };
+}
diff --git a/mobile/android/base/java/org/mozilla/gecko/torbootstrap/TorPreferences.java b/mobile/android/base/java/org/mozilla/gecko/torbootstrap/TorPreferences.java
new file mode 100644
index 000000000000..32a3bed3e685
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/torbootstrap/TorPreferences.java
@@ -0,0 +1,950 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.torbootstrap;
+
+
+import android.app.Activity;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.graphics.Typeface;
+import android.os.Bundle;
+import android.preference.Preference;
+import android.preference.PreferenceFragment;
+import android.preference.PreferenceScreen;
+import android.preference.SwitchPreference;
+import android.text.style.ClickableSpan;
+import android.text.SpannableString;
+import android.text.Spanned;
+import android.text.method.LinkMovementMethod;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+import android.view.ViewParent;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+import android.widget.ListView;
+import android.widget.AdapterView;
+import android.widget.RadioButton;
+import android.widget.RadioGroup;
+import android.widget.Switch;
+import android.widget.TextView;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.Xml;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Vector;
+
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.preferences.AppCompatPreferenceActivity;
+
+import org.torproject.android.service.util.Prefs;
+
+import org.xmlpull.v1.XmlPullParser;
+
+import static org.mozilla.gecko.preferences.GeckoPreferences.NON_PREF_PREFIX;
+
+
+/** TorPreferences provides the Tor-related preferences
+ *
+ * We configure bridges using either a set of built-in bridges where the user enables
+ * them based on bridge type (the name of the pluggable transport) or the user provides
+ * their own bridge (obtained from another person or BridgeDB, etc).
+ *
+ * This class (TorPreferences) is divided into multiple Fragments (screens). The first
+ * screen is where the user enables or disables Bridges. The second screen shows the
+ * user a list of built-in bridge types (obfs4, meek, etc) where they may select one of
+ * them. It shows a button they may press for providing their own bridge, as well. The
+ * third screen is where the user may provide (copy/paste) their own bridge.
+ *
+ * On the first screen, if bridges are currently enabled, then the switch/toggle is
+ * shown as enabled. In addition, the user is shown a message saying whether built-in or
+ * provided bridges are being used. There is a link, labeled "Change", where they
+ * transitioned to the appropriate screen for modifying the configuration if it is pressed.
+ *
+ * The second screen shows radio buttons for the built-in bridge types.
+ *
+ * The State of Bridges-Enabled:
+ * There are a few moving parts here, a higher-level description of how we expect this
+ * works, where "Enabled" is "Bridges Enabled", "Type" is "Bridge Type", and "Provided"
+ * is "Bridge Provided":
+ *
+ * We have five preferences:
+ *   PREFS_BRIDGES_ENABLED
+ *   PREFS_BRIDGES_TYPE
+ *   PREFS_BRIDGES_PROVIDE
+ *   pref_bridges_enabled (Orbot)
+ *   pref_bridges_list    (Orbot)
+ *
+ * These may be in following three end states where PREFS_BRIDGES_ENABLED and
+ * pref_bridges_enabled must always match, and pref_bridges_list must either match
+ * PREFS_BRIDGES_PROVIDE or contain a list of bridges of type PREFS_BRIDGES_TYPE.
+ *
+ *   PREFS_BRIDGES_ENABLED=false
+ *   PREFS_BRIDGES_TYPE=null
+ *   PREFS_BRIDGES_PROVIDE=null
+ *   pref_bridges_enabled=false
+ *   pref_bridges_list=null
+ *
+ *   PREFS_BRIDGES_ENABLED=true
+ *   PREFS_BRIDGES_TYPE=T1
+ *   PREFS_BRIDGES_PROVIDE=null
+ *   pref_bridges_enabled=true
+ *   pref_bridges_list=X1
+ *
+ *   PREFS_BRIDGES_ENABLED=true
+ *   PREFS_BRIDGES_TYPE=null
+ *   PREFS_BRIDGES_PROVIDE=X2
+ *   pref_bridges_enabled=true
+ *   pref_bridges_list=X2
+ *
+ * There are transition states where this is not consistent, for example when the
+ * "Bridges Enabled" switch is toggled but "Bridge Type" and "Bridge Provided" are null.
+ */
+
+public class TorPreferences extends AppCompatPreferenceActivity {
+    private static final String LOGTAG = "TorPreferences";
+
+    private static final String PREFS_BRIDGES_ENABLED = NON_PREF_PREFIX + "tor.bridges.enabled";
+    private static final String PREFS_BRIDGES_TYPE = NON_PREF_PREFIX + "tor.bridges.type";
+    private static final String PREFS_BRIDGES_PROVIDE = NON_PREF_PREFIX + "tor.bridges.provide";
+
+    private static final String sClassName = TorPreferences.class.getName();
+    private static final String sTorNetworkBridgesEnabledPreferenceName = sClassName + "$TorNetworkBridgesEnabledPreference";
+    private static final String sTorNetworkBridgeSelectPreferenceName = sClassName + "$TorNetworkBridgeSelectPreference";
+    private static final String sTorNetworkBridgeProvidePreferenceName = sClassName + "$TorNetworkBridgeProvidePreference";
+    private static final String[] sTorPreferenceFragments = {sTorNetworkBridgesEnabledPreferenceName,
+                                                  sTorNetworkBridgeSelectPreferenceName,
+                                                  sTorNetworkBridgeProvidePreferenceName};
+    // Current displayed PreferenceFragment
+    private TorNetworkPreferenceFragment mFrag;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        // Begin with the first (Enable Bridges) fragment
+        getIntent().putExtra(EXTRA_SHOW_FRAGMENT, sTorNetworkBridgesEnabledPreferenceName);
+        super.onCreate(savedInstanceState);
+
+        mFrag = null;
+    }
+
+    // Save the current preference when the app is minimized or swiped away.
+    @Override
+    public void onStop() {
+        mFrag.onSaveState();
+        super.onStop();
+    }
+
+    // This is needed because launching a fragment fails if this
+    // method doesn't return true.
+    @Override
+    protected boolean isValidFragment(String fragmentName) {
+        for (String frag : sTorPreferenceFragments) {
+            if (fragmentName.equals(frag)) {
+                return true;
+            }
+        }
+        Log.i(LOGTAG, "isValidFragment(): Returning false (" + fragmentName + ")");
+        return false;
+    }
+
+    public void setFragment(TorNetworkPreferenceFragment frag) {
+        mFrag = frag;
+    }
+
+    // Save the preference when the user returns to the previous screen using
+    // the back button
+    @Override
+    public void onBackPressed() {
+        mFrag.onSaveState();
+        super.onBackPressed();
+    }
+
+    // Control the behavior when the Up button (back button in top-left
+    // corner) is pressed. Save the current preference and return to the
+    // previous screen.
+    @Override
+    public boolean onNavigateUp() {
+        super.onNavigateUp();
+
+        if (mFrag == null) {
+            Log.w(LOGTAG, "onNavigateUp(): mFrag is null");
+            return false;
+        }
+
+        // Handle the user pressing the Up button in the same way as
+        // we handle them pressing the Back button. Strictly, this
+        // isn't correct, but it will prevent confusion.
+        mFrag.onSaveState();
+
+        if (mFrag.getFragmentManager().getBackStackEntryCount() > 0) {
+            Log.i(LOGTAG, "onNavigateUp(): popping from backstatck");
+            mFrag.getFragmentManager().popBackStack();
+        } else {
+            Log.i(LOGTAG, "onNavigateUp(): finishing activity");
+            finish();
+        }
+        return true;
+    }
+
+    // Overriding this method is necessary because before Oreo the PreferenceActivity didn't
+    // correctly handle the Home button (Up button). This was implemented in Oreo (Android 8+,
+    // API 26+).
+    // https://android.googlesource.com/platform/frameworks/base/+/6af15ebcfec64d0cc6879a0af9cfffd3e084ee73
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        if (item.getItemId() == android.R.id.home) {
+            Log.i(LOGTAG, "onOptionsItemSelected(): Home");
+            onNavigateUp();
+            return true;
+        }
+
+        return super.onOptionsItemSelected(item);
+    }
+
+    // Helper abstract Fragment with common methods
+    public static abstract class TorNetworkPreferenceFragment extends PreferenceFragment {
+        protected TorPreferences mTorPrefAct;
+
+        @Override
+        public void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+
+            // This is only ever a TorPreferences
+            mTorPrefAct = (TorPreferences) getActivity();
+        }
+
+        @Override
+        public void onResume() {
+            super.onResume();
+            mTorPrefAct.setFragment(this);
+        }
+
+        // Implement this callback in child Fragments
+        public void onSaveState() {
+        }
+
+        // Helper method for walking a View hierarchy and printing the children
+        protected void walkViewTree(View view, int depth) {
+            if (view instanceof ViewGroup) {
+                ViewGroup vg = (ViewGroup) view;
+                int childIdx = 0;
+                for (; childIdx < vg.getChildCount(); childIdx++) {
+                    walkViewTree(vg.getChildAt(childIdx), depth + 1);
+                }
+            }
+            Log.i(LOGTAG, "walkViewTree: " + depth + ": view: " + view);
+            Log.i(LOGTAG, "walkViewTree: " + depth + ": view id: " + view.getId());
+            Log.i(LOGTAG, "walkViewTree: " + depth + ": view is focused: " + view.isFocused());
+            Log.i(LOGTAG, "walkViewTree: " + depth + ": view is enabled: " + view.isEnabled());
+            Log.i(LOGTAG, "walkViewTree: " + depth + ": view is selected: " + view.isSelected());
+            Log.i(LOGTAG, "walkViewTree: " + depth + ": view is in touch mode: " + view.isInTouchMode());
+            Log.i(LOGTAG, "walkViewTree: " + depth + ": view is activated: " + view.isActivated());
+            Log.i(LOGTAG, "walkViewTree: " + depth + ": view is clickable: " + view.isClickable());
+            Log.i(LOGTAG, "walkViewTree: " + depth + ": view is focusable: " + view.isFocusable());
+            Log.i(LOGTAG, "walkViewTree: " + depth + ": view is FocusableInTouchMode: " + view.isFocusableInTouchMode());
+        }
+
+        // Helper returning the ListView
+        protected ListView getListView(View view) {
+            if (!(view instanceof ViewGroup) || view == null) {
+                return null;
+            }
+
+            View rawListView = view.findViewById(android.R.id.list);
+            if (!(rawListView instanceof ListView) || rawListView == null) {
+                return null;
+            }
+
+            return (ListView) rawListView;
+        }
+
+        // Get Bridges associated with the provided pref key saved in the
+        // provided SharedPreferences. Return null if the SharedPreferences
+        // is null or if there isn't any value associated with the pref.
+        protected String getBridges(SharedPreferences sharedPrefs, String pref) {
+            if (sharedPrefs == null) {
+                Log.w(LOGTAG, "getBridges: sharedPrefs is null");
+                return null;
+            }
+            return sharedPrefs.getString(pref, null);
+        }
+
+        // Save the bridge type and bridge line preferences.
+        //
+        // Save the bridgesType with the PREFS_BRIDGES_TYPE pref as the key
+        // (for future lookup). If bridgesType is null, then save the
+        // bridgesLines with the PREFS_BRIDGES_PROVIDE pref as the key, and
+        // use Orbot's helper method and enable Orbot's bridge pref.
+        protected boolean setBridges(SharedPreferences.Editor editor, String bridgesType, String bridgesLines) {
+            if (editor == null) {
+                Log.w(LOGTAG, "setBridges: editor is null");
+                return false;
+            }
+            Log.i(LOGTAG, "Saving bridge type preference: " + bridgesType);
+            Log.i(LOGTAG, "Saving bridge line preference: " + bridgesLines);
+
+            // If bridgesType is null, then clear the pref and save the bridgesLines
+            // as a provided bridge. If bridgesType is not null, then save the type
+            // but don't save it as a provided bridge.
+            editor.putString(PREFS_BRIDGES_TYPE, bridgesType);
+            if (bridgesType == null) {
+                editor.putString(PREFS_BRIDGES_PROVIDE, bridgesLines);
+            } else {
+                editor.putString(PREFS_BRIDGES_PROVIDE, null);
+            }
+
+            if (!editor.commit()) {
+                return false;
+            }
+
+            // Set Orbot's preference
+            Prefs.setBridgesList(bridgesLines);
+
+            // If either of these are not null, then we're enabling bridges
+            boolean bridgesAreEnabled = (bridgesType != null) || (bridgesLines != null);
+            // Inform Orbot bridges are enabled
+            Prefs.putBridgesEnabled(bridgesAreEnabled);
+            return true;
+        }
+
+        // Disable the bridges.enabled Preference
+        protected void disableBridges(PreferenceFragment frag) {
+            SwitchPreference bridgesEnabled = (SwitchPreference) frag.findPreference(PREFS_BRIDGES_ENABLED);
+            Preference bridgesType = frag.findPreference(PREFS_BRIDGES_TYPE);
+            Preference bridgesProvide = frag.findPreference(PREFS_BRIDGES_PROVIDE);
+            Preference pref = null;
+
+            if (bridgesEnabled != null) {
+                Log.i(LOGTAG, "disableBridges: bridgesEnabled is not null");
+                pref = bridgesEnabled;
+            } else if (bridgesType != null) {
+                Log.i(LOGTAG, "disableBridges: bridgesType is not null");
+                pref = bridgesType;
+            } else if (bridgesProvide != null) {
+                Log.i(LOGTAG, "disableBridges: bridgesProvide is not null");
+                pref = bridgesProvide;
+            } else {
+                Log.w(LOGTAG, "disableBridges: all the expected preferences are is null?");
+                return;
+            }
+
+            // Clear the saved prefs (it's okay we're using a different
+            // SharedPreference.Editor here, they modify the same backend).
+            // In addition, passing null is equivalent to clearing the
+            // preference.
+            setBridges(pref.getEditor(), null, null);
+
+            if (bridgesEnabled != null) {
+                bridgesEnabled.setChecked(false);
+            }
+        }
+
+        // Set the current title
+        protected void setTitle(int resId) {
+            mTorPrefAct.getSupportActionBar().setTitle(resId);
+        }
+    }
+
+    // Fragment implementing the screen for enabling Bridges
+    public static class TorNetworkBridgesEnabledPreference extends TorNetworkPreferenceFragment {
+
+        @Override
+        public void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            addPreferencesFromResource(R.xml.preferences_tor_network_main);
+        }
+
+        // This class is instantiated within the OnClickListener of the
+        // PreferenceSwitch's Switch widget
+        public class BridgesEnabledSwitchOnClickListener implements View.OnClickListener {
+                @Override
+                public void onClick(View v) {
+                    Log.i(LOGTAG, "bridgesEnabledSwitch clicked");
+                    if (!(v instanceof Switch)) {
+                        Log.w(LOGTAG, "View isn't an instance of Switch?");
+                        return;
+                    }
+
+                    Switch bridgesEnabledSwitch = (Switch) v;
+
+                    // The widget was pressed, now find the preference and set it
+                    // such that it is synchronized with the widget.
+                    final SwitchPreference bridgesEnabled = (SwitchPreference) TorNetworkBridgesEnabledPreference.this.findPreference(PREFS_BRIDGES_ENABLED);
+                    if (bridgesEnabled == null) {
+                        Log.w(LOGTAG, "onCreate: bridgesEnabled is null?");
+                        return;
+                    }
+
+                    bridgesEnabled.setChecked(bridgesEnabledSwitch.isChecked());
+
+                    // Only launch the Fragment if we're enabling bridges.
+                    if (bridgesEnabledSwitch.isChecked()) {
+                        TorNetworkBridgesEnabledPreference.this.mTorPrefAct.startPreferenceFragment(new TorNetworkBridgeSelectPreference(), true);
+                    } else {
+                        disableBridges(TorNetworkBridgesEnabledPreference.this);
+                    }
+                }
+        }
+
+        // This method must be overridden because, when creating Preferences, the
+        // creation of the View hierarchy occurs asynchronously. Usually
+        // onCreateView() gives us the View hierarchy as it is defined in the XML layout.
+        // However, with Preferences the layout is created across multiple threads and it
+        // usually isn't available at the time onCreateView() or onViewCreated() are
+        // called. As a result, we find the ListView (which is almost guaranteed to exist
+        // at this time) and we add an OnHierarchyChangeListener where we wait until the
+        // children are added into the tree.
+        @Override
+        public void onViewCreated(View view, Bundle savedInstanceState) {
+            super.onViewCreated(view, savedInstanceState);
+            setTitle(R.string.pref_tor_network_title);
+
+            final SwitchPreference bridgesEnabled = (SwitchPreference) findPreference(PREFS_BRIDGES_ENABLED);
+            if (bridgesEnabled == null) {
+                Log.w(LOGTAG, "onCreate: bridgesEnabled is null?");
+                return;
+            }
+
+            // If we return from either of the "Select Bridge Type" screen
+            // or "Provide Bridge" screen without selecting or inputing
+            // any value, then we could arrive here without any bridge
+            // saved/enabled but this switch is enabled. Disable it.
+            if (!Prefs.bridgesEnabled()) {
+                bridgesEnabled.setChecked(false);
+            }
+
+            // Decide if the configured bridges were provided by the user or
+            // selected from the list of bridge types
+            if (isBridgeProvided(bridgesEnabled)) {
+                String newSummary = getString(R.string.pref_tor_network_bridges_enabled_change_custom);
+                setBridgesEnabledSummaryAndOnClickListener(bridgesEnabled, newSummary, true);
+            } else if (Prefs.bridgesEnabled()) {
+                // If isBridgeProvided() returned false, but Prefs.bridgesEnabled() returns true.
+                // This means we have bridges, but they weren't provided by the user - therefore
+                // they must be built-in bridges.
+                String newSummary = getString(R.string.pref_tor_network_bridges_enabled_change_builtin);
+                setBridgesEnabledSummaryAndOnClickListener(bridgesEnabled, newSummary, false);
+            }
+
+            ListView lv = getListView(view);
+            if (lv == null) {
+                Log.i(LOGTAG, "onViewCreated: ListView not found");
+                return;
+            }
+
+            lv.setOnHierarchyChangeListener(new ViewGroup.OnHierarchyChangeListener() {
+
+                @Override
+                public void onChildViewAdded(View parent, View child) {
+                    Log.i(LOGTAG, "onChildViewAdded: Adding ListView child view");
+
+                    // Make sure the Switch widget is synchronized with the preference
+                    final Switch bridgesEnabledSwitch =
+                        (Switch) parent.findViewById(android.R.id.switch_widget);
+
+                    if (bridgesEnabledSwitch != null) {
+                        bridgesEnabledSwitch.setChecked(bridgesEnabled.isChecked());
+
+                        // When the Switch is pressed by the user, either load the next
+                        // fragment (where the user chooses a bridge type), or return to
+                        // the main bootstrapping screen.
+                        bridgesEnabledSwitch.setOnClickListener(new BridgesEnabledSwitchOnClickListener());
+                    }
+
+                    final TextView bridgesEnabledSummary =
+                                (TextView) parent.findViewById(android.R.id.summary);
+                    if (bridgesEnabledSummary == null) {
+                        Log.w(LOGTAG, "Bridge Enabled Summary is null, we can't enable the span");
+                        return;
+                    }
+
+                    // Make the ClickableSpan clickable within the TextView.
+                    // This is a requirement for using a ClickableSpan in
+                    // setBridgesEnabledSummaryAndOnClickListener().
+                    bridgesEnabledSummary.setMovementMethod(LinkMovementMethod.getInstance());
+               }
+
+                @Override
+                public void onChildViewRemoved(View parent, View child) {
+                }
+            });
+        }
+
+        // This is a common OnClickListener for when the user clicks on the Change link.
+        // The span won't be clickable until the MovementMethod is set. This happens in
+        // onViewCreated within the OnHierarchyChangeListener we set on the ListView.
+        private void setBridgesEnabledSummaryAndOnClickListener(SwitchPreference bridgesEnabled, final String newSummary, final boolean custom) {
+            Log.i(LOGTAG, "Bridge Summary clicked");
+            if (bridgesEnabled == null) {
+                Log.w(LOGTAG, "Bridge Enabled switch is null");
+                return;
+            }
+
+            // Here we obtain the correct text, based on whether the bridges
+            // were provided (custom) or built-in. Using that text, we create
+            // a spannable string and find the substring "Change" within it.
+            // If it exists, we make that substring clickable.
+            // Note: TODO This breaks with localization.
+            if (newSummary == null) {
+                Log.w(LOGTAG, "R.string.pref_tor_network_bridges_enabled_change_builtin is null");
+                return;
+            }
+            int changeStart = newSummary.indexOf("Change");
+            if (changeStart == -1) {
+                Log.w(LOGTAG, "R.string.pref_tor_network_bridges_enabled_change_builtin doesn't contain 'Change'");
+                return;
+            }
+            SpannableString newSpannableSummary = new SpannableString(newSummary);
+            newSpannableSummary.setSpan(new ClickableSpan() {
+                @Override
+                public void onClick(View v) {
+                    // If a custom (provided) bridge is configured, then
+                    // open the BridgesProvide preference fragment. Else,
+                    // open the built-in/bridge-type fragment.
+                    Log.i(LOGTAG, "Span onClick!");
+
+                    // Add this Fragment regardless of which Fragment we're showing next. If the Change
+                    // link goes to the built-in bridges, then this is what we show the user. If the Change
+                    // link goes to the provided bridges, then we consider this a deep-link and we inject the
+                    // built-in bridges screen into the backstack so they are shown it when they press Back
+                    // from the provided-bridges screen.
+                    mTorPrefAct.startPreferenceFragment(new
+                            TorNetworkBridgeSelectPreference(), true);
+
+                    if (custom) {
+                        mTorPrefAct.startPreferenceFragment(new
+                                TorNetworkBridgeProvidePreference(), true);
+                    }
+                }
+            },
+            // Begin the span
+            changeStart,
+            // End the span
+            newSummary.length(),
+            // Don't include new characters added into the spanned substring
+            Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+
+            bridgesEnabled.setSummaryOn(newSpannableSummary);
+        }
+
+        // We follow this logic:
+        //   If the bridgesEnabled switch is off, then false
+        //   If Orbot doesn't have bridges enabled, then false
+        //   If PREFS_BRIDGES_PROVIDE is not null, then true
+        //   Else false
+        private boolean isBridgeProvided(SwitchPreference bridgesEnabled) {
+            if (!bridgesEnabled.isChecked()) {
+                Log.i(LOGTAG, "isBridgeProvided: bridgesEnabled is not checked");
+                return false;
+            }
+
+            if (!Prefs.bridgesEnabled()) {
+                Log.i(LOGTAG, "isBridgeProvided: bridges are not enabled");
+                return false;
+            }
+            SharedPreferences sharedPrefs = bridgesEnabled.getSharedPreferences();
+            boolean hasBridgeProvide =
+                sharedPrefs.getString(PREFS_BRIDGES_PROVIDE, null) != null;
+
+            Log.i(LOGTAG, "isBridgeProvided: We have provided bridges: " + hasBridgeProvide);
+            return hasBridgeProvide;
+        }
+    }
+
+    // Fragment implementing the screen for selecting a built-in Bridge type
+    public static class TorNetworkBridgeSelectPreference extends TorNetworkPreferenceFragment {
+
+        @Override
+        public void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            addPreferencesFromResource(R.xml.preferences_tor_network_select_bridge_type);
+        }
+
+        // Add OnClickListeners after the View is created
+        @Override
+        public void onViewCreated(View view, Bundle savedInstanceState) {
+            super.onViewCreated(view, savedInstanceState);
+            setTitle(R.string.pref_tor_select_a_bridge_title);
+
+            ListView lv = getListView(view);
+            if (lv == null) {
+                Log.i(LOGTAG, "onViewCreated: ListView not found");
+                return;
+            }
+
+            // Configure onClick handler for "Provide a Bridge" button
+            lv.setOnHierarchyChangeListener(new ViewGroup.OnHierarchyChangeListener() {
+
+                @Override
+                public void onChildViewAdded(View parent, View child) {
+                    // Set the previously chosen RadioButton as checked
+                    final RadioGroup group = getBridgeTypeRadioGroup();
+                    if (group == null) {
+                        Log.w(LOGTAG, "Radio Group is null");
+                        return;
+                    }
+
+                    final View titleAndSummaryView = parent.findViewById(R.id.title_and_summary);
+                    if (titleAndSummaryView == null) {
+                        Log.w(LOGTAG, "title and summary view is null");
+                        group.setVisibility(View.VISIBLE);
+                        return;
+                    }
+
+                    titleAndSummaryView.setOnClickListener(new View.OnClickListener() {
+                        @Override
+                        public void onClick(View v) {
+                            group.setVisibility(View.VISIBLE);
+                        }
+                    });
+
+                    final View provideABridge = parent.findViewById(R.id.tor_network_provide_a_bridge);
+                    if (provideABridge == null) {
+                        return;
+                    }
+
+                    provideABridge.setOnClickListener(new View.OnClickListener() {
+                        @Override
+                        public void onClick(View v) {
+                            Log.i(LOGTAG, "bridgesProvide clicked");
+                            saveCurrentCheckedRadioButton();
+
+                            mTorPrefAct.startPreferenceFragment(new TorNetworkBridgeProvidePreference(), true);
+                        }
+                    });
+
+                    final TextView provideABridgeSummary = (TextView) parent.findViewById(R.id.tor_network_provide_a_bridge_summary);
+                    if (provideABridgeSummary == null) {
+                        Log.i(LOGTAG, "provideABridgeSummary is null");
+                        return;
+                    }
+
+                    Preference bridgesTypePref = findPreference(PREFS_BRIDGES_TYPE);
+                    if (bridgesTypePref == null) {
+                        return;
+                    }
+
+                    SharedPreferences sharedPrefs = bridgesTypePref.getSharedPreferences();
+                    String provideBridges = sharedPrefs.getString(PREFS_BRIDGES_PROVIDE, null);
+                    if (provideBridges != null) {
+                        if (provideBridges.indexOf("\n") != -1) {
+                            provideABridgeSummary.setText(R.string.pref_tor_network_using_multiple_provided_bridges);
+                        } else {
+                            String summary = getString(R.string.pref_tor_network_using_a_provided_bridge, provideBridges);
+                            provideABridgeSummary.setText(summary);
+                        }
+                    }
+
+                    final String configuredBridgeType = getBridges(bridgesTypePref.getSharedPreferences(), PREFS_BRIDGES_TYPE);
+                    if (configuredBridgeType == null) {
+                        return;
+                    }
+
+                    int buttonId = -1;
+                    // Note: Keep these synchronized with the layout xml file.
+                    switch (configuredBridgeType) {
+                        case "obfs4":
+                            buttonId = R.id.radio_pref_bridges_obfs4;
+                            break;
+                        case "meek":
+                            buttonId = R.id.radio_pref_bridges_meek_azure;
+                            break;
+                        case "obfs3":
+                            buttonId = R.id.radio_pref_bridges_obfs3;
+                            break;
+                    }
+
+                    if (buttonId != -1) {
+                        group.check(buttonId);
+                        // If a bridge is selected, then make the list visible
+                        group.setVisibility(View.VISIBLE);
+                    }
+                }
+
+                @Override
+                public void onChildViewRemoved(View parent, View child) {
+                }
+            });
+
+        }
+
+        // Save the checked RadioButton in the SharedPreferences
+        private boolean saveCurrentCheckedRadioButton() {
+            ListView lv = getListView(getView());
+            if (lv == null) {
+                Log.w(LOGTAG, "ListView is null");
+                return false;
+            }
+
+            RadioGroup group = getBridgeTypeRadioGroup();
+            if (group == null) {
+                Log.w(LOGTAG, "RadioGroup is null");
+                return false;
+            }
+
+            int checkedId = group.getCheckedRadioButtonId();
+            RadioButton selectedBridgeType = lv.findViewById(checkedId);
+            if (selectedBridgeType == null) {
+                Log.w(LOGTAG, "RadioButton is null");
+                return false;
+            }
+
+            String bridgesType = selectedBridgeType.getText().toString();
+            if (bridgesType == null) {
+                // We don't know with which bridgesType this Id is associated
+                Log.w(LOGTAG, "RadioButton has null text");
+                return false;
+            }
+
+            // Currently obfs4 is the recommended pluggable transport. As a result,
+            // the text contains " (recommended)". This won't be expected elsewhere,
+            // so replace the string with only the pluggable transport name.
+            // This will need updating when another transport is "recommended".
+            //
+            // Similarly, if meek-azure is chosen, substitute it with "meek" (Orbot
+            // only handles these keywords specially if they are less than 5 characters).
+            if (bridgesType.contains("obfs4")) {
+                bridgesType = "obfs4";
+            } else if (bridgesType.contains("meek-azure")) {
+                bridgesType = "meek";
+            }
+
+            Preference bridgesTypePref = findPreference(PREFS_BRIDGES_TYPE);
+            if (bridgesTypePref == null) {
+                Log.w(LOGTAG, PREFS_BRIDGES_TYPE + " preference not found");
+                disableBridges(this);
+                return false;
+            }
+
+            if (!setBridges(bridgesTypePref.getEditor(), bridgesType, bridgesType)) {
+                Log.w(LOGTAG, "Saving Bridge preference failed.");
+                disableBridges(this);
+                return false;
+            }
+
+            return true;
+        }
+
+        // Handle onSaveState when the user presses Back. Save the selected
+        // built-in bridge type.
+        @Override
+        public void onSaveState() {
+            saveCurrentCheckedRadioButton();
+        }
+
+        // Find the RadioGroup within the View hierarchy now.
+        private RadioGroup getBridgeTypeRadioGroup() {
+            ListView lv = getListView(getView());
+            if (lv == null) {
+                Log.w(LOGTAG, "ListView is null");
+                return null;
+            }
+            ViewParent listViewParent = lv.getParent();
+            // If the parent of this ListView isn't a View, then
+            // the RadioGroup doesn't exist
+            if (!(listViewParent instanceof View)) {
+                Log.w(LOGTAG, "ListView's parent isn't a View. Failing");
+                return null;
+            }
+            View lvParent = (View) listViewParent;
+            // Find the RadioGroup with this View hierarchy.
+            return (RadioGroup) lvParent.findViewById(R.id.pref_radio_group_builtin_bridges_type);
+        }
+    }
+
+    // Fragment implementing the screen for providing a Bridge
+    public static class TorNetworkBridgeProvidePreference extends TorNetworkPreferenceFragment {
+        @Override
+        public void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            addPreferencesFromResource(R.xml.preferences_tor_network_provide_bridge);
+        }
+
+        // If there is a provided bridge saved in the preference,
+        // then fill-in the text field with that value.
+        private void setBridgeProvideText(View parent) {
+            final View provideBridge1 = parent.findViewById(R.id.tor_network_provide_bridge1);
+            final View provideBridge2 = parent.findViewById(R.id.tor_network_provide_bridge2);
+            final View provideBridge3 = parent.findViewById(R.id.tor_network_provide_bridge3);
+
+            EditText provideBridge1ET = null;
+            EditText provideBridge2ET = null;
+            EditText provideBridge3ET = null;
+
+            if (provideBridge1 != null) {
+                if (provideBridge1 instanceof EditText) {
+                    provideBridge1ET = (EditText) provideBridge1;
+                }
+            }
+
+            if (provideBridge2 != null) {
+                if (provideBridge2 instanceof EditText) {
+                    provideBridge2ET = (EditText) provideBridge2;
+                }
+            }
+
+            if (provideBridge3 != null) {
+                if (provideBridge3 instanceof EditText) {
+                    provideBridge3ET = (EditText) provideBridge3;
+                }
+            }
+
+            Preference bridgesProvide = findPreference(PREFS_BRIDGES_PROVIDE);
+            if (bridgesProvide != null) {
+                Log.i(LOGTAG, "setBridgeProvideText: bridgesProvide isn't null");
+                String bridgesLines = getBridges(bridgesProvide.getSharedPreferences(), PREFS_BRIDGES_PROVIDE);
+                if (bridgesLines != null) {
+                    Log.i(LOGTAG, "setBridgeProvideText: bridgesLines isn't null");
+                    if (bridgesLines.contains("\n")) {
+                        String[] lines = bridgesLines.split("\n");
+                        if (provideBridge1ET != null && lines.length >= 1) {
+                            provideBridge1ET.setText(lines[0]);
+                        }
+                        if (provideBridge2ET != null && lines.length >= 2) {
+                            provideBridge2ET.setText(lines[1]);
+                        }
+                        if (provideBridge3ET != null && lines.length >= 3) {
+                            provideBridge3ET.setText(lines[2]);
+                        }
+                    } else {
+                        // Simply set the single line as the text field input if the text field exists.
+                        if (provideBridge1ET != null) {
+                            provideBridge1ET.setText(bridgesLines);
+                        }
+                    }
+                }
+            }
+        }
+
+        // See explanation of TorNetworkBridgesEnabledPreference.onViewCreated()
+        @Override
+        public void onViewCreated(View view, Bundle savedInstanceState) {
+            super.onViewCreated(view, savedInstanceState);
+            setTitle(R.string.pref_tor_provide_a_bridge_title);
+            ListView lv = getListView(view);
+            if (lv == null) {
+                Log.i(LOGTAG, "onViewCreated: ListView not found");
+                return;
+            }
+            // The ListView is given "focus" by default when the EditText
+            // field is selected, this prevents typing anything into the field.
+            // We set FOCUS_AFTER_DESCENDANTS so the ListView's children are
+            // given focus (and, therefore, the EditText) before it is
+            // given focus.
+            lv.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
+
+            // The preferences are adding into the ListView hierarchy asynchronously.
+            // We need the onChildViewAdded callback so we can modify the layout after
+            // the child is added.
+            lv.setOnHierarchyChangeListener(new ViewGroup.OnHierarchyChangeListener() {
+                @Override
+                public void onChildViewAdded(View parent, View child) {
+                    // If we have a bridge line saved for this pref,
+                    // then show the user
+                    setBridgeProvideText(parent);
+                }
+
+                @Override
+                public void onChildViewRemoved(View parent, View child) {
+                }
+            });
+        }
+
+        private String getBridgeLineFromView(View provideBridge) {
+            if (provideBridge != null) {
+                if (provideBridge instanceof EditText) {
+                    Log.i(LOGTAG, "onSaveState: Saving bridge");
+                    EditText provideBridgeET = (EditText) provideBridge;
+
+                    // Get the bridge line (provided text) from the text
+                    // field.
+                    String bridgesLine = provideBridgeET.getText().toString();
+                    if (bridgesLine != null && !bridgesLine.equals("")) {
+                        return bridgesLine;
+                    }
+                } else {
+                    Log.w(LOGTAG, "onSaveState: provideBridge isn't an EditText");
+                }
+            }
+            return null;
+        }
+
+        // Save EditText field value when the Back button or Up button are pressed.
+        @Override
+        public void onSaveState() {
+            ListView lv = getListView(getView());
+            if (lv == null) {
+                Log.i(LOGTAG, "onSaveState: ListView not found");
+                return;
+            }
+
+            final View provideBridge1 = lv.findViewById(R.id.tor_network_provide_bridge1);
+            final View provideBridge2 = lv.findViewById(R.id.tor_network_provide_bridge2);
+            final View provideBridge3 = lv.findViewById(R.id.tor_network_provide_bridge3);
+
+            String bridgesLines = null;
+            String bridgesLine1 = getBridgeLineFromView(provideBridge1);
+            String bridgesLine2 = getBridgeLineFromView(provideBridge2);
+            String bridgesLine3 = getBridgeLineFromView(provideBridge3);
+
+            if (bridgesLine1 != null) {
+                Log.i(LOGTAG, "bridgesLine1 is not null.");
+                bridgesLines = bridgesLine1;
+            }
+
+            if (bridgesLine2 != null) {
+                // If bridgesLine1 was not null, then append a newline.
+                Log.i(LOGTAG, "bridgesLine2 is not null.");
+                if (bridgesLines != null) {
+                    bridgesLines += "\n" + bridgesLine2;
+                } else {
+                    bridgesLines = bridgesLine2;
+                }
+            }
+
+            if (bridgesLine3 != null) {
+                // If bridgesLine1 was not null, then append a newline.
+                Log.i(LOGTAG, "bridgesLine3 is not null.");
+                if (bridgesLines != null) {
+                    bridgesLines += "\n" + bridgesLine3;
+                } else {
+                    bridgesLines = bridgesLine3;
+                }
+            }
+
+            Preference bridgesProvide = findPreference(PREFS_BRIDGES_PROVIDE);
+            if (bridgesProvide == null) {
+                Log.w(LOGTAG, PREFS_BRIDGES_PROVIDE + " preference not found");
+                disableBridges(this);
+                return;
+            }
+
+            if (bridgesLines == null) {
+                Log.i(LOGTAG, "provideBridge is empty. Disabling.");
+                // If provided bridges are null/empty, then only disable all bridges if
+                // the user did not select a built-in bridge
+                String configuredBuiltinBridges = getBridges(bridgesProvide.getSharedPreferences(), PREFS_BRIDGES_TYPE);
+                if (configuredBuiltinBridges == null) {
+                    disableBridges(this);
+                }
+                return;
+            }
+
+            // Set the preferences (both our preference and Orbot's preference)
+            Log.w(LOGTAG, "Saving Bridge preference: " + bridgesLines);
+            if (!setBridges(bridgesProvide.getEditor(), null, bridgesLines)) {
+                // TODO inform the user
+                Log.w(LOGTAG, "Saving Bridge preference failed.");
+                disableBridges(this);
+            }
+        }
+    }
+}



More information about the tor-commits mailing list