From 06eb4014837704e04d6ea8e9d7a69475a58c3c65 Mon Sep 17 00:00:00 2001 From: cyBerta Date: Fri, 23 Feb 2018 02:22:25 +0100 Subject: #8754 enable pause openvpn on device inactivity feature --- .../java/se/leap/bitmaskclient/ConfigHelper.java | 38 ++-- .../main/java/se/leap/bitmaskclient/Constants.java | 5 + .../main/java/se/leap/bitmaskclient/Dashboard.java | 0 .../leap/bitmaskclient/DrawerSettingsAdapter.java | 219 +++++++++++++++++++++ .../java/se/leap/bitmaskclient/EipFragment.java | 2 +- .../java/se/leap/bitmaskclient/MainActivity.java | 17 +- .../drawer/NavigationDrawerFragment.java | 149 ++++++++++++-- app/src/main/res/layout/switch_list_item.xml | 15 ++ app/src/main/res/values/strings.xml | 3 + 9 files changed, 417 insertions(+), 31 deletions(-) delete mode 100644 app/src/main/java/se/leap/bitmaskclient/Dashboard.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/DrawerSettingsAdapter.java create mode 100644 app/src/main/res/layout/switch_list_item.xml (limited to 'app/src') diff --git a/app/src/main/java/se/leap/bitmaskclient/ConfigHelper.java b/app/src/main/java/se/leap/bitmaskclient/ConfigHelper.java index a52df460..50d23106 100644 --- a/app/src/main/java/se/leap/bitmaskclient/ConfigHelper.java +++ b/app/src/main/java/se/leap/bitmaskclient/ConfigHelper.java @@ -16,7 +16,9 @@ */ package se.leap.bitmaskclient; +import android.content.Context; import android.content.SharedPreferences; +import android.preference.PreferenceManager; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.Log; @@ -52,6 +54,7 @@ import java.util.Locale; import java.util.Map; import static android.R.attr.name; +import static se.leap.bitmaskclient.Constants.DEFAULT_SHARED_PREFS_BATTERY_SAVER; import static se.leap.bitmaskclient.Constants.PREFERENCES_APP_VERSION; import static se.leap.bitmaskclient.Constants.PROVIDER_CONFIGURED; import static se.leap.bitmaskclient.Constants.PROVIDER_EIP_DEFINITION; @@ -383,6 +386,7 @@ public class ConfigHelper { clearDataOfLastProvider(preferences, false); } + @Deprecated public static void clearDataOfLastProvider(SharedPreferences preferences, boolean commit) { Map allEntries = preferences.getAll(); List lastProvidersKeys = new ArrayList<>(); @@ -410,18 +414,30 @@ public class ConfigHelper { } public static void deleteProviderDetailsFromPreferences(@NonNull SharedPreferences preferences, String providerDomain) { - preferences.edit(). - remove(Provider.KEY + "." + providerDomain). - remove(Provider.CA_CERT + "." + providerDomain). - remove(Provider.CA_CERT_FINGERPRINT + "." + providerDomain). - remove(Provider.MAIN_URL + "." + providerDomain). - remove(Provider.KEY + "." + providerDomain). - remove(Provider.CA_CERT + "." + providerDomain). - remove(PROVIDER_EIP_DEFINITION + "." + providerDomain). - remove(PROVIDER_PRIVATE_KEY + "." + providerDomain). - remove(PROVIDER_VPN_CERTIFICATE + "." + providerDomain). - apply(); + preferences.edit(). + remove(Provider.KEY + "." + providerDomain). + remove(Provider.CA_CERT + "." + providerDomain). + remove(Provider.CA_CERT_FINGERPRINT + "." + providerDomain). + remove(Provider.MAIN_URL + "." + providerDomain). + remove(Provider.KEY + "." + providerDomain). + remove(Provider.CA_CERT + "." + providerDomain). + remove(PROVIDER_EIP_DEFINITION + "." + providerDomain). + remove(PROVIDER_PRIVATE_KEY + "." + providerDomain). + remove(PROVIDER_VPN_CERTIFICATE + "." + providerDomain). + apply(); } + public static void saveBattery(Context context, boolean isEnabled) { + if (context == null) { + return; + } + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); + preferences.edit().putBoolean(DEFAULT_SHARED_PREFS_BATTERY_SAVER, isEnabled).apply(); + } + + public static boolean getSaveBattery(@NonNull Context context) { + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); + return preferences.getBoolean(DEFAULT_SHARED_PREFS_BATTERY_SAVER, false); + } } diff --git a/app/src/main/java/se/leap/bitmaskclient/Constants.java b/app/src/main/java/se/leap/bitmaskclient/Constants.java index 2b7a8113..58145015 100644 --- a/app/src/main/java/se/leap/bitmaskclient/Constants.java +++ b/app/src/main/java/se/leap/bitmaskclient/Constants.java @@ -81,4 +81,9 @@ public interface Constants { String BROADCAST_RESULT_CODE = "BROADCAST.RESULT_CODE"; String BROADCAST_RESULT_KEY = "BROADCAST.RESULT_KEY"; + + ////////////////////////////////////////////// + // ICS-OPENVPN CONSTANTS + ///////////////////////////////////////////// + String DEFAULT_SHARED_PREFS_BATTERY_SAVER = "screenoff"; } diff --git a/app/src/main/java/se/leap/bitmaskclient/Dashboard.java b/app/src/main/java/se/leap/bitmaskclient/Dashboard.java deleted file mode 100644 index e69de29b..00000000 diff --git a/app/src/main/java/se/leap/bitmaskclient/DrawerSettingsAdapter.java b/app/src/main/java/se/leap/bitmaskclient/DrawerSettingsAdapter.java new file mode 100644 index 00000000..867f3d48 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/DrawerSettingsAdapter.java @@ -0,0 +1,219 @@ +/** + * Copyright (c) 2018 LEAP Encryption Access Project and contributers + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package se.leap.bitmaskclient; + +import android.support.annotation.NonNull; +import android.support.v7.widget.SwitchCompat; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.CompoundButton; +import android.widget.TextView; + +import java.util.ArrayList; + +/** + * Created by cyberta on 21.02.18. + */ + +public class DrawerSettingsAdapter extends BaseAdapter { + + //item types + public static final int NONE = -1; + public static final int SWITCH_PROVIDER = 0; + public static final int LOG = 1; + public static final int ABOUT = 2; + public static final int BATTERY_SAVER = 3; + + //view types + public final static int VIEW_SIMPLE_TEXT = 0; + public final static int VIEW_SWITCH = 1; + + public static class DrawerSettingsItem { + private String description = ""; + private int viewType = VIEW_SIMPLE_TEXT; + private boolean isChecked = false; + private int itemType = NONE; + private CompoundButton.OnCheckedChangeListener callback; + + private DrawerSettingsItem(String description, int viewType, boolean isChecked, int itemType, CompoundButton.OnCheckedChangeListener callback) { + this.description = description; + this.viewType = viewType; + this.isChecked = isChecked; + this.itemType = itemType; + this.callback = callback; + } + + public static DrawerSettingsItem getSimpleTextInstance(String description, int itemType) { + return new DrawerSettingsItem(description, VIEW_SIMPLE_TEXT, false, itemType, null); + } + + public static DrawerSettingsItem getSwitchInstance(String description, boolean isChecked, int itemType, CompoundButton.OnCheckedChangeListener callback) { + return new DrawerSettingsItem(description, VIEW_SWITCH, isChecked, itemType, callback); + } + + public int getItemType() { + return itemType; + } + + public void setChecked(boolean checked) { + isChecked = checked; + } + + public boolean isChecked() { + return isChecked; + } + } + + private ArrayList mData = new ArrayList<>(); + private LayoutInflater mInflater; + + public DrawerSettingsAdapter(LayoutInflater layoutInflater) { + mInflater = layoutInflater; + } + + public void addItem(final DrawerSettingsItem item) { + mData.add(item); + notifyDataSetChanged(); + } + + @Override + public int getItemViewType(int position) { + DrawerSettingsItem item = mData.get(position); + return item.viewType; + } + + @Override + public int getViewTypeCount() { + boolean hasSwitchItem = false; + for (DrawerSettingsItem item : mData) { + if (item.viewType == VIEW_SWITCH) { + hasSwitchItem = true; + break; + } + } + return hasSwitchItem ? 2 : 1; + } + + @Override + public int getCount() { + return mData.size(); + } + + @Override + public DrawerSettingsItem getItem(int position) { + return mData.get(position); + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + + DrawerSettingsItem drawerSettingsItem = mData.get(position); + ViewHolder holder = null; + int type = getItemViewType(position); + if (convertView == null) { + holder = new ViewHolder(); + switch(type) { + case VIEW_SIMPLE_TEXT: + convertView = initTextViewBinding(holder); + holder.textView.setText(drawerSettingsItem.description); + break; + case VIEW_SWITCH: + convertView = initSwitchBinding(holder); + bindSwitch(drawerSettingsItem, holder); + break; + } + convertView.setTag(holder); + } else { + holder = (ViewHolder)convertView.getTag(); + switch (type) { + case VIEW_SIMPLE_TEXT: + if (holder.isSwitchViewHolder()) { + holder.resetSwitchView(); + convertView = initTextViewBinding(holder); + } + holder.textView.setText(drawerSettingsItem.description); + break; + case VIEW_SWITCH: + if (!holder.isSwitchViewHolder()) { + holder.resetTextView(); + convertView = initSwitchBinding(holder); + } + bindSwitch(drawerSettingsItem, holder); + break; + } + convertView.setTag(holder); + } + return convertView; + } + + private void bindSwitch(DrawerSettingsItem drawerSettingsItem, ViewHolder holder) { + holder.switchView.setChecked(drawerSettingsItem.isChecked); + holder.switchView.setText(drawerSettingsItem.description); + holder.switchView.setOnCheckedChangeListener(drawerSettingsItem.callback); + } + + @NonNull + private View initSwitchBinding(ViewHolder holder) { + View convertView = mInflater.inflate(R.layout.switch_list_item, null); + holder.switchView = convertView.findViewById(android.R.id.text1); + return convertView; + } + + @NonNull + private View initTextViewBinding(ViewHolder holder) { + View convertView = mInflater.inflate(R.layout.single_list_item, null); + holder.textView = convertView.findViewById(android.R.id.text1); + return convertView; + } + + public DrawerSettingsItem getDrawerItem(int elementType) { + for (DrawerSettingsItem item : mData) { + if (item.itemType == elementType) { + return item; + } + } + return null; + } + + static class ViewHolder { + TextView textView; + SwitchCompat switchView; + + boolean isSwitchViewHolder() { + return switchView != null; + } + + void resetSwitchView() { + switchView.setOnCheckedChangeListener(null); + switchView = null; + } + + void resetTextView() { + textView = null; + } + } +} + + + diff --git a/app/src/main/java/se/leap/bitmaskclient/EipFragment.java b/app/src/main/java/se/leap/bitmaskclient/EipFragment.java index fb57aea8..7b84657c 100644 --- a/app/src/main/java/se/leap/bitmaskclient/EipFragment.java +++ b/app/src/main/java/se/leap/bitmaskclient/EipFragment.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2013 LEAP Encryption Access Project and contributers + * Copyright (c) 2018 LEAP Encryption Access Project and contributers * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/app/src/main/java/se/leap/bitmaskclient/MainActivity.java b/app/src/main/java/se/leap/bitmaskclient/MainActivity.java index 6e778309..952f2d1f 100644 --- a/app/src/main/java/se/leap/bitmaskclient/MainActivity.java +++ b/app/src/main/java/se/leap/bitmaskclient/MainActivity.java @@ -1,3 +1,19 @@ +/** + * Copyright (c) 2018 LEAP Encryption Access Project and contributers + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ package se.leap.bitmaskclient; @@ -20,7 +36,6 @@ import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; import android.util.Log; -import org.jetbrains.annotations.NotNull; import org.json.JSONException; import org.json.JSONObject; diff --git a/app/src/main/java/se/leap/bitmaskclient/drawer/NavigationDrawerFragment.java b/app/src/main/java/se/leap/bitmaskclient/drawer/NavigationDrawerFragment.java index 772140b0..ebe5783a 100644 --- a/app/src/main/java/se/leap/bitmaskclient/drawer/NavigationDrawerFragment.java +++ b/app/src/main/java/se/leap/bitmaskclient/drawer/NavigationDrawerFragment.java @@ -1,6 +1,24 @@ +/** + * Copyright (c) 2018 LEAP Encryption Access Project and contributers + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ package se.leap.bitmaskclient.drawer; +import android.app.Activity; +import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.Configuration; @@ -11,6 +29,7 @@ import android.support.v4.view.GravityCompat; import android.support.v4.widget.DrawerLayout; import android.support.v7.app.ActionBar; import android.support.v7.app.ActionBarDrawerToggle; +import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; import android.util.Log; @@ -22,10 +41,13 @@ import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.ArrayAdapter; +import android.widget.CompoundButton; import android.widget.ListView; import android.widget.Toast; import se.leap.bitmaskclient.ConfigHelper; +import se.leap.bitmaskclient.DrawerSettingsAdapter; +import se.leap.bitmaskclient.DrawerSettingsAdapter.DrawerSettingsItem; import se.leap.bitmaskclient.Provider; import se.leap.bitmaskclient.ProviderListActivity; import se.leap.bitmaskclient.EipFragment; @@ -35,9 +57,19 @@ import se.leap.bitmaskclient.fragments.LogFragment; import static android.content.Context.MODE_PRIVATE; import static se.leap.bitmaskclient.BitmaskApp.getRefWatcher; +import static se.leap.bitmaskclient.ConfigHelper.getSaveBattery; import static se.leap.bitmaskclient.Constants.PROVIDER_KEY; import static se.leap.bitmaskclient.Constants.REQUEST_CODE_SWITCH_PROVIDER; import static se.leap.bitmaskclient.Constants.SHARED_PREFERENCES; +import static se.leap.bitmaskclient.DrawerSettingsAdapter.ABOUT; +import static se.leap.bitmaskclient.DrawerSettingsAdapter.BATTERY_SAVER; +import static se.leap.bitmaskclient.DrawerSettingsAdapter.DrawerSettingsItem.getSimpleTextInstance; +import static se.leap.bitmaskclient.DrawerSettingsAdapter.DrawerSettingsItem.getSwitchInstance; +import static se.leap.bitmaskclient.DrawerSettingsAdapter.LOG; +import static se.leap.bitmaskclient.DrawerSettingsAdapter.SWITCH_PROVIDER; +import static se.leap.bitmaskclient.R.string.about_fragment_title; +import static se.leap.bitmaskclient.R.string.log_fragment_title; +import static se.leap.bitmaskclient.R.string.switch_provider_menu_option; /** * Fragment used for managing interactions for and presentation of a navigation drawer. @@ -63,6 +95,7 @@ public class NavigationDrawerFragment extends Fragment { private ListView mDrawerAccountsListView; private View mFragmentContainerView; private ArrayAdapter accountListAdapter; + private DrawerSettingsAdapter settingsListAdapter; private boolean mFromSavedInstanceState; private boolean mUserLearnedDrawer; @@ -71,17 +104,23 @@ public class NavigationDrawerFragment extends Fragment { private SharedPreferences preferences; + private final static String KEY_SHOW_ENABLE_EXPERIMENTAL_FEATURE = "KEY_SHOW_ENABLE_EXPERIMENTAL_FEATURE"; + private boolean showEnableExperimentalFeature = false; + AlertDialog alertDialog; + public NavigationDrawerFragment() { } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - // Read in the flag indicating whether or not the user has demonstrated awareness of the // drawer. See PREF_USER_LEARNED_DRAWER for details. preferences = getContext().getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE); mUserLearnedDrawer = preferences.getBoolean(PREF_USER_LEARNED_DRAWER, false); + if (savedInstanceState != null) { + mFromSavedInstanceState = true; + } } @Override @@ -95,7 +134,9 @@ public class NavigationDrawerFragment extends Fragment { public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { mDrawerView = inflater.inflate(R.layout.drawer_main, container, false); + restoreFromSavedInstance(savedInstanceState); return mDrawerView; + } public boolean isDrawerOpen() { @@ -119,16 +160,23 @@ public class NavigationDrawerFragment extends Fragment { selectItem(parent, position); } }); + settingsListAdapter = new DrawerSettingsAdapter(getLayoutInflater()); + if (getContext() != null) { + settingsListAdapter.addItem(getSwitchInstance(getString(R.string.save_battery), + getSaveBattery(getContext()), + BATTERY_SAVER, + new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean newStateIsChecked) { + onSwitchItemSelected(BATTERY_SAVER, newStateIsChecked); + } + })); + } + settingsListAdapter.addItem(getSimpleTextInstance(getString(switch_provider_menu_option), SWITCH_PROVIDER)); + settingsListAdapter.addItem(getSimpleTextInstance(getString(log_fragment_title), LOG)); + settingsListAdapter.addItem(getSimpleTextInstance(getString(about_fragment_title), ABOUT)); - mDrawerSettingsListView.setAdapter(new ArrayAdapter( - actionBar.getThemedContext(), - R.layout.single_list_item, - android.R.id.text1, - new String[]{ - getString(R.string.switch_provider_menu_option), - getString(R.string.log_fragment_title), - getString(R.string.about_fragment_title), - })); + mDrawerSettingsListView.setAdapter(settingsListAdapter); mDrawerAccountsListView = mDrawerView.findViewById(R.id.accountList); mDrawerAccountsListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @@ -215,12 +263,55 @@ public class NavigationDrawerFragment extends Fragment { if (mDrawerLayout != null) { mDrawerLayout.closeDrawer(mFragmentContainerView); } - onNavigationDrawerItemSelected(list, position); + onTextItemSelected(list, position); } @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); + if (showEnableExperimentalFeature) { + outState.putBoolean(KEY_SHOW_ENABLE_EXPERIMENTAL_FEATURE, true); + alertDialog.dismiss(); + } + } + + private void restoreFromSavedInstance(Bundle savedInstanceState) { + if (savedInstanceState != null && savedInstanceState.containsKey(KEY_SHOW_ENABLE_EXPERIMENTAL_FEATURE)) { + showEnableExperimentalFeature = true; + showExperimentalFeatureAlert(); + } + } + + private void showExperimentalFeatureAlert() { + Activity activity = getActivity(); + if (activity == null) { + return; + } + + AlertDialog.Builder alertBuilder = new AlertDialog.Builder(getActivity()); + showEnableExperimentalFeature = true; + alertDialog = alertBuilder.setTitle(activity.getString(R.string.experimental_feature_title)) + .setMessage(activity.getString(R.string.experimental_feature_message)) + .setPositiveButton((android.R.string.yes), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + DrawerSettingsItem item = settingsListAdapter.getDrawerItem(BATTERY_SAVER); + item.setChecked(true); + settingsListAdapter.notifyDataSetChanged(); + ConfigHelper.saveBattery(getContext(), item.isChecked()); + } + }) + .setNegativeButton(activity.getString(android.R.string.no), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + disableSwitch(BATTERY_SAVER); + } + }).setOnDismissListener(new DialogInterface.OnDismissListener() { + @Override + public void onDismiss(DialogInterface dialog) { + showEnableExperimentalFeature = false; + } + }).show(); } @Override @@ -272,7 +363,28 @@ public class NavigationDrawerFragment extends Fragment { return ((AppCompatActivity) getActivity()).getSupportActionBar(); } - public void onNavigationDrawerItemSelected(AdapterView parent, int position) { + private void onSwitchItemSelected(int elementType, boolean newStateIsChecked) { + switch (elementType) { + case BATTERY_SAVER: + if (newStateIsChecked) { + showExperimentalFeatureAlert(); + } else { + ConfigHelper.saveBattery(this.getContext(), false); + disableSwitch(BATTERY_SAVER); + } + break; + default: + break; + } + } + + private void disableSwitch(int elementType) { + DrawerSettingsItem item = settingsListAdapter.getDrawerItem(elementType); + item.setChecked(false); + settingsListAdapter.notifyDataSetChanged(); + } + + public void onTextItemSelected(AdapterView parent, int position) { // update the main content by replacing fragments FragmentManager fragmentManager = getFragmentManager(); Fragment fragment = null; @@ -286,16 +398,17 @@ public class NavigationDrawerFragment extends Fragment { fragment.setArguments(arguments); } else { Log.d("Drawer", String.format("Selected position %d", position)); - switch (position) { - case 0: + DrawerSettingsItem settingsItem = settingsListAdapter.getItem(position); + switch (settingsItem.getItemType()) { + case SWITCH_PROVIDER: getActivity().startActivityForResult(new Intent(getActivity(), ProviderListActivity.class), REQUEST_CODE_SWITCH_PROVIDER); break; - case 1: - mTitle = getString(R.string.log_fragment_title); + case LOG: + mTitle = getString(log_fragment_title); fragment = new LogFragment(); break; - case 2: - mTitle = getString(R.string.about_fragment_title); + case ABOUT: + mTitle = getString(about_fragment_title); fragment = new AboutFragment(); break; default: diff --git a/app/src/main/res/layout/switch_list_item.xml b/app/src/main/res/layout/switch_list_item.xml new file mode 100644 index 00000000..bdb9a74c --- /dev/null +++ b/app/src/main/res/layout/switch_list_item.xml @@ -0,0 +1,15 @@ + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2ea4670a..a69c2420 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -107,4 +107,7 @@ Downloading the VPN certificate failed. Try again or choose another provider. VPN certificate is invalid. Try to download a new one. The VPN certificate is invalid. Please log in to download a new one. + Save battery + Save Battery Warning + Background data connections will hibernate when your phone is inactive. This feature is still experimental. -- cgit v1.2.3