[tor-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 tor-commits
mailing list