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