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