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

gk at torproject.org gk at torproject.org
Sat Aug 31 19:46:16 UTC 2019


commit 385689ddc6c13420a40f1f813051ec0af99f306d
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
    
    Also:
    Bug 30214 - Kill background thread when Activity is null
    Bug 30239 - Render Fragments after crash
    Bug 29982 - Force single-pane UI on Tor Preferences
---
 .../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      |  83 ++
 .../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    |  52 +-
 .../TorBootstrapAnimationContainer.java            |  82 ++
 .../gecko/torbootstrap/TorBootstrapLogPanel.java   |  54 ++
 .../gecko/torbootstrap/TorBootstrapLogger.java     |  17 +
 .../gecko/torbootstrap/TorBootstrapPager.java      | 203 +++++
 .../torbootstrap/TorBootstrapPagerConfig.java      |  48 +
 .../gecko/torbootstrap/TorBootstrapPanel.java      | 575 ++++++++++++
 .../gecko/torbootstrap/TorLogEventListener.java    | 128 +++
 .../mozilla/gecko/torbootstrap/TorPreferences.java | 975 +++++++++++++++++++++
 22 files changed, 2680 insertions(+), 5 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 f48e7fc9f3be..d6a6133496e2 100644
--- a/mobile/android/app/src/main/res/layout/gecko_app.xml
+++ b/mobile/android/app/src/main/res/layout/gecko_app.xml
@@ -63,6 +63,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..af9c7d11d3f2
--- /dev/null
+++ b/mobile/android/app/src/main/res/layout/tor_bootstrap.xml
@@ -0,0 +1,83 @@
+<?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_marginBottom="37dp"
+                   android:layout_marginRight="10dp"
+                   android:layout_marginLeft="10dp"
+                   android:layout_centerHorizontal="true"
+                   android:layout_below="@id/tor_bootstrap_settings_gear"
+                   android:layout_above="@id/tor_bootstrap_last_status_message" />
+</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 c60210e0332c..228a7b6399b0 100644
--- a/mobile/android/base/AndroidManifest.xml.in
+++ b/mobile/android/base/AndroidManifest.xml.in
@@ -580,5 +580,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 e0ef8e9c43d9..da25e3b395be 100644
--- a/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
@@ -153,6 +153,7 @@ import org.mozilla.gecko.toolbar.BrowserToolbar;
 import org.mozilla.gecko.toolbar.BrowserToolbar.CommitEventSource;
 import org.mozilla.gecko.toolbar.BrowserToolbar.TabEditingState;
 import org.mozilla.gecko.toolbar.PwaConfirm;
+import org.mozilla.gecko.torbootstrap.TorBootstrapAnimationContainer;
 import org.mozilla.gecko.updater.PostUpdateHandler;
 import org.mozilla.gecko.updater.UpdateServiceHelper;
 import org.mozilla.gecko.util.ActivityUtils;
@@ -255,6 +256,7 @@ public class BrowserApp extends GeckoApp
     // We can't name the TabStrip class because it's not included on API 9.
     private TabStripInterface mTabStrip;
     private AnimatedProgressBar mProgressView;
+    private TorBootstrapAnimationContainer mTorBootstrapAnimationContainer;
     private HomeScreen mHomeScreen;
     private TabsPanel mTabsPanel;
 
@@ -390,7 +392,7 @@ public class BrowserApp extends GeckoApp
         Log.d(LOGTAG, "BrowserApp.onTabChanged: " + tab.getId() + ": " + msg);
         switch (msg) {
             case SELECTED:
-                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);
@@ -400,7 +402,7 @@ public class BrowserApp extends GeckoApp
                 }
                 // fall through
             case LOCATION_CHANGE:
-                if (Tabs.getInstance().isSelectedTab(tab)) {
+                if (Tabs.getInstance().isSelectedTab(tab) && !isTorBootstrapVisible()) {
                     updateHomePagerForTab(tab);
                 }
 
@@ -413,7 +415,7 @@ public class BrowserApp extends GeckoApp
                 if (Tabs.getInstance().isSelectedTab(tab)) {
                     invalidateOptionsMenu();
 
-                    if (mDynamicToolbar.isEnabled()) {
+                    if (mDynamicToolbar.isEnabled() && !isTorBootstrapVisible()) {
                         mDynamicToolbar.setVisible(true, VisibilityTransition.ANIMATE);
                     }
                 }
@@ -1191,8 +1193,12 @@ public class BrowserApp extends GeckoApp
         final SafeIntent intent = new SafeIntent(getIntent());
 
         if (!IntentUtils.getIsInAutomationFromEnvironment(intent)) {
-            // We can't show the first run experience until Gecko has finished initialization (bug 1077583).
-            mOnboardingHelper.checkFirstRun();
+            if (mTorNeedsStart) {
+                showTorBootstrapPager();
+            } else {
+                // We can't show the first run experience until Gecko has finished initialization (bug 1077583).
+                mOnboardingHelper.checkFirstRun();
+            }
         }
     }
 
@@ -2627,6 +2633,11 @@ public class BrowserApp extends GeckoApp
         return (SplashScreen) splashLayout.findViewById(R.id.splash_root);
     }
 
+    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
@@ -2988,6 +2999,37 @@ 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(true);
+
+                    // If we finished, then Tor bootstrapped 100%
+                    mTorNeedsStart = false;
+
+                    // When bootstrapping completes, check if the Firstrun (onboarding) screens
+                    // should be shown.
+                    mOnboardingHelper.checkFirstRun();
+                }
+            });
+        }
+
+        mHomeScreenContainer.setVisibility(View.VISIBLE);
+    }
+
     private void showHomePager(String panelId, Bundle panelRestoreData) {
         showHomePagerWithAnimator(panelId, panelRestoreData, 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..587806791e52
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/torbootstrap/TorBootstrapPager.java
@@ -0,0 +1,203 @@
+/* -*- 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();
+
+        this.pagerNavigation = new TorBootstrapPanel.PagerNavigation() {
+            @Override
+            public void next() {
+                // No-op implementation.
+            }
+
+            @Override
+            public void finish() {
+                if (onFinishListener != null) {
+                    onFinishListener.onFinish();
+                }
+            }
+        };
+
+        ViewPagerAdapter viewPagerAdapter = new ViewPagerAdapter(fm, panels);
+        setAdapter(viewPagerAdapter);
+
+        // The Fragments (Panels) should be attached to a parent View at this point (and
+        // the parent View should be |this|). If the Fragment's getParent() method returns
+        // |null|, then the Fragment was probably instantiated earlier by the FragmentManager
+        // (most likely because the app's state is being restored after it was killed by the
+        // system). If the parent View is not null, then the Fragment was instantiated below
+        // in the ViewPagerAdapter constructor.
+        //
+        // In the case where the Fragment's getParent() is null, then the Fragment was
+        // instantiated before TorBootstrapPager (|this|) was created. As a result, the
+        // fragment wasn't automatically added as a child View of the Pager (|this|) when it
+        // was created. Add the Fragments as children now.
+        //
+        // There may be a more Androidy-way of handling this.
+        for (int i = 0; i < viewPagerAdapter.getCount(); i++) {
+            Fragment fragment = viewPagerAdapter.getItem(i);
+            if (fragment == null) {
+                continue;
+            }
+
+            View fragmentView = fragment.getView();
+            if (fragmentView == null) {
+                continue;
+            }
+
+            if (fragmentView.getParent() == null) {
+              addView(fragmentView);
+            }
+        }
+
+        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 = getPagerPanels(fm);
+        }
+
+        private Fragment[] getPagerPanels(FragmentManager fm) {
+            Fragment[] fragments = new Fragment[panels.size()];
+            for (int i = 0; i < fragments.length; i++) {
+                TorBootstrapPagerConfig.TorBootstrapPanelConfig panelConfig = panels.get(i);
+
+                // Fragment tag is created as "android:switcher:" + viewId + ":" + id
+                // where |viewId| is the ID of the parent View container (in this case
+                // TorBootstrapPager is the parent View of the panels), and |id| is the
+                // position within the pager (in this case, it is |i| here)
+                // https://android.googlesource.com/platform/frameworks/support/+/refs/heads/marshmallow-release/v4/java/android/support/v4/app/FragmentPagerAdapter.java#172
+                String fragmentTag = "android:switcher:" + TorBootstrapPager.this.getId() + ":" + i;
+
+                // If the Activity is being restored, then find the existing fragment. If the
+                // fragment doesn't exist, then instantiate it.
+                fragments[i] = fm.findFragmentByTag(fragmentTag);
+                if (fragments[i] == null) {
+                    // We know the class is within the "org.mozilla.gecko.torbootstrap" package namespace
+                    fragments[i] = Fragment.instantiate(mActivity.getApplicationContext(), panelConfig.getClassname());
+                }
+
+                ((TorBootstrapPanel) fragments[i]).setPagerNavigation(pagerNavigation);
+                ((TorBootstrapPanel) fragments[i]).setContext(mActivity);
+                ((TorBootstrapPanel) fragments[i]).setBootstrapController(this);
+            }
+            return fragments;
+        }
+
+        @Override
+        public Fragment getItem(int i) {
+            return fragments[i];
+        }
+
+        @Override
+        public int getCount() {
+            return panels.size();
+        }
+
+        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..17454da91444
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/torbootstrap/TorBootstrapPagerConfig.java
@@ -0,0 +1,48 @@
+/* -*- 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.util.Log;
+import org.mozilla.gecko.GeckoSharedPrefs;
+
+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> getDefaultBootstrapPanel() {
+         final List<TorBootstrapPanelConfig> panels = new LinkedList<>();
+         panels.add(SimplePanelConfigs.bootstrapPanelConfig);
+         panels.add(SimplePanelConfigs.torLogPanelConfig);
+
+         return panels;
+    }
+
+    public static class TorBootstrapPanelConfig {
+
+        private String classname;
+
+        public TorBootstrapPanelConfig(String classname) {
+            this.classname = classname;
+        }
+
+        public String getClassname() {
+            return this.classname;
+        }
+    }
+
+    private static class SimplePanelConfigs {
+        public static final TorBootstrapPanelConfig bootstrapPanelConfig = new TorBootstrapPanelConfig(TorBootstrapPanel.class.getName());
+        public static final TorBootstrapPanelConfig torLogPanelConfig = new TorBootstrapPanelConfig(TorBootstrapLogPanel.class.getName());
+
+    }
+}
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..54b1c41b1a9f
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/torbootstrap/TorBootstrapPanel.java
@@ -0,0 +1,575 @@
+/* -*- 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.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.view.ViewTreeObserver;
+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;
+
+    private ViewTreeLayoutListener mViewTreeLayoutListener;
+
+    // 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) {
+                     // Stop the animation and terminate this thread if the main thread
+                     // set |mOnionAlphaChangerRunning| to |false| or if
+                     // getActivity() returns |null|.
+                     if (!mOnionAlphaChangerRunning || getActivity() == null) {
+                         // 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) {}
+            }
+        }
+    }
+
+    // Android tries scaling the image as a square. Create a modified ViewPort via padding
+    // top, left, right, and bottom such that the image aspect ratio is correct.
+    private void setOnionImgLayout() {
+        if (mRoot == null) {
+            Log.i(LOGTAG, "setOnionImgLayout: mRoot is null");
+            return;
+        }
+
+        ImageView onionImg = (ImageView) mRoot.findViewById(R.id.tor_bootstrap_onion);
+        if (onionImg == null) {
+            Log.i(LOGTAG, "setOnionImgLayout: onionImg is null");
+            return;
+        }
+
+        // Dimensions of the SVG. If the image is ever changed, update these values. The
+        // SVG viewport is 2dp wider due to clipping.
+        final double imgHeight = 289.;
+        final double imgWidth = 247.;
+
+        // Dimensions of the current ImageView
+        final int currentHeight = onionImg.getHeight();
+        final int currentWidth = onionImg.getWidth();
+
+        // If we only consider one dimension of the image, calculate the expected value
+        // of the other dimension (width vs. height).
+        final int expectedHeight = (int) (currentWidth*imgHeight/imgWidth);
+        final int expectedWidth = (int) (currentHeight*imgWidth/imgHeight);
+
+        // Set current values as default.
+        int newWidth = currentWidth;
+        int newHeight = currentHeight;
+
+        Log.d(LOGTAG, "Current Top=" + onionImg.getTop());
+        Log.d(LOGTAG, "Current Height=" + currentHeight);
+        Log.d(LOGTAG, "Current Width=" + currentWidth);
+        Log.d(LOGTAG, "Expected height=" + expectedHeight);
+        Log.d(LOGTAG, "Expected width=" + expectedWidth);
+
+        // Configure the width or height based on its expected value. This is based on
+        // the intuition that:
+        //   - If the device is in portrait mode, then the device's height is (likely)
+        //     greater than its width. When this is the case, then:
+        //         - The image's View object is likely using all available vertical area
+        //             (but the image is bounded by the width of the device due to
+        //             maintaining the scaling factor).
+        //         - However, the height and width of the graphic are equal (because
+        //             Android enforces this).
+        //         - The width should be less than the height (this is a property of
+        //             the image itself).
+        //         - The width should be proportional to the imgHeight and imgWidth
+        //             defined above.
+        //     Adjust the height when the current width is less than the expected width.
+        //     The width is the limiting-factor, therefore choose the height proportional
+        //     to the current width.
+        //
+        //   - The opposite is likely true when the device is in landscape mode with
+        //     respect to the height and width. Adjust the width when the height is less
+        //     than the expected height. The height is the limiting-factor, therefore
+        //     choose the width proportional to the current height.
+        //
+        // Subtract 1 from the expected value as a way of accounting for rounding
+        // error.
+        if (currentWidth < (expectedWidth - 1)) {
+            newHeight = expectedHeight;
+        } else if (currentHeight < (expectedHeight - 1)) {
+            newWidth = expectedWidth;
+        }
+
+        Log.d(LOGTAG, "New height=" + newHeight);
+        Log.d(LOGTAG, "New width=" + newWidth);
+
+        // Define the padding as the available space between the current height (as it
+        // is displayed to the user) and the new height (as it was calculated above).
+        int verticalPadding = currentHeight - newHeight;
+        int sidePadding = currentWidth - newWidth;
+        int leftPadding = 0;
+        int topPadding = 0;
+        int bottomPadding = 0;
+        int rightPadding = 0;
+
+        // If the width of the image is greater than 600dp, then cap it at 702x600 (HxW).
+        // Furthermore, if the width is "near" 600dp (within 100dp), then decrease the
+        // dimensions to 468x400 dp. This should "look" better on lower-resolution
+        // devices.
+        final int MAXIMUM_WIDTH = 600;
+        final int distanceFromMaxWidth = newWidth - MAXIMUM_WIDTH;
+        final boolean isNearMaxWidth = Math.abs(distanceFromMaxWidth) < 100;
+        if ((newWidth > MAXIMUM_WIDTH) || isNearMaxWidth) {
+            if (isNearMaxWidth) {
+                // If newWidth is near MAX_WIDTH, then add additional padding (therefore
+                // decreasing the width by an additional 200dp).
+                sidePadding += 200;
+            }
+
+            final int paddingSpaceAvailable = (distanceFromMaxWidth > 0) ? distanceFromMaxWidth : 0;
+            sidePadding += paddingSpaceAvailable;
+
+            final int newWidthWithoutPadding = currentWidth - sidePadding;
+
+            final int newHeightWithoutPadding = (int) (newWidthWithoutPadding*imgHeight/imgWidth);
+
+            Log.d(LOGTAG, "New width without padding=" + newWidthWithoutPadding);
+            Log.d(LOGTAG, "New height without padding=" + newHeightWithoutPadding);
+
+            verticalPadding = currentHeight - newHeightWithoutPadding;
+        }
+
+        Log.d(LOGTAG, "New top padding=" + verticalPadding);
+        Log.d(LOGTAG, "New side padding=" + sidePadding);
+
+        if (verticalPadding < 0) {
+            Log.i(LOGTAG, "vertical padding is " + verticalPadding);
+            verticalPadding = 0;
+        } else {
+            // Place 4/5 of padding at top, and 1/5 of padding at bottom.
+            topPadding = (verticalPadding*4)/5;
+            bottomPadding = verticalPadding/5;
+        }
+
+        if (sidePadding < 0) {
+            Log.i(LOGTAG, "side padding is " + sidePadding);
+            leftPadding = 0;
+            rightPadding = 0;
+        } else {
+            // Divide the padding equally on the left and right side.
+            leftPadding = sidePadding/2;
+            rightPadding = leftPadding;
+        }
+
+        // Create a padding-box around the image and let Android fill the box with
+        // the image. Android will scale the width and height independently, so the
+        // end result should be a correctly-sized graphic.
+        onionImg.setPadding(leftPadding, topPadding, rightPadding, bottomPadding);
+
+        // Separately scale x- and y-dimension.
+        onionImg.setScaleType(ImageView.ScaleType.FIT_XY);
+
+        // Invalidate the view because the image disappears (is not redrawn) sometimes when
+        // the screen is rotated.
+        onionImg.invalidate();
+    }
+
+    private class ViewTreeLayoutListener implements ViewTreeObserver.OnGlobalLayoutListener {
+        @Override
+        public void onGlobalLayout() {
+            TorBootstrapPanel.this.setOnionImgLayout();
+        }
+    }
+
+    @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();
+            }
+        });
+
+        if (Build.VERSION.SDK_INT > 20) {
+            // Round the button's edges, but only on API 21+. Earlier versions
+            // do not support this.
+            //
+            // 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);
+
+        // Add a callback for notification when the layout is complete and all components
+        // are measured. Waiting until the layout is complete is necessary before we correctly
+        // set the size of the onion. Cache the listener so we may remove it later.
+        mViewTreeLayoutListener = new ViewTreeLayoutListener();
+        mRoot.getViewTreeObserver().addOnGlobalLayoutListener(mViewTreeLayoutListener);
+
+        return mRoot;
+    }
+
+    @Override
+    public void onDestroyView() {
+        // Inform the background AlphaChanging thread it should terminate.
+        synchronized(mOnionAlphaChangerLock) {
+            mOnionAlphaChangerRunning = false;
+        }
+
+        super.onDestroyView();
+    }
+
+    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();
+
+            // Remove the listener when we're done
+            mRoot.getViewTreeObserver().removeOnGlobalLayoutListener(mViewTreeLayoutListener);
+        }
+    }
+
+    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 {
+                // The reference is not nulled in this case because another
+                // background thread would start otherwise. The thread is 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.
+                // 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;
+        }
+        // Start bootstrap process and 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);
+
+        Drawable drawableOnion = onionImg.getDrawable();
+
+        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();
+
+        // 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..9e74c49f3f91
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/torbootstrap/TorPreferences.java
@@ -0,0 +1,975 @@
+/* -*- 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.support.v7.app.ActionBar;
+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 (tor-android-service)
+ *   pref_bridges_list    (tor-android-service)
+ *
+ * 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 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=T1
+ *
+ *   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[] sTorPreferenceFragments = {TorPreferences.TorNetworkBridgesEnabledPreference.class.getName(),
+                                                  TorPreferences.TorNetworkBridgeSelectPreference.class.getName(),
+                                                  TorPreferences.TorNetworkBridgeProvidePreference.class.getName()};
+    // Current displayed PreferenceFragment
+    private TorNetworkPreferenceFragment mFrag;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        // Begin with the first (Enable Bridges) fragment
+        getIntent().putExtra(EXTRA_SHOW_FRAGMENT, TorPreferences.TorNetworkBridgesEnabledPreference.class.getName());
+        getIntent().putExtra(EXTRA_NO_HEADERS, true);
+        super.onCreate(savedInstanceState);
+
+        mFrag = null;
+    }
+
+    // Save the current preference when the app is minimized or swiped away.
+    @Override
+    public void onStop() {
+        if (mFrag != null) {
+            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() {
+        if (mFrag != null) {
+            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 != null && 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 onActivityCreated(Bundle savedInstanceState) {
+            super.onActivityCreated(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 tor-android-service's helper method and enable
+        // tor-android-service'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 tor-android service's preference
+            Prefs.setBridgesList(bridgesLines);
+
+            // If either of these are not null, then we're enabling bridges
+            boolean bridgesAreEnabled = (bridgesType != null) || (bridgesLines != null);
+            // Inform tor-android-service bridges are enabled
+            Prefs.putBridgesEnabled(bridgesAreEnabled);
+            return true;
+        }
+
+        // Disable the bridges.enabled Preference
+        protected void disableBridges(PreferenceFragment frag) {
+            if (frag == null) {
+                Log.w(LOGTAG, "disableBridges: frag is null");
+                return;
+            }
+
+            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 of the expected preferences are 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) {
+            ActionBar actionBar = mTorPrefAct.getSupportActionBar();
+
+            if (actionBar == null) {
+                Log.w(LOGTAG, "setTitle: actionBar is null");
+                return;
+            }
+
+            actionBar.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, "onClick: 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);
+
+            final SwitchPreference bridgesEnabled = (SwitchPreference) findPreference(PREFS_BRIDGES_ENABLED);
+            if (bridgesEnabled == null) {
+                Log.w(LOGTAG, "onViewCreated: 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");
+
+                    setTitle(R.string.pref_tor_network_title);
+
+                    // 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 tor-android-service 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 == null) {
+                Log.i(LOGTAG, "isBridgeProvided: bridgesEnabled is null");
+                return false;
+            }
+
+            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);
+
+            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) {
+                    setTitle(R.string.pref_tor_select_a_bridge_title);
+
+                    // 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"
+            // (tor-android-service 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);
+            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) {
+                    setTitle(R.string.pref_tor_provide_a_bridge_title);
+
+                    // 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) {
+                Log.i(LOGTAG, "bridgesLine2 is not null.");
+                if (bridgesLines != null) {
+                    // If bridgesLine1 was not null, then append a newline.
+                    bridgesLines += "\n" + bridgesLine2;
+                } else {
+                    bridgesLines = bridgesLine2;
+                }
+            }
+
+            if (bridgesLine3 != null) {
+                Log.i(LOGTAG, "bridgesLine3 is not null.");
+                if (bridgesLines != null) {
+                    // If bridgesLine1 or bridgesLine2 were not null, then append a newline.
+                    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) {
+                // 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) {
+                    Log.i(LOGTAG, "Custom bridges are empty. Disabling.");
+                    disableBridges(this);
+                }
+                return;
+            }
+
+            // Set the preferences (both our preference and
+            // tor-android-service'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 tbb-commits mailing list