From 6b032b751324a30120cfaabe88940f95171df11f Mon Sep 17 00:00:00 2001 From: cyBerta Date: Tue, 29 Dec 2020 00:54:08 +0100 Subject: new year cleanup: restructure messy project --- .../base/fragments/AboutFragment.java | 67 +++ .../base/fragments/AlwaysOnDialog.java | 76 +++ .../base/fragments/DonationReminderDialog.java | 120 ++++ .../bitmaskclient/base/fragments/EipFragment.java | 608 +++++++++++++++++++++ .../base/fragments/ExcludeAppsFragment.java | 335 ++++++++++++ .../bitmaskclient/base/fragments/LogFragment.java | 587 ++++++++++++++++++++ .../base/fragments/MainActivityErrorDialog.java | 174 ++++++ .../base/fragments/TetheringDialog.java | 258 +++++++++ 8 files changed, 2225 insertions(+) create mode 100644 app/src/main/java/se/leap/bitmaskclient/base/fragments/AboutFragment.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/base/fragments/AlwaysOnDialog.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/base/fragments/DonationReminderDialog.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/base/fragments/EipFragment.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/base/fragments/ExcludeAppsFragment.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/base/fragments/LogFragment.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/base/fragments/MainActivityErrorDialog.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/base/fragments/TetheringDialog.java (limited to 'app/src/main/java/se/leap/bitmaskclient/base/fragments') diff --git a/app/src/main/java/se/leap/bitmaskclient/base/fragments/AboutFragment.java b/app/src/main/java/se/leap/bitmaskclient/base/fragments/AboutFragment.java new file mode 100644 index 00000000..d901ba68 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/base/fragments/AboutFragment.java @@ -0,0 +1,67 @@ +package se.leap.bitmaskclient.base.fragments; + +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager.NameNotFoundException; +import android.os.Bundle; +import androidx.fragment.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import butterknife.ButterKnife; +import butterknife.InjectView; +import se.leap.bitmaskclient.BuildConfig; +import se.leap.bitmaskclient.R; + +import static android.view.View.VISIBLE; + +public class AboutFragment extends Fragment { + + final public static String TAG = AboutFragment.class.getSimpleName(); + final public static int VIEWED = 0; + + @InjectView(R.id.version) + TextView versionTextView; + + @InjectView(R.id.terms_of_service) + TextView termsOfService; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.f_about, container, false); + ButterKnife.inject(this, view); + return view; + } + + @Override + public void onStart() { + super.onStart(); + String version; + String name = "Bitmask"; + try { + PackageInfo packageinfo = getActivity().getPackageManager().getPackageInfo( + getActivity().getPackageName(), 0); + version = packageinfo.versionName; + name = getString(R.string.app_name); + } catch (NameNotFoundException e) { + version = "error fetching version"; + } + + versionTextView.setText(getString(R.string.version_info, name, version)); + + if (BuildConfig.FLAVOR_branding.equals("custom") && hasTermsOfServiceResource()) { + termsOfService.setText(getString(getTermsOfServiceResource())); + termsOfService.setVisibility(VISIBLE); + } + } + + private boolean hasTermsOfServiceResource() { + return getTermsOfServiceResource() != 0; + } + + private int getTermsOfServiceResource() { + return this.getContext().getResources().getIdentifier("terms_of_service", "string", this.getContext().getPackageName()); + } + +} diff --git a/app/src/main/java/se/leap/bitmaskclient/base/fragments/AlwaysOnDialog.java b/app/src/main/java/se/leap/bitmaskclient/base/fragments/AlwaysOnDialog.java new file mode 100644 index 00000000..a8034e1a --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/base/fragments/AlwaysOnDialog.java @@ -0,0 +1,76 @@ +package se.leap.bitmaskclient.base.fragments; + +import android.app.Dialog; +import android.content.Intent; +import android.os.Build; +import android.os.Bundle; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatDialogFragment; +import androidx.appcompat.widget.AppCompatTextView; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.CheckBox; + +import butterknife.ButterKnife; +import butterknife.InjectView; +import se.leap.bitmaskclient.R; +import se.leap.bitmaskclient.base.views.IconTextView; + +import static se.leap.bitmaskclient.base.utils.PreferenceHelper.saveShowAlwaysOnDialog; + + +/** + * Created by cyberta on 25.02.18. + */ + + + +public class AlwaysOnDialog extends AppCompatDialogFragment { + + public final static String TAG = AlwaysOnDialog.class.getName(); + + @InjectView(R.id.do_not_show_again) + CheckBox doNotShowAgainCheckBox; + + @InjectView(R.id.user_message) + IconTextView userMessage; + + @InjectView(R.id.block_vpn_user_message) + AppCompatTextView blockVpnUserMessage; + + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + LayoutInflater inflater = getActivity().getLayoutInflater(); + View view = inflater.inflate(R.layout.d_checkbox_confirm, null); + ButterKnife.inject(this, view); + + userMessage.setIcon(R.drawable.ic_settings); + userMessage.setText(getString(R.string.always_on_vpn_user_message)); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + blockVpnUserMessage.setVisibility(View.VISIBLE); + } + + builder.setView(view) + .setPositiveButton(android.R.string.ok, (dialog, id) -> { + if (doNotShowAgainCheckBox.isChecked()) { + saveShowAlwaysOnDialog(getContext(), false); + } + Intent intent = new Intent("android.net.vpn.SETTINGS"); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + }) + .setNegativeButton(R.string.cancel, (dialog, id) -> dialog.cancel()); + return builder.create(); + } +} diff --git a/app/src/main/java/se/leap/bitmaskclient/base/fragments/DonationReminderDialog.java b/app/src/main/java/se/leap/bitmaskclient/base/fragments/DonationReminderDialog.java new file mode 100644 index 00000000..0277933c --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/base/fragments/DonationReminderDialog.java @@ -0,0 +1,120 @@ +package se.leap.bitmaskclient.base.fragments; + +import android.app.Dialog; +import android.content.ActivityNotFoundException; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatDialogFragment; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.Button; + +import java.text.ParseException; + +import butterknife.ButterKnife; +import butterknife.InjectView; +import se.leap.bitmaskclient.R; +import se.leap.bitmaskclient.base.utils.DateHelper; +import se.leap.bitmaskclient.base.utils.PreferenceHelper; + +import static se.leap.bitmaskclient.base.models.Constants.DONATION_REMINDER_DURATION; +import static se.leap.bitmaskclient.base.models.Constants.DONATION_URL; +import static se.leap.bitmaskclient.base.models.Constants.ENABLE_DONATION; +import static se.leap.bitmaskclient.base.models.Constants.ENABLE_DONATION_REMINDER; +import static se.leap.bitmaskclient.base.models.Constants.FIRST_TIME_USER_DATE; +import static se.leap.bitmaskclient.base.models.Constants.LAST_DONATION_REMINDER_DATE; + +public class DonationReminderDialog extends AppCompatDialogFragment { + + public final static String TAG = DonationReminderDialog.class.getName(); + private static boolean isShown = false; + + @InjectView(R.id.btnDonate) + Button btnDonate; + + @InjectView(R.id.btnLater) + Button btnLater; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + LayoutInflater inflater = getActivity().getLayoutInflater(); + View view = inflater.inflate(R.layout.donation_reminder_dialog, null); + ButterKnife.inject(this, view); + isShown = true; + + builder.setView(view); + btnDonate.setOnClickListener(v -> { + Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(DONATION_URL)); + try { + startActivity(browserIntent); + } catch (ActivityNotFoundException e) { + e.printStackTrace(); + } + PreferenceHelper.putString(getContext(), LAST_DONATION_REMINDER_DATE, + DateHelper.getCurrentDateString()); + dismiss(); + }); + btnLater.setOnClickListener(v -> { + PreferenceHelper.putString(getContext(), LAST_DONATION_REMINDER_DATE, + DateHelper.getCurrentDateString()); + dismiss(); + }); + + return builder.create(); + } + + public static boolean isCallable(Context context) { + if (isShown) { + return false; + } + + if (!ENABLE_DONATION || !ENABLE_DONATION_REMINDER) { + return false; + } + + if (context == null) { + Log.e(TAG, "context is null!"); + return false; + } + + String firstTimeUserDate = PreferenceHelper.getString(context, FIRST_TIME_USER_DATE, null); + if (firstTimeUserDate == null) { + PreferenceHelper.putString(context, FIRST_TIME_USER_DATE, DateHelper.getCurrentDateString()); + return false; + } + + try { + long diffDays; + + diffDays = DateHelper.getDateDiffToCurrentDateInDays(firstTimeUserDate); + if (diffDays < 1) { + return false; + } + + String lastDonationReminderDate = PreferenceHelper.getString(context, LAST_DONATION_REMINDER_DATE, null); + if (lastDonationReminderDate == null) { + return true; + } + diffDays = DateHelper.getDateDiffToCurrentDateInDays(lastDonationReminderDate); + return diffDays >= DONATION_REMINDER_DURATION; + + } catch (ParseException e) { + e.printStackTrace(); + Log.e(TAG, e.getMessage()); + return false; + } + } +} diff --git a/app/src/main/java/se/leap/bitmaskclient/base/fragments/EipFragment.java b/app/src/main/java/se/leap/bitmaskclient/base/fragments/EipFragment.java new file mode 100644 index 00000000..9544fb1e --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/base/fragments/EipFragment.java @@ -0,0 +1,608 @@ +/** + * 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.base.fragments; + +import android.app.Activity; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.content.SharedPreferences; +import android.graphics.ColorMatrix; +import android.graphics.ColorMatrixColorFilter; +import android.os.Bundle; +import android.os.IBinder; +import android.os.Vibrator; +import android.text.TextUtils; +import android.util.Log; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.widget.AppCompatButton; +import androidx.appcompat.widget.AppCompatImageView; +import androidx.appcompat.widget.AppCompatTextView; +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentTransaction; + +import java.util.Observable; +import java.util.Observer; + +import butterknife.ButterKnife; +import butterknife.InjectView; +import butterknife.OnClick; +import de.blinkt.openvpn.core.IOpenVPNServiceInternal; +import de.blinkt.openvpn.core.OpenVPNService; +import de.blinkt.openvpn.core.VpnStatus; +import se.leap.bitmaskclient.providersetup.ProviderListActivity; +import se.leap.bitmaskclient.R; +import se.leap.bitmaskclient.base.FragmentManagerEnhanced; +import se.leap.bitmaskclient.base.models.Provider; +import se.leap.bitmaskclient.base.models.ProviderObservable; +import se.leap.bitmaskclient.base.views.VpnStateImage; +import se.leap.bitmaskclient.eip.EipCommand; +import se.leap.bitmaskclient.eip.EipStatus; +import se.leap.bitmaskclient.providersetup.ProviderAPICommand; +import se.leap.bitmaskclient.providersetup.activities.CustomProviderSetupActivity; +import se.leap.bitmaskclient.providersetup.activities.LoginActivity; +import se.leap.bitmaskclient.providersetup.models.LeapSRPSession; + +import static android.view.View.GONE; +import static android.view.View.VISIBLE; +import static de.blinkt.openvpn.core.ConnectionStatus.LEVEL_NONETWORK; +import static se.leap.bitmaskclient.R.string.vpn_certificate_user_message; +import static se.leap.bitmaskclient.base.models.Constants.ASK_TO_CANCEL_VPN; +import static se.leap.bitmaskclient.base.models.Constants.EIP_ACTION_START; +import static se.leap.bitmaskclient.base.models.Constants.EIP_EARLY_ROUTES; +import static se.leap.bitmaskclient.base.models.Constants.EIP_RESTART_ON_BOOT; +import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_KEY; +import static se.leap.bitmaskclient.base.models.Constants.REQUEST_CODE_CONFIGURE_LEAP; +import static se.leap.bitmaskclient.base.models.Constants.REQUEST_CODE_LOG_IN; +import static se.leap.bitmaskclient.base.models.Constants.REQUEST_CODE_SWITCH_PROVIDER; +import static se.leap.bitmaskclient.base.models.Constants.SHARED_PREFERENCES; +import static se.leap.bitmaskclient.base.utils.ConfigHelper.isDefaultBitmask; +import static se.leap.bitmaskclient.base.utils.ViewHelper.convertDimensionToPx; +import static se.leap.bitmaskclient.eip.EipSetupObserver.connectionRetry; +import static se.leap.bitmaskclient.eip.EipSetupObserver.gatewayOrder; +import static se.leap.bitmaskclient.eip.EipSetupObserver.reconnectingWithDifferentGateway; +import static se.leap.bitmaskclient.providersetup.ProviderAPI.DOWNLOAD_GEOIP_JSON; +import static se.leap.bitmaskclient.providersetup.ProviderAPI.UPDATE_INVALID_VPN_CERTIFICATE; +import static se.leap.bitmaskclient.providersetup.ProviderAPI.USER_MESSAGE; + +public class EipFragment extends Fragment implements Observer { + + public final static String TAG = EipFragment.class.getSimpleName(); + + + private SharedPreferences preferences; + private Provider provider; + + @InjectView(R.id.background) + AppCompatImageView background; + + @InjectView(R.id.vpn_state_image) + VpnStateImage vpnStateImage; + + @InjectView(R.id.vpn_main_button) + AppCompatButton mainButton; + + @InjectView(R.id.routed_text) + AppCompatTextView routedText; + + @InjectView(R.id.vpn_route) + AppCompatTextView vpnRoute; + + + + private EipStatus eipStatus; + + //---saved Instance ------- + private final String KEY_SHOW_PENDING_START_CANCELLATION = "KEY_SHOW_PENDING_START_CANCELLATION"; + private final String KEY_SHOW_ASK_TO_STOP_EIP = "KEY_SHOW_ASK_TO_STOP_EIP"; + private boolean showPendingStartCancellation = false; + private boolean showAskToStopEip = false; + //------------------------ + AlertDialog alertDialog; + + private IOpenVPNServiceInternal mService; + private ServiceConnection openVpnConnection; + + @Override + public void onAttach(Context context) { + super.onAttach(context); + Bundle arguments = getArguments(); + Activity activity = getActivity(); + if (activity != null) { + if (arguments != null) { + provider = arguments.getParcelable(PROVIDER_KEY); + if (provider == null) { + handleNoProvider(activity); + } else { + Log.d(TAG, provider.getName() + " configured as provider"); + } + } else { + handleNoProvider(activity); + } + } + } + + private void handleNoProvider(Activity activity) { + if (isDefaultBitmask()) { + activity.startActivityForResult(new Intent(activity, ProviderListActivity.class), REQUEST_CODE_SWITCH_PROVIDER); + } else { + Log.e(TAG, "no provider given - try to reconfigure custom provider"); + startActivityForResult(new Intent(activity, CustomProviderSetupActivity.class), REQUEST_CODE_CONFIGURE_LEAP); + + } + + } + + + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + openVpnConnection = new EipFragmentServiceConnection(); + eipStatus = EipStatus.getInstance(); + Activity activity = getActivity(); + if (activity != null) { + preferences = getActivity().getSharedPreferences(SHARED_PREFERENCES, Context.MODE_PRIVATE); + } else { + Log.e(TAG, "activity is null in onCreate - no preferences set!"); + } + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + eipStatus.addObserver(this); + View view = inflater.inflate(R.layout.f_eip, container, false); + ButterKnife.inject(this, view); + + Bundle arguments = getArguments(); + if (arguments != null && arguments.containsKey(ASK_TO_CANCEL_VPN) && arguments.getBoolean(ASK_TO_CANCEL_VPN)) { + arguments.remove(ASK_TO_CANCEL_VPN); + setArguments(arguments); + askToStopEIP(); + } + restoreFromSavedInstance(savedInstanceState); + return view; + } + + @Override + public void onStart() { + super.onStart(); + if (DonationReminderDialog.isCallable(getContext())) { + showDonationReminderDialog(); + } + } + + @Override + public void onResume() { + super.onResume(); + //FIXME: avoid race conditions while checking certificate an logging in at about the same time + //eipCommand(Constants.EIP_ACTION_CHECK_CERT_VALIDITY); + bindOpenVpnService(); + handleNewState(); + } + + @Override + public void onPause() { + super.onPause(); + + Activity activity = getActivity(); + if (activity != null) { + getActivity().unbindService(openVpnConnection); + } + } + + @Override + public void onSaveInstanceState(@NonNull Bundle outState) { + super.onSaveInstanceState(outState); + if (showAskToStopEip) { + outState.putBoolean(KEY_SHOW_ASK_TO_STOP_EIP, true); + alertDialog.dismiss(); + } else if (showPendingStartCancellation) { + outState.putBoolean(KEY_SHOW_PENDING_START_CANCELLATION, true); + alertDialog.dismiss(); + } + } + + private void restoreFromSavedInstance(Bundle savedInstanceState) { + if (savedInstanceState != null && savedInstanceState.containsKey(KEY_SHOW_PENDING_START_CANCELLATION)) { + showPendingStartCancellation = true; + askPendingStartCancellation(); + } else if (savedInstanceState != null && savedInstanceState.containsKey(KEY_SHOW_ASK_TO_STOP_EIP)) { + showAskToStopEip = true; + askToStopEIP(); + } + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + eipStatus.deleteObserver(this); + } + + private void saveStatus(boolean restartOnBoot) { + preferences.edit().putBoolean(EIP_RESTART_ON_BOOT, restartOnBoot).apply(); + } + + @OnClick(R.id.vpn_main_button) + void onButtonClick() { + handleIcon(); + } + + @OnClick(R.id.vpn_state_image) + void onVpnStateImageClick() { + handleIcon(); + } + + void handleIcon() { + if (isOpenVpnRunningWithoutNetwork() || eipStatus.isConnected() || eipStatus.isConnecting()) + handleSwitchOff(); + else + handleSwitchOn(); + } + + private void handleSwitchOn() { + Context context = getContext(); + if (context == null) { + Log.e(TAG, "context is null when switch turning on"); + return; + } + + if (canStartEIP()) { + startEipFromScratch(); + } else if (canLogInToStartEIP()) { + askUserToLogIn(getString(vpn_certificate_user_message)); + } else { + // provider has no VpnCertificate but user is logged in + updateInvalidVpnCertificate(); + } + } + + private boolean canStartEIP() { + boolean certificateExists = provider.hasVpnCertificate(); + boolean isAllowedAnon = provider.allowsAnonymous(); + return (isAllowedAnon || certificateExists) && !eipStatus.isConnected() && !eipStatus.isConnecting(); + } + + private boolean canLogInToStartEIP() { + boolean isAllowedRegistered = provider.allowsRegistered(); + boolean isLoggedIn = LeapSRPSession.loggedIn(); + return isAllowedRegistered && !isLoggedIn && !eipStatus.isConnecting() && !eipStatus.isConnected(); + } + + private void handleSwitchOff() { + if (isOpenVpnRunningWithoutNetwork() || eipStatus.isConnecting()) { + askPendingStartCancellation(); + } else if (eipStatus.isConnected()) { + askToStopEIP(); + } + } + + private void setMainButtonEnabled(boolean enabled) { + mainButton.setEnabled(enabled); + vpnStateImage.setEnabled(enabled); + } + + public void startEipFromScratch() { + saveStatus(true); + Context context = getContext(); + if (context == null) { + Log.e(TAG, "context is null when trying to start VPN"); + return; + } + if (!provider.getGeoipUrl().isDefault() && provider.shouldUpdateGeoIpJson()) { + Bundle bundle = new Bundle(); + bundle.putBoolean(EIP_ACTION_START, true); + bundle.putBoolean(EIP_EARLY_ROUTES, false); + ProviderAPICommand.execute(getContext().getApplicationContext(), DOWNLOAD_GEOIP_JSON, bundle, provider); + } else { + EipCommand.startVPN(context.getApplicationContext(), false); + } + vpnStateImage.showProgress(); + routedText.setVisibility(GONE); + vpnRoute.setVisibility(GONE); + colorBackgroundALittle(); + } + + protected void stopEipIfPossible() { + Context context = getContext(); + if (context == null) { + Log.e(TAG, "context is null when trying to stop EIP"); + return; + } + EipCommand.stopVPN(context.getApplicationContext()); + } + + private void askPendingStartCancellation() { + Activity activity = getActivity(); + if (activity == null) { + Log.e(TAG, "activity is null when asking to cancel"); + return; + } + + try { + AlertDialog.Builder alertBuilder = new AlertDialog.Builder(getActivity()); + showPendingStartCancellation = true; + alertDialog = alertBuilder.setTitle(activity.getString(R.string.eip_cancel_connect_title)) + .setMessage(activity.getString(R.string.eip_cancel_connect_text)) + .setPositiveButton((android.R.string.yes), (dialog, which) -> stopEipIfPossible()) + .setNegativeButton(activity.getString(android.R.string.no), (dialog, which) -> { + }).setOnDismissListener(dialog -> showPendingStartCancellation = false).show(); + } catch (IllegalStateException e) { + e.printStackTrace(); + } + + } + + protected void askToStopEIP() { + Activity activity = getActivity(); + if (activity == null) { + Log.e(TAG, "activity is null when asking to stop EIP"); + return; + } + try { + AlertDialog.Builder alertBuilder = new AlertDialog.Builder(activity); + showAskToStopEip = true; + alertDialog = alertBuilder.setTitle(activity.getString(R.string.eip_cancel_connect_title)) + .setMessage(activity.getString(R.string.eip_warning_browser_inconsistency)) + .setPositiveButton((android.R.string.yes), (dialog, which) -> stopEipIfPossible()) + .setNegativeButton(activity.getString(android.R.string.no), (dialog, which) -> { + }).setOnDismissListener(dialog -> showAskToStopEip = false).show(); + } catch (IllegalStateException e) { + e.printStackTrace(); + } + + } + + @Override + public void update(Observable observable, Object data) { + if (observable instanceof EipStatus) { + eipStatus = (EipStatus) observable; + Activity activity = getActivity(); + if (activity != null) { + activity.runOnUiThread(this::handleNewState); + } else { + Log.e("EipFragment", "activity is null"); + } + } else if (observable instanceof ProviderObservable) { + provider = ((ProviderObservable) observable).getCurrentProvider(); + } + } + + private void handleNewState() { + Activity activity = getActivity(); + if (activity == null) { + Log.e(TAG, "activity is null while trying to handle new state"); + return; + } + + //Log.d(TAG, "eip fragment eipStatus state: " + eipStatus.getState() + " - level: " + eipStatus.getLevel() + " - is reconnecting: " + eipStatus.isReconnecting()); + + + if (eipStatus.isConnecting() ) { + setMainButtonEnabled(true); + showConnectingLayout(activity); + if (eipStatus.isReconnecting()) { + //Log.d(TAG, "eip show reconnecting toast!"); + //showReconnectToast(activity); + } + } else if (eipStatus.isConnected() ) { + mainButton.setText(activity.getString(R.string.vpn_button_turn_off)); + setMainButtonEnabled(true); + vpnStateImage.setStateIcon(R.drawable.vpn_connected); + vpnStateImage.stopProgress(false); + routedText.setText(R.string.vpn_securely_routed); + routedText.setVisibility(VISIBLE); + vpnRoute.setVisibility(VISIBLE); + setVpnRouteText(); + colorBackground(); + } else if(isOpenVpnRunningWithoutNetwork()){ + mainButton.setText(activity.getString(R.string.vpn_button_turn_off)); + setMainButtonEnabled(true); + vpnStateImage.setStateIcon(R.drawable.vpn_disconnected); + vpnStateImage.stopProgress(false); + routedText.setText(R.string.vpn_securely_routed_no_internet); + routedText.setVisibility(VISIBLE); + vpnRoute.setVisibility(VISIBLE); + setVpnRouteText(); + colorBackgroundALittle(); + } else if (eipStatus.isDisconnected() && reconnectingWithDifferentGateway()) { + showConnectingLayout(activity); + // showRetryToast(activity); + } else if (eipStatus.isDisconnecting()) { + setMainButtonEnabled(false); + showDisconnectingLayout(activity); + } else if (eipStatus.isBlocking()) { + setMainButtonEnabled(true); + vpnStateImage.setStateIcon(R.drawable.vpn_blocking); + vpnStateImage.stopProgress(false); + routedText.setText(getString(R.string.void_vpn_establish, getString(R.string.app_name))); + routedText.setVisibility(VISIBLE); + vpnRoute.setVisibility(GONE); + colorBackgroundALittle(); + } else { + mainButton.setText(activity.getString(R.string.vpn_button_turn_on)); + setMainButtonEnabled(true); + vpnStateImage.setStateIcon(R.drawable.vpn_disconnected); + vpnStateImage.stopProgress(false); + routedText.setVisibility(GONE); + vpnRoute.setVisibility(GONE); + greyscaleBackground(); + } + } + + private void showToast(Activity activity, String message, boolean vibrateLong) { + LayoutInflater inflater = getLayoutInflater(); + View layout = inflater.inflate(R.layout.custom_toast, + activity.findViewById(R.id.custom_toast_container)); + + TextView text = layout.findViewById(R.id.text); + text.setText(message); + + Vibrator v = (Vibrator) activity.getSystemService(Context.VIBRATOR_SERVICE); + if (vibrateLong) { + v.vibrate(100); + v.vibrate(200); + } else { + v.vibrate(100); + } + + Toast toast = new Toast(activity.getApplicationContext()); + toast.setGravity(Gravity.BOTTOM, 0, convertDimensionToPx(this.getContext(), R.dimen.stdpadding)); + toast.setDuration(Toast.LENGTH_LONG); + toast.setView(layout); + toast.show(); + } + private void showReconnectToast(Activity activity) { + String message = (String.format("Retry %d of %d before the next closest gateway will be selected.", connectionRetry()+1, 5)); + showToast(activity, message, false); + } + + private void showRetryToast(Activity activity) { + int nClosestGateway = gatewayOrder(); + String message = String.format("Server number " + nClosestGateway + " not reachable. Trying next gateway."); + showToast(activity, message, true ); + } + + private void showConnectingLayout(Context activity) { + showConnectionTransitionLayout(activity, true); + } + + private void showDisconnectingLayout(Activity activity) { + showConnectionTransitionLayout(activity, false); + } + + private void showConnectionTransitionLayout(Context activity, boolean isConnecting) { + mainButton.setText(activity.getString(android.R.string.cancel)); + vpnStateImage.setStateIcon(R.drawable.vpn_connecting); + vpnStateImage.showProgress(); + routedText.setVisibility(GONE); + vpnRoute.setVisibility(GONE); + if (isConnecting) { + colorBackgroundALittle(); + } else { + greyscaleBackground(); + } + } + + private boolean isOpenVpnRunningWithoutNetwork() { + boolean isRunning = false; + try { + isRunning = eipStatus.getLevel() == LEVEL_NONETWORK && + mService.isVpnRunning(); + } catch (Exception e) { + //eat me + e.printStackTrace(); + } + + return isRunning; + } + + private void bindOpenVpnService() { + Activity activity = getActivity(); + if (activity == null) { + Log.e(TAG, "activity is null when binding OpenVpn"); + return; + } + + Intent intent = new Intent(activity, OpenVPNService.class); + intent.setAction(OpenVPNService.START_SERVICE); + activity.bindService(intent, openVpnConnection, Context.BIND_AUTO_CREATE); + + } + + private void greyscaleBackground() { + ColorMatrix matrix = new ColorMatrix(); + matrix.setSaturation(0); + ColorMatrixColorFilter cf = new ColorMatrixColorFilter(matrix); + background.setColorFilter(cf); + background.setImageAlpha(255); + } + + private void colorBackgroundALittle() { + background.setColorFilter(null); + background.setImageAlpha(144); + } + + private void colorBackground() { + background.setColorFilter(null); + background.setImageAlpha(210); + } + + private void updateInvalidVpnCertificate() { + ProviderAPICommand.execute(getContext(), UPDATE_INVALID_VPN_CERTIFICATE, provider); + } + + private void askUserToLogIn(String userMessage) { + Intent intent = new Intent(getContext(), LoginActivity.class); + intent.putExtra(PROVIDER_KEY, provider); + + if(userMessage != null) { + intent.putExtra(USER_MESSAGE, userMessage); + } + + Activity activity = getActivity(); + if (activity != null) { + activity.startActivityForResult(intent, REQUEST_CODE_LOG_IN); + } + } + + private void setVpnRouteText() { + String vpnRouteString = provider.getName(); + String profileName = VpnStatus.getLastConnectedVpnName(); + if (!TextUtils.isEmpty(profileName)) { + vpnRouteString += " (" + profileName + ")"; + } + vpnRoute.setText(vpnRouteString); + } + + private class EipFragmentServiceConnection implements ServiceConnection { + @Override + public void onServiceConnected(ComponentName className, + IBinder service) { + mService = IOpenVPNServiceInternal.Stub.asInterface(service); + handleNewState(); + } + + @Override + public void onServiceDisconnected(ComponentName arg0) { + mService = null; + } + } + + public void showDonationReminderDialog() { + try { + FragmentTransaction fragmentTransaction = new FragmentManagerEnhanced( + getActivity().getSupportFragmentManager()).removePreviousFragment( + DonationReminderDialog.TAG); + DialogFragment newFragment = new DonationReminderDialog(); + newFragment.setCancelable(false); + newFragment.show(fragmentTransaction, DonationReminderDialog.TAG); + } catch (IllegalStateException | NullPointerException e) { + e.printStackTrace(); + } + } +} diff --git a/app/src/main/java/se/leap/bitmaskclient/base/fragments/ExcludeAppsFragment.java b/app/src/main/java/se/leap/bitmaskclient/base/fragments/ExcludeAppsFragment.java new file mode 100644 index 00000000..18000171 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/base/fragments/ExcludeAppsFragment.java @@ -0,0 +1,335 @@ +/* + * Copyright (c) 2012-2016 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package se.leap.bitmaskclient.base.fragments; + +import android.Manifest; +import android.app.Activity; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.os.Bundle; +import androidx.fragment.app.Fragment; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.BaseAdapter; +import android.widget.CompoundButton; +import android.widget.Filter; +import android.widget.Filterable; +import android.widget.ImageView; +import android.widget.ListView; +import android.widget.SearchView; +import android.widget.TextView; + +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.Vector; + +import de.blinkt.openvpn.VpnProfile; +import se.leap.bitmaskclient.R; +import se.leap.bitmaskclient.base.utils.PreferenceHelper; + +/** + * Created by arne on 16.11.14. + */ +public class ExcludeAppsFragment extends Fragment implements AdapterView.OnItemClickListener, CompoundButton.OnCheckedChangeListener, View.OnClickListener { + private ListView mListView; + private VpnProfile mProfile; + private PackageAdapter mListAdapter; + + private Set apps; + + public interface ExcludedAppsCallback { + void onAppsExcluded(int number); + } + + private ExcludedAppsCallback callback; + + @Override + public void onAttach(Context context) { + super.onAttach(context); + if (context instanceof ExcludedAppsCallback) { + callback = (ExcludedAppsCallback) context; + } + } + + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + AppViewHolder avh = (AppViewHolder) view.getTag(); + avh.checkBox.toggle(); + } + + @Override + public void onClick(View v) { + + } + + static class AppViewHolder { + public ApplicationInfo mInfo; + public View rootView; + public TextView appName; + public ImageView appIcon; + //public TextView appSize; + //public TextView disabled; + public CompoundButton checkBox; + + static public AppViewHolder createOrRecycle(LayoutInflater inflater, View convertView, ViewGroup parent) { + if (convertView == null) { + convertView = inflater.inflate(R.layout.allowed_application_layout, parent, false); + + // Creates a ViewHolder and store references to the two children views + // we want to bind data to. + AppViewHolder holder = new AppViewHolder(); + holder.rootView = convertView; + holder.appName = convertView.findViewById(R.id.app_name); + holder.appIcon = convertView.findViewById(R.id.app_icon); + holder.checkBox = convertView.findViewById(R.id.app_selected); + convertView.setTag(holder); + + return holder; + } else { + // Get the ViewHolder back to get fast access to the TextView + // and the ImageView. + return (AppViewHolder) convertView.getTag(); + } + } + + } + + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + String packageName = (String) buttonView.getTag(); + + if (isChecked) { + Log.d("openvpn", "adding to allowed apps" + packageName); + apps.add(packageName); + + } else { + Log.d("openvpn", "removing from allowed apps" + packageName); + apps.remove(packageName); + } + + if (callback != null) { + callback.onAppsExcluded(apps.size()); + } + } + + class PackageAdapter extends BaseAdapter implements Filterable { + private Vector mPackages; + private final LayoutInflater mInflater; + private final PackageManager mPm; + private ItemFilter mFilter = new ItemFilter(); + private Vector mFilteredData; + + + private class ItemFilter extends Filter { + @Override + protected FilterResults performFiltering(CharSequence constraint) { + + String filterString = constraint.toString().toLowerCase(Locale.getDefault()); + + FilterResults results = new FilterResults(); + + + int count = mPackages.size(); + final Vector nlist = new Vector<>(count); + + for (int i = 0; i < count; i++) { + ApplicationInfo pInfo = mPackages.get(i); + CharSequence appName = pInfo.loadLabel(mPm); + + if (TextUtils.isEmpty(appName)) + appName = pInfo.packageName; + + if (appName instanceof String) { + if (((String) appName).toLowerCase(Locale.getDefault()).contains(filterString)) + nlist.add(pInfo); + } else { + if (appName.toString().toLowerCase(Locale.getDefault()).contains(filterString)) + nlist.add(pInfo); + } + } + results.values = nlist; + results.count = nlist.size(); + + return results; + } + + @Override + protected void publishResults(CharSequence constraint, FilterResults results) { + mFilteredData = (Vector) results.values; + notifyDataSetChanged(); + } + + } + + + PackageAdapter(Context c, VpnProfile vp) { + mPm = c.getPackageManager(); + mProfile = vp; + mInflater = LayoutInflater.from(c); + + mPackages = new Vector<>(); + mFilteredData = mPackages; + } + + private void populateList(Activity c) { + List installedPackages = mPm.getInstalledApplications(PackageManager.GET_META_DATA); + + // Remove apps not using Internet + + int androidSystemUid = 0; + ApplicationInfo system = null; + Vector apps = new Vector(); + + try { + system = mPm.getApplicationInfo("android", PackageManager.GET_META_DATA); + androidSystemUid = system.uid; + apps.add(system); + } catch (PackageManager.NameNotFoundException e) { + } + + + for (ApplicationInfo app : installedPackages) { + + if (mPm.checkPermission(Manifest.permission.INTERNET, app.packageName) == PackageManager.PERMISSION_GRANTED && + app.uid != androidSystemUid) { + + apps.add(app); + } + } + + Collections.sort(apps, new ApplicationInfo.DisplayNameComparator(mPm)); + mPackages = apps; + mFilteredData = apps; + c.runOnUiThread(new Runnable() { + @Override + public void run() { + notifyDataSetChanged(); + } + }); + } + + @Override + public int getCount() { + return mFilteredData.size(); + } + + @Override + public Object getItem(int position) { + return mFilteredData.get(position); + } + + @Override + public long getItemId(int position) { + return mFilteredData.get(position).packageName.hashCode(); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + AppViewHolder viewHolder = AppViewHolder.createOrRecycle(mInflater, convertView, parent); + + viewHolder.mInfo = mFilteredData.get(position); + final ApplicationInfo mInfo = mFilteredData.get(position); + + + CharSequence appName = mInfo.loadLabel(mPm); + + if (TextUtils.isEmpty(appName)) + appName = mInfo.packageName; + viewHolder.appName.setText(appName); + viewHolder.appIcon.setImageDrawable(mInfo.loadIcon(mPm)); + viewHolder.checkBox.setTag(mInfo.packageName); + viewHolder.checkBox.setOnCheckedChangeListener(ExcludeAppsFragment.this); + viewHolder.checkBox.setChecked(apps.contains(mInfo.packageName)); + + return viewHolder.rootView; + } + + @Override + public Filter getFilter() { + return mFilter; + } + } + + @Override + public void onResume() { + super.onResume(); + } + + @Override + public void onDestroy() { + PreferenceHelper.setExcludedApps(this.getActivity().getApplicationContext(), apps); + super.onDestroy(); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + apps = PreferenceHelper.getExcludedApps(this.getContext()); + + setHasOptionsMenu(true); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.allowed_apps, menu); + + SearchView searchView = (SearchView) menu.findItem( R.id.app_search_widget ).getActionView(); + searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String query) { + mListView.setFilterText(query); + mListView.setTextFilterEnabled(true); + return true; + } + + @Override + public boolean onQueryTextChange(String newText) { + mListView.setFilterText(newText); + if (TextUtils.isEmpty(newText)) + mListView.setTextFilterEnabled(false); + else + mListView.setTextFilterEnabled(true); + + return true; + } + }); + searchView.setOnCloseListener(() -> { + mListView.clearTextFilter(); + mListAdapter.getFilter().filter(""); + return false; + }); + + super.onCreateOptionsMenu(menu, inflater); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View v = inflater.inflate(R.layout.allowed_vpn_apps, container, false); + + mListView = v.findViewById(android.R.id.list); + + mListAdapter = new PackageAdapter(getActivity(), mProfile); + mListView.setAdapter(mListAdapter); + mListView.setOnItemClickListener(this); + + mListView.setEmptyView(v.findViewById(R.id.loading_container)); + + new Thread(() -> mListAdapter.populateList(getActivity())).start(); + + return v; + } + +} diff --git a/app/src/main/java/se/leap/bitmaskclient/base/fragments/LogFragment.java b/app/src/main/java/se/leap/bitmaskclient/base/fragments/LogFragment.java new file mode 100644 index 00000000..d788b9e6 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/base/fragments/LogFragment.java @@ -0,0 +1,587 @@ +/* + * Copyright (c) 2012-2016 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package se.leap.bitmaskclient.base.fragments; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.content.Intent; +import android.content.res.Resources; +import android.database.DataSetObserver; +import android.os.Bundle; +import android.os.Handler; +import android.os.Handler.Callback; +import android.os.Message; +import android.preference.PreferenceManager; +import androidx.annotation.Nullable; +import androidx.fragment.app.ListFragment; +import android.text.SpannableString; +import android.text.format.DateFormat; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemLongClickListener; +import android.widget.CheckBox; +import android.widget.LinearLayout; +import android.widget.ListAdapter; +import android.widget.ListView; +import android.widget.RadioGroup; +import android.widget.SeekBar; +import android.widget.TextView; +import android.widget.Toast; + +import java.text.SimpleDateFormat; +import java.util.Collections; +import java.util.Date; +import java.util.Locale; +import java.util.Vector; + +import de.blinkt.openvpn.VpnProfile; +import de.blinkt.openvpn.core.ConnectionStatus; +import de.blinkt.openvpn.core.LogItem; +import de.blinkt.openvpn.core.OpenVPNManagement; +import de.blinkt.openvpn.core.OpenVPNService; +import de.blinkt.openvpn.core.Preferences; +import de.blinkt.openvpn.core.VpnStatus; +import de.blinkt.openvpn.core.VpnStatus.LogListener; +import de.blinkt.openvpn.core.VpnStatus.StateListener; +import se.leap.bitmaskclient.base.models.Constants; +import se.leap.bitmaskclient.R; + +import static de.blinkt.openvpn.core.OpenVPNService.humanReadableByteCount; + +public class LogFragment extends ListFragment implements StateListener, SeekBar.OnSeekBarChangeListener, RadioGroup.OnCheckedChangeListener, VpnStatus.ByteCountListener { + public static final String TAG = LogFragment.class.getSimpleName(); + private static final String LOGTIMEFORMAT = "logtimeformat"; + private static final String VERBOSITYLEVEL = "verbositylevel"; + + + + private SeekBar mLogLevelSlider; + private LinearLayout mOptionsLayout; + private RadioGroup mTimeRadioGroup; + private TextView mUpStatus; + private TextView mDownStatus; + private TextView mConnectStatus; + private boolean mShowOptionsLayout; + private CheckBox mClearLogCheckBox; + + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + ladapter.setLogLevel(progress + 1); + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + } + + @Override + public void onCheckedChanged(RadioGroup group, int checkedId) { + switch (checkedId) { + case R.id.radioISO: + ladapter.setTimeFormat(LogWindowListAdapter.TIME_FORMAT_ISO); + break; + case R.id.radioNone: + ladapter.setTimeFormat(LogWindowListAdapter.TIME_FORMAT_NONE); + break; + case R.id.radioShort: + ladapter.setTimeFormat(LogWindowListAdapter.TIME_FORMAT_SHORT); + break; + + } + } + + @Override + public void updateByteCount(long in, long out, long diffIn, long diffOut) { + //%2$s/s %1$s - ↑%4$s/s %3$s + Resources res = getActivity().getResources(); + final String down = String.format("%2$s %1$s", humanReadableByteCount(in, false, res), humanReadableByteCount(diffIn / OpenVPNManagement.mBytecountInterval, true, res)); + final String up = String.format("%2$s %1$s", humanReadableByteCount(out, false, res), humanReadableByteCount(diffOut / OpenVPNManagement.mBytecountInterval, true, res)); + + if (mUpStatus != null && mDownStatus != null) { + if (getActivity() != null) { + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + mUpStatus.setText(up); + mDownStatus.setText(down); + } + }); + } + } + + } + + + class LogWindowListAdapter implements ListAdapter, LogListener, Callback { + + private static final int MESSAGE_NEWLOG = 0; + + private static final int MESSAGE_CLEARLOG = 1; + + private static final int MESSAGE_NEWTS = 2; + private static final int MESSAGE_NEWLOGLEVEL = 3; + + public static final int TIME_FORMAT_NONE = 0; + public static final int TIME_FORMAT_SHORT = 1; + public static final int TIME_FORMAT_ISO = 2; + private static final int MAX_STORED_LOG_ENTRIES = 1000; + + private Vector allEntries = new Vector<>(); + + private Vector currentLevelEntries = new Vector(); + + private Handler mHandler; + + private Vector observers = new Vector(); + + private int mTimeFormat = 0; + private int mLogLevel = 3; + + + public LogWindowListAdapter() { + initLogBuffer(); + if (mHandler == null) { + mHandler = new Handler(this); + } + + VpnStatus.addLogListener(this); + } + + + private void initLogBuffer() { + allEntries.clear(); + Collections.addAll(allEntries, VpnStatus.getlogbuffer()); + initCurrentMessages(); + } + + String getLogStr() { + String str = ""; + for (LogItem entry : allEntries) { + str += getTime(entry, TIME_FORMAT_ISO) + entry.getString(getActivity()) + '\n'; + } + return str; + } + + + private void shareLog() { + Intent shareIntent = new Intent(Intent.ACTION_SEND); + shareIntent.putExtra(Intent.EXTRA_TEXT, getLogStr()); + shareIntent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.ics_openvpn_log_file)); + shareIntent.setType("text/plain"); + startActivity(Intent.createChooser(shareIntent, "Send Logfile")); + } + + @Override + public void registerDataSetObserver(DataSetObserver observer) { + observers.add(observer); + + } + + @Override + public void unregisterDataSetObserver(DataSetObserver observer) { + observers.remove(observer); + } + + @Override + public int getCount() { + return currentLevelEntries.size(); + } + + @Override + public Object getItem(int position) { + return currentLevelEntries.get(position); + } + + @Override + public long getItemId(int position) { + return ((Object) currentLevelEntries.get(position)).hashCode(); + } + + @Override + public boolean hasStableIds() { + return true; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + TextView v; + if (convertView == null) + v = new TextView(getActivity()); + else + v = (TextView) convertView; + + LogItem le = currentLevelEntries.get(position); + String msg = le.getString(getActivity()); + String time = getTime(le, mTimeFormat); + msg = time + msg; + + int spanStart = time.length(); + + SpannableString t = new SpannableString(msg); + + v.setText(t); + return v; + } + + private String getTime(LogItem le, int time) { + if (time != TIME_FORMAT_NONE) { + Date d = new Date(le.getLogtime()); + java.text.DateFormat timeformat; + if (time == TIME_FORMAT_ISO) + timeformat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()); + else + timeformat = DateFormat.getTimeFormat(getActivity()); + + return timeformat.format(d) + " "; + + } else { + return ""; + } + + } + + @Override + public int getItemViewType(int position) { + return 0; + } + + @Override + public int getViewTypeCount() { + return 1; + } + + @Override + public boolean isEmpty() { + return currentLevelEntries.isEmpty(); + + } + + @Override + public boolean areAllItemsEnabled() { + return true; + } + + @Override + public boolean isEnabled(int position) { + return true; + } + + @Override + public void newLog(LogItem logMessage) { + Message msg = Message.obtain(); + assert (msg != null); + msg.what = MESSAGE_NEWLOG; + Bundle bundle = new Bundle(); + bundle.putParcelable("logmessage", logMessage); + msg.setData(bundle); + mHandler.sendMessage(msg); + } + + @Override + public boolean handleMessage(Message msg) { + // We have been called + if (msg.what == MESSAGE_NEWLOG) { + + LogItem logMessage = msg.getData().getParcelable("logmessage"); + if (addLogMessage(logMessage)) + for (DataSetObserver observer : observers) { + observer.onChanged(); + } + } else if (msg.what == MESSAGE_CLEARLOG) { + for (DataSetObserver observer : observers) { + observer.onInvalidated(); + } + initLogBuffer(); + } else if (msg.what == MESSAGE_NEWTS) { + for (DataSetObserver observer : observers) { + observer.onInvalidated(); + } + } else if (msg.what == MESSAGE_NEWLOGLEVEL) { + initCurrentMessages(); + + for (DataSetObserver observer : observers) { + observer.onChanged(); + } + + } + + return true; + } + + private void initCurrentMessages() { + currentLevelEntries.clear(); + for (LogItem li : allEntries) { + if (li.getVerbosityLevel() <= mLogLevel || + mLogLevel == VpnProfile.MAXLOGLEVEL) + currentLevelEntries.add(li); + } + } + + /** + * @param logmessage + * @return True if the current entries have changed + */ + private boolean addLogMessage(LogItem logmessage) { + allEntries.add(logmessage); + + if (allEntries.size() > MAX_STORED_LOG_ENTRIES) { + Vector oldAllEntries = allEntries; + allEntries = new Vector(allEntries.size()); + for (int i = 50; i < oldAllEntries.size(); i++) { + allEntries.add(oldAllEntries.elementAt(i)); + } + initCurrentMessages(); + return true; + } else { + if (logmessage.getVerbosityLevel() <= mLogLevel) { + currentLevelEntries.add(logmessage); + return true; + } else { + return false; + } + } + } + + void clearLog() { + // Actually is probably called from GUI Thread as result of the user + // pressing a button. But better safe than sorry + VpnStatus.clearLog(); + VpnStatus.logInfo(R.string.logCleared); + mHandler.sendEmptyMessage(MESSAGE_CLEARLOG); + } + + + public void setTimeFormat(int newTimeFormat) { + mTimeFormat = newTimeFormat; + mHandler.sendEmptyMessage(MESSAGE_NEWTS); + } + + public void setLogLevel(int logLevel) { + mLogLevel = logLevel; + mHandler.sendEmptyMessage(MESSAGE_NEWLOGLEVEL); + } + + } + + + private LogWindowListAdapter ladapter; + private TextView mSpeedView; + + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == R.id.clearlog) { + ladapter.clearLog(); + return true; + } else if (item.getItemId() == R.id.send) { + ladapter.shareLog(); + } else if (item.getItemId() == R.id.toggle_time) { + showHideOptionsPanel(); + } + return super.onOptionsItemSelected(item); + + } + + private void showHideOptionsPanel() { + boolean optionsVisible = (mOptionsLayout.getVisibility() != View.GONE); + + ObjectAnimator anim; + if (optionsVisible) { + anim = ObjectAnimator.ofFloat(mOptionsLayout, "alpha", 1.0f, 0f); + anim.addListener(collapseListener); + + } else { + mOptionsLayout.setVisibility(View.VISIBLE); + anim = ObjectAnimator.ofFloat(mOptionsLayout, "alpha", 0f, 1.0f); + //anim = new TranslateAnimation(0.0f, 0.0f, mOptionsLayout.getHeight(), 0.0f); + + } + + //anim.setInterpolator(new AccelerateInterpolator(1.0f)); + //anim.setDuration(300); + //mOptionsLayout.startAnimation(anim); + anim.start(); + + } + + AnimatorListenerAdapter collapseListener = new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animator) { + mOptionsLayout.setVisibility(View.GONE); + } + + }; + + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.f_log, menu); + if (getResources().getBoolean(R.bool.logSildersAlwaysVisible)) + menu.removeItem(R.id.toggle_time); + } + + + @Override + public void onResume() { + super.onResume(); + Intent intent = new Intent(getActivity(), OpenVPNService.class); + intent.setAction(OpenVPNService.START_SERVICE); + } + + @Override + public void onStart() { + super.onStart(); + VpnStatus.addStateListener(this); + VpnStatus.addByteCountListener(this); + } + + @Override + public void onStop() { + super.onStop(); + VpnStatus.removeStateListener(this); + VpnStatus.removeByteCountListener(this); + + getActivity().getPreferences(0).edit().putInt(LOGTIMEFORMAT, ladapter.mTimeFormat) + .putInt(VERBOSITYLEVEL, ladapter.mLogLevel).apply(); + + } + + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + ListView lv = getListView(); + + lv.setOnItemLongClickListener(new OnItemLongClickListener() { + + @Override + public boolean onItemLongClick(AdapterView parent, View view, + int position, long id) { + ClipboardManager clipboard = (ClipboardManager) + getActivity().getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clip = ClipData.newPlainText("Log Entry", ((TextView) view).getText()); + clipboard.setPrimaryClip(clip); + Toast.makeText(getActivity(), R.string.copied_entry, Toast.LENGTH_SHORT).show(); + return true; + } + }); + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View v = inflater.inflate(R.layout.f_log, container, false); + + setHasOptionsMenu(true); + + ladapter = new LogWindowListAdapter(); + ladapter.mTimeFormat = getActivity().getPreferences(0).getInt(LOGTIMEFORMAT, 1); + int logLevel = getActivity().getPreferences(0).getInt(VERBOSITYLEVEL, 1); + ladapter.setLogLevel(logLevel); + + setListAdapter(ladapter); + + mTimeRadioGroup = v.findViewById(R.id.timeFormatRadioGroup); + mTimeRadioGroup.setOnCheckedChangeListener(this); + + if (ladapter.mTimeFormat == LogWindowListAdapter.TIME_FORMAT_ISO) { + mTimeRadioGroup.check(R.id.radioISO); + } else if (ladapter.mTimeFormat == LogWindowListAdapter.TIME_FORMAT_NONE) { + mTimeRadioGroup.check(R.id.radioNone); + } else if (ladapter.mTimeFormat == LogWindowListAdapter.TIME_FORMAT_SHORT) { + mTimeRadioGroup.check(R.id.radioShort); + } + + mClearLogCheckBox = v.findViewById(R.id.clearlogconnect); + mClearLogCheckBox.setChecked(PreferenceManager.getDefaultSharedPreferences(getActivity()).getBoolean(Constants.CLEARLOG, true)); + mClearLogCheckBox.setOnCheckedChangeListener((buttonView, isChecked) -> + Preferences.getDefaultSharedPreferences(getActivity()).edit().putBoolean(Constants.CLEARLOG, isChecked).apply()); + + mSpeedView = v.findViewById(R.id.speed); + + mOptionsLayout = v.findViewById(R.id.logOptionsLayout); + mLogLevelSlider = v.findViewById(R.id.LogLevelSlider); + mLogLevelSlider.setMax(VpnProfile.MAXLOGLEVEL - 1); + mLogLevelSlider.setProgress(logLevel - 1); + + mLogLevelSlider.setOnSeekBarChangeListener(this); + + if (getResources().getBoolean(R.bool.logSildersAlwaysVisible)) + mOptionsLayout.setVisibility(View.VISIBLE); + + mUpStatus = v.findViewById(R.id.speedUp); + mDownStatus = v.findViewById(R.id.speedDown); + mConnectStatus = v.findViewById(R.id.speedStatus); + if (mShowOptionsLayout) + mOptionsLayout.setVisibility(View.VISIBLE); + return v; + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + // Scroll to the end of the list end + //getListView().setSelection(getListView().getAdapter().getCount()-1); + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + if (getResources().getBoolean(R.bool.logSildersAlwaysVisible)) { + mShowOptionsLayout = true; + if (mOptionsLayout != null) + mOptionsLayout.setVisibility(View.VISIBLE); + } + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + } + + + @Override + public void updateState(final String status, final String logMessage, final int resId, final ConnectionStatus level) { + if (isAdded()) { + final String cleanLogMessage = VpnStatus.getLastCleanLogMessage(getActivity()); + + getActivity().runOnUiThread(() -> { + if (isAdded()) { + if (mSpeedView != null) { + mSpeedView.setText(cleanLogMessage); + } + if (mConnectStatus != null) + mConnectStatus.setText(cleanLogMessage); + } + }); + } + } + + @Override + public void setConnectedVPN(String uuid) { + } + + + @Override + public void onDestroy() { + VpnStatus.removeLogListener(ladapter); + super.onDestroy(); + } + +} diff --git a/app/src/main/java/se/leap/bitmaskclient/base/fragments/MainActivityErrorDialog.java b/app/src/main/java/se/leap/bitmaskclient/base/fragments/MainActivityErrorDialog.java new file mode 100644 index 00000000..4b307f23 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/base/fragments/MainActivityErrorDialog.java @@ -0,0 +1,174 @@ +/** + * 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.base.fragments; + +import android.app.Dialog; +import android.content.Context; +import android.os.Bundle; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.DialogFragment; +import androidx.appcompat.app.AlertDialog; + +import org.json.JSONObject; + +import se.leap.bitmaskclient.R; +import se.leap.bitmaskclient.eip.EIP; +import se.leap.bitmaskclient.eip.EipCommand; +import se.leap.bitmaskclient.base.models.Provider; +import se.leap.bitmaskclient.providersetup.ProviderAPICommand; + +import static se.leap.bitmaskclient.providersetup.ProviderAPI.UPDATE_INVALID_VPN_CERTIFICATE; +import static se.leap.bitmaskclient.R.string.warning_option_try_ovpn; +import static se.leap.bitmaskclient.R.string.warning_option_try_pt; +import static se.leap.bitmaskclient.eip.EIP.EIPErrors.UNKNOWN; +import static se.leap.bitmaskclient.eip.EIP.EIPErrors.valueOf; +import static se.leap.bitmaskclient.eip.EIP.ERRORS; +import static se.leap.bitmaskclient.eip.EIP.ERRORID; +import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getUsePluggableTransports; +import static se.leap.bitmaskclient.base.utils.PreferenceHelper.usePluggableTransports; + +/** + * Implements an error dialog for the main activity. + * + * @author fupduck + * @author cyberta + */ +public class MainActivityErrorDialog extends DialogFragment { + + final public static String TAG = "downloaded_failed_dialog"; + final private static String KEY_REASON_TO_FAIL = "key reason to fail"; + final private static String KEY_PROVIDER = "key provider"; + private String reasonToFail; + private EIP.EIPErrors downloadError = UNKNOWN; + + private Provider provider; + + /** + * @return a new instance of this DialogFragment. + */ + public static DialogFragment newInstance(Provider provider, String reasonToFail) { + return newInstance(provider, reasonToFail, UNKNOWN); + } + + /** + * @return a new instance of this DialogFragment. + */ + public static DialogFragment newInstance(Provider provider, String reasonToFail, EIP.EIPErrors error) { + MainActivityErrorDialog dialogFragment = new MainActivityErrorDialog(); + dialogFragment.reasonToFail = reasonToFail; + dialogFragment.provider = provider; + dialogFragment.downloadError = error; + return dialogFragment; + } + + /** + * @return a new instance of this DialogFragment. + */ + public static DialogFragment newInstance(Provider provider, JSONObject errorJson) { + MainActivityErrorDialog dialogFragment = new MainActivityErrorDialog(); + dialogFragment.provider = provider; + try { + if (errorJson.has(ERRORS)) { + dialogFragment.reasonToFail = errorJson.getString(ERRORS); + } else { + //default error msg + dialogFragment.reasonToFail = dialogFragment.getString(R.string.error_io_exception_user_message); + } + + if (errorJson.has(ERRORID)) { + dialogFragment.downloadError = valueOf(errorJson.getString(ERRORID)); + } + } catch (Exception e) { + e.printStackTrace(); + dialogFragment.reasonToFail = dialogFragment.getString(R.string.error_io_exception_user_message); + } + return dialogFragment; + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + restoreFromSavedInstance(savedInstanceState); + } + + @Override + @NonNull + public Dialog onCreateDialog(Bundle savedInstanceState) { + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + Context applicationContext = getContext().getApplicationContext(); + builder.setMessage(reasonToFail) + .setNegativeButton(R.string.cancel, (dialog, id) -> { + }); + switch (downloadError) { + case ERROR_INVALID_VPN_CERTIFICATE: + builder.setPositiveButton(R.string.update_certificate, (dialog, which) -> + ProviderAPICommand.execute(getContext(), UPDATE_INVALID_VPN_CERTIFICATE, provider)); + break; + case NO_MORE_GATEWAYS: + if (provider.supportsPluggableTransports()) { + if (getUsePluggableTransports(applicationContext)) { + builder.setPositiveButton(warning_option_try_ovpn, ((dialog, which) -> { + usePluggableTransports(applicationContext, false); + EipCommand.startVPN(applicationContext, false); + })); + } else { + builder.setPositiveButton(warning_option_try_pt, ((dialog, which) -> { + usePluggableTransports(applicationContext, true); + EipCommand.startVPN(applicationContext, false); + })); + } + } else { + builder.setPositiveButton(R.string.retry, (dialog, which) -> { + EipCommand.startVPN(applicationContext, false); + }); + } + break; + case ERROR_VPN_PREPARE: + builder.setPositiveButton(R.string.retry, (dialog, which) -> { + EipCommand.startVPN(applicationContext, false); + }); + break; + default: + break; + } + + // Create the AlertDialog object and return it + return builder.create(); + } + + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putString(KEY_REASON_TO_FAIL, reasonToFail); + outState.putParcelable(KEY_PROVIDER, provider); + } + + private void restoreFromSavedInstance(Bundle savedInstanceState) { + if (savedInstanceState == null) { + return; + } + if (savedInstanceState.containsKey(KEY_PROVIDER)) { + this.provider = savedInstanceState.getParcelable(KEY_PROVIDER); + } + if (savedInstanceState.containsKey(KEY_REASON_TO_FAIL)) { + this.reasonToFail = savedInstanceState.getString(KEY_REASON_TO_FAIL); + } + } + +} diff --git a/app/src/main/java/se/leap/bitmaskclient/base/fragments/TetheringDialog.java b/app/src/main/java/se/leap/bitmaskclient/base/fragments/TetheringDialog.java new file mode 100644 index 00000000..8593e25c --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/base/fragments/TetheringDialog.java @@ -0,0 +1,258 @@ +package se.leap.bitmaskclient.base.fragments; + +import android.app.Dialog; +import android.content.ActivityNotFoundException; +import android.content.ComponentName; +import android.content.Intent; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.provider.Settings; +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatDialogFragment; +import androidx.appcompat.widget.AppCompatTextView; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.method.LinkMovementMethod; +import android.text.style.ClickableSpan; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import java.util.Observable; +import java.util.Observer; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import butterknife.ButterKnife; +import butterknife.InjectView; +import de.blinkt.openvpn.core.VpnStatus; +import se.leap.bitmaskclient.R; +import se.leap.bitmaskclient.firewall.FirewallManager; +import se.leap.bitmaskclient.tethering.TetheringObservable; +import se.leap.bitmaskclient.base.utils.PreferenceHelper; +import se.leap.bitmaskclient.base.views.IconCheckboxEntry; + +/** + * Copyright (c) 2020 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 . + */ + +public class TetheringDialog extends AppCompatDialogFragment implements Observer { + + public final static String TAG = TetheringDialog.class.getName(); + + @InjectView(R.id.tvTitle) + AppCompatTextView title; + + @InjectView(R.id.user_message) + AppCompatTextView userMessage; + + @InjectView(R.id.selection_list_view) + RecyclerView selectionListView; + DialogListAdapter adapter; + private DialogListAdapter.ViewModel[] dataset; + + public static class DialogListAdapter extends RecyclerView.Adapter { + + interface OnItemClickListener { + void onItemClick(ViewModel item); + } + + private ViewModel[] dataSet; + private OnItemClickListener clickListener; + + DialogListAdapter(ViewModel[] dataSet, OnItemClickListener clickListener) { + this.dataSet = dataSet; + this.clickListener = clickListener; + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) { + IconCheckboxEntry v = new IconCheckboxEntry(viewGroup.getContext()); + return new ViewHolder(v); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder viewHolder, int i) { + viewHolder.bind(dataSet[i], clickListener); + } + + @Override + public int getItemCount() { + return dataSet.length; + } + + public static class ViewModel { + + public Drawable image; + public String text; + public boolean checked; + public boolean enabled; + + ViewModel(Drawable image, String text, boolean checked, boolean enabled) { + this.image = image; + this.text = text; + this.checked = checked; + this.enabled = enabled; + } + } + + public static class ViewHolder extends RecyclerView.ViewHolder { + + ViewHolder(IconCheckboxEntry v) { + super(v); + } + + public void bind(ViewModel model, OnItemClickListener onClickListener) { + ((IconCheckboxEntry) this.itemView).bind(model); + this.itemView.setOnClickListener(v -> { + model.checked = !model.checked; + ((IconCheckboxEntry) itemView).setChecked(model.checked); + onClickListener.onItemClick(model); + }); + } + } + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + LayoutInflater inflater = getActivity().getLayoutInflater(); + View view = inflater.inflate(R.layout.d_list_selection, null); + ButterKnife.inject(this, view); + + title.setText(R.string.tethering); + userMessage.setMovementMethod(LinkMovementMethod.getInstance()); + userMessage.setLinkTextColor(getContext().getResources().getColor(R.color.colorPrimary)); + userMessage.setText(createUserMessage()); + + initDataset(); + adapter = new DialogListAdapter(dataset, this::onItemClick); + selectionListView.setAdapter(adapter); + selectionListView.setLayoutManager(new LinearLayoutManager(getActivity())); + + + builder.setView(view) + .setPositiveButton(android.R.string.ok, (dialog, id) -> { + PreferenceHelper.allowWifiTethering(getContext(), dataset[0].checked); + PreferenceHelper.allowUsbTethering(getContext(), dataset[1].checked); + PreferenceHelper.allowBluetoothTethering(getContext(), dataset[2].checked); + TetheringObservable.allowVpnWifiTethering(dataset[0].checked); + TetheringObservable.allowVpnUsbTethering(dataset[1].checked); + TetheringObservable.allowVpnBluetoothTethering(dataset[2].checked); + FirewallManager firewallManager = new FirewallManager(getContext().getApplicationContext(), false); + if (VpnStatus.isVPNActive()) { + if (TetheringObservable.getInstance().getTetheringState().hasAnyDeviceTetheringEnabled() && + TetheringObservable.getInstance().getTetheringState().hasAnyVpnTetheringAllowed()) { + firewallManager.startTethering(); + } else { + firewallManager.stopTethering(); + } + } + }).setNegativeButton(R.string.cancel, (dialog, id) -> dialog.cancel()); + return builder.create(); + } + + @Override + public void onResume() { + super.onResume(); + dataset[0].enabled = TetheringObservable.getInstance().isWifiTetheringEnabled(); + dataset[1].enabled = TetheringObservable.getInstance().isUsbTetheringEnabled(); + dataset[2].enabled = TetheringObservable.getInstance().isBluetoothTetheringEnabled(); + adapter.notifyDataSetChanged(); + TetheringObservable.getInstance().addObserver(this); + } + + @Override + public void onPause() { + super.onPause(); + TetheringObservable.getInstance().deleteObserver(this); + } + + public void onItemClick(DialogListAdapter.ViewModel item) { + + } + + private CharSequence createUserMessage() { + String tetheringMessage = getString(R.string.tethering_message); + String systemSettingsMessage = getString(R.string.tethering_enabled_message); + Pattern pattern = Pattern.compile("([\\w .]*)()+([\\w ]*)()([\\w .]*)"); + Matcher matcher = pattern.matcher(systemSettingsMessage); + int startIndex = 0; + int endIndex = 0; + if (matcher.matches()) { + startIndex = matcher.start(2); + endIndex = startIndex + matcher.group(3).length(); + } + systemSettingsMessage = systemSettingsMessage.replace("", "").replace("", ""); + String wholeMessage = systemSettingsMessage + "\n\n" + tetheringMessage; + Spannable spannable = new SpannableString(wholeMessage); + spannable.setSpan(new ClickableSpan() { + @Override + public void onClick(@NonNull View widget) { + try { + final Intent intent = new Intent(Intent.ACTION_MAIN, null); + intent.addCategory(Intent.CATEGORY_LAUNCHER); + final ComponentName cn = new ComponentName("com.android.settings", "com.android.settings.TetherSettings"); + intent.setComponent(cn); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + } catch (ActivityNotFoundException e) { + Intent intent = new Intent(Settings.ACTION_WIRELESS_SETTINGS); + startActivity(intent); + } + + } + }, startIndex, endIndex, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + + return spannable; + } + + private void initDataset() { + dataset = new DialogListAdapter.ViewModel[] { + new DialogListAdapter.ViewModel(getContext().getResources().getDrawable(R.drawable.ic_wifi), + getContext().getString(R.string.tethering_wifi), + PreferenceHelper.isWifiTetheringAllowed(getContext()), + TetheringObservable.getInstance().isWifiTetheringEnabled()), + new DialogListAdapter.ViewModel(getContext().getResources().getDrawable(R.drawable.ic_usb), + getContext().getString(R.string.tethering_usb), + PreferenceHelper.isUsbTetheringAllowed(getContext()), + TetheringObservable.getInstance().isUsbTetheringEnabled()), + new DialogListAdapter.ViewModel(getContext().getResources().getDrawable(R.drawable.ic_bluetooth), + getContext().getString(R.string.tethering_bluetooth), + PreferenceHelper.isBluetoothTetheringAllowed(getContext()), + TetheringObservable.getInstance().isUsbTetheringEnabled()) + }; + } + + @Override + public void update(Observable o, Object arg) { + if (o instanceof TetheringObservable) { + TetheringObservable observable = (TetheringObservable) o; + Log.d(TAG, "TetheringObservable is updated"); + dataset[0].enabled = observable.isWifiTetheringEnabled(); + dataset[1].enabled = observable.isUsbTetheringEnabled(); + dataset[2].enabled = observable.isBluetoothTetheringEnabled(); + adapter.notifyDataSetChanged(); + } + } + +} -- cgit v1.2.3 From 6af193f7d3c0fa3f73f5809442d83367bf025ffd Mon Sep 17 00:00:00 2001 From: cyBerta Date: Tue, 29 Dec 2020 01:01:05 +0100 Subject: move NavigationDrawerFragment into fragments directory --- .../base/fragments/NavigationDrawerFragment.java | 668 +++++++++++++++++++++ 1 file changed, 668 insertions(+) create mode 100644 app/src/main/java/se/leap/bitmaskclient/base/fragments/NavigationDrawerFragment.java (limited to 'app/src/main/java/se/leap/bitmaskclient/base/fragments') diff --git a/app/src/main/java/se/leap/bitmaskclient/base/fragments/NavigationDrawerFragment.java b/app/src/main/java/se/leap/bitmaskclient/base/fragments/NavigationDrawerFragment.java new file mode 100644 index 00000000..2ce0e597 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/base/fragments/NavigationDrawerFragment.java @@ -0,0 +1,668 @@ +/** + * Copyright (c) 2019 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.base.fragments; + + +import android.app.Activity; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.res.Configuration; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import androidx.annotation.NonNull; +import androidx.annotation.StringRes; +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentTransaction; +import androidx.core.view.GravityCompat; +import androidx.drawerlayout.widget.DrawerLayout; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.ActionBarDrawerToggle; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import java.util.Observable; +import java.util.Observer; +import java.util.Set; + +import de.blinkt.openvpn.core.VpnStatus; +import se.leap.bitmaskclient.base.FragmentManagerEnhanced; +import se.leap.bitmaskclient.base.MainActivity; +import se.leap.bitmaskclient.base.models.Provider; +import se.leap.bitmaskclient.providersetup.ProviderListActivity; +import se.leap.bitmaskclient.base.models.ProviderObservable; +import se.leap.bitmaskclient.R; +import se.leap.bitmaskclient.eip.EipCommand; +import se.leap.bitmaskclient.eip.EipStatus; +import se.leap.bitmaskclient.firewall.FirewallManager; +import se.leap.bitmaskclient.tethering.TetheringObservable; +import se.leap.bitmaskclient.base.utils.PreferenceHelper; +import se.leap.bitmaskclient.base.views.IconSwitchEntry; +import se.leap.bitmaskclient.base.views.IconTextEntry; + +import static android.content.Context.MODE_PRIVATE; +import static android.view.View.GONE; +import static android.view.View.VISIBLE; +import static se.leap.bitmaskclient.base.BitmaskApp.getRefWatcher; +import static se.leap.bitmaskclient.base.models.Constants.DONATION_URL; +import static se.leap.bitmaskclient.base.models.Constants.ENABLE_DONATION; +import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_KEY; +import static se.leap.bitmaskclient.base.models.Constants.REQUEST_CODE_SWITCH_PROVIDER; +import static se.leap.bitmaskclient.base.models.Constants.SHARED_PREFERENCES; +import static se.leap.bitmaskclient.base.models.Constants.USE_IPv6_FIREWALL; +import static se.leap.bitmaskclient.base.models.Constants.USE_PLUGGABLE_TRANSPORTS; +import static se.leap.bitmaskclient.R.string.about_fragment_title; +import static se.leap.bitmaskclient.R.string.exclude_apps_fragment_title; +import static se.leap.bitmaskclient.R.string.log_fragment_title; +import static se.leap.bitmaskclient.base.utils.ConfigHelper.isDefaultBitmask; +import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getSaveBattery; +import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getShowAlwaysOnDialog; +import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getUsePluggableTransports; +import static se.leap.bitmaskclient.base.utils.PreferenceHelper.saveBattery; +import static se.leap.bitmaskclient.base.utils.PreferenceHelper.showExperimentalFeatures; +import static se.leap.bitmaskclient.base.utils.PreferenceHelper.usePluggableTransports; + +/** + * Fragment used for managing interactions for and presentation of a navigation drawer. + * See the + * design guidelines for a complete explanation of the behaviors implemented here. + */ +public class NavigationDrawerFragment extends Fragment implements SharedPreferences.OnSharedPreferenceChangeListener, Observer { + + /** + * Per the design guidelines, you should show the drawer on launch until the user manually + * expands it. This shared preference tracks this. + */ + private static final String PREF_USER_LEARNED_DRAWER = "navigation_drawer_learned"; + private static final String TAG = NavigationDrawerFragment.class.getName(); + public static final int TWO_SECONDS = 2000; + + /** + * Helper component that ties the action bar to the navigation drawer. + */ + private ActionBarDrawerToggle drawerToggle; + + private DrawerLayout drawerLayout; + private View drawerView; + private View fragmentContainerView; + private Toolbar toolbar; + private IconTextEntry account; + private IconSwitchEntry saveBattery; + private IconTextEntry tethering; + private IconSwitchEntry firewall; + private View experimentalFeatureFooter; + + private boolean userLearnedDrawer; + private volatile boolean wasPaused; + private volatile boolean shouldCloseOnResume; + + private SharedPreferences preferences; + + private final static String KEY_SHOW_SAVE_BATTERY_ALERT = "KEY_SHOW_SAVE_BATTERY_ALERT"; + private volatile boolean showSaveBattery = false; + AlertDialog alertDialog; + private FirewallManager firewallManager; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + // Reads 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); + userLearnedDrawer = preferences.getBoolean(PREF_USER_LEARNED_DRAWER, false); + preferences.registerOnSharedPreferenceChangeListener(this); + firewallManager = new FirewallManager(getContext().getApplicationContext(), false); + + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + // Indicates that this fragment would like to influence the set of actions in the action bar. + setHasOptionsMenu(true); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + drawerView = inflater.inflate(R.layout.f_drawer_main, container, false); + restoreFromSavedInstance(savedInstanceState); + TetheringObservable.getInstance().addObserver(this); + EipStatus.getInstance().addObserver(this); + return drawerView; + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + TetheringObservable.getInstance().deleteObserver(this); + EipStatus.getInstance().deleteObserver(this); + } + + public boolean isDrawerOpen() { + return drawerLayout != null && drawerLayout.isDrawerOpen(fragmentContainerView); + } + + @Override + public void onResume() { + super.onResume(); + wasPaused = false; + if (shouldCloseOnResume) { + closeDrawerWithDelay(); + } + } + + @Override + public void onPause() { + super.onPause(); + wasPaused = true; + } + + + + /** + * Users of this fragment must call this method to set up the navigation drawer interactions. + * + * @param fragmentId The android:id of this fragment in its activity's layout. + * @param drawerLayout The DrawerLayout containing this fragment's UI. + */ + public void setUp(int fragmentId, DrawerLayout drawerLayout) { + final AppCompatActivity activity = (AppCompatActivity) getActivity(); + fragmentContainerView = activity.findViewById(fragmentId); + this.drawerLayout = drawerLayout; + // set a custom shadow that overlays the main content when the drawer opens + this.drawerLayout.setDrawerShadow(R.drawable.drawer_shadow, GravityCompat.START); + toolbar = this.drawerLayout.findViewById(R.id.toolbar); + + setupActionBar(); + setupEntries(); + setupActionBarDrawerToggle(activity); + + if (!userLearnedDrawer) { + openNavigationDrawerForFirstTimeUsers(); + } + + // Defer code dependent on restoration of previous instance state. + this.drawerLayout.post(() -> drawerToggle.syncState()); + this.drawerLayout.addDrawerListener(drawerToggle); + } + + private void setupActionBarDrawerToggle(final AppCompatActivity activity) { + // ActionBarDrawerToggle ties together the the proper interactions + // between the navigation drawer and the action bar app icon. + drawerToggle = new ActionBarDrawerToggle( + activity, + drawerLayout, + toolbar, + R.string.navigation_drawer_open, + R.string.navigation_drawer_close + ) { + @Override + public void onDrawerClosed(View drawerView) { + super.onDrawerClosed(drawerView); + if (!isAdded()) { + return; + } + activity.invalidateOptionsMenu(); + } + + @Override + public void onDrawerOpened(View drawerView) { + super.onDrawerOpened(drawerView); + if (!isAdded()) { + return; + } + + if (!userLearnedDrawer) { + // The user manually opened the drawer; store this flag to prevent auto-showing + // the navigation drawer automatically in the future. + userLearnedDrawer = true; + preferences.edit().putBoolean(PREF_USER_LEARNED_DRAWER, true).apply(); + } + activity.invalidateOptionsMenu(); + } + }; + } + + private void setupEntries() { + initAccountEntry(); + initSwitchProviderEntry(); + initUseBridgesEntry(); + initSaveBatteryEntry(); + initAlwaysOnVpnEntry(); + initExcludeAppsEntry(); + initShowExperimentalHint(); + initTetheringEntry(); + initFirewallEntry(); + initExperimentalFeatureFooter(); + initDonateEntry(); + initLogEntry(); + initAboutEntry(); + } + + private void initAccountEntry() { + account = drawerView.findViewById(R.id.account); + FragmentManagerEnhanced fragmentManager = new FragmentManagerEnhanced(getActivity().getSupportFragmentManager()); + Provider currentProvider = ProviderObservable.getInstance().getCurrentProvider(); + account.setText(currentProvider.getName()); + account.setOnClickListener((buttonView) -> { + Fragment fragment = new EipFragment(); + Bundle arguments = new Bundle(); + arguments.putParcelable(PROVIDER_KEY, currentProvider); + fragment.setArguments(arguments); + hideActionBarSubTitle(); + fragmentManager.replace(R.id.main_container, fragment, MainActivity.TAG); + closeDrawer(); + }); + } + + private void initSwitchProviderEntry() { + if (isDefaultBitmask()) { + IconTextEntry switchProvider = drawerView.findViewById(R.id.switch_provider); + switchProvider.setVisibility(VISIBLE); + switchProvider.setOnClickListener(v -> + getActivity().startActivityForResult(new Intent(getActivity(), ProviderListActivity.class), REQUEST_CODE_SWITCH_PROVIDER)); + } + } + + private void initUseBridgesEntry() { + IconSwitchEntry useBridges = drawerView.findViewById(R.id.bridges_switch); + if (ProviderObservable.getInstance().getCurrentProvider().supportsPluggableTransports()) { + useBridges.setVisibility(VISIBLE); + useBridges.setChecked(getUsePluggableTransports(getContext())); + useBridges.setOnCheckedChangeListener((buttonView, isChecked) -> { + if (!buttonView.isPressed()) { + return; + } + usePluggableTransports(getContext(), isChecked); + if (VpnStatus.isVPNActive()) { + EipCommand.startVPN(getContext(), false); + closeDrawer(); + } + }); + + + } else { + useBridges.setVisibility(GONE); + } + } + + private void initSaveBatteryEntry() { + saveBattery = drawerView.findViewById(R.id.battery_switch); + saveBattery.showSubtitle(false); + saveBattery.setChecked(getSaveBattery(getContext())); + saveBattery.setOnCheckedChangeListener(((buttonView, isChecked) -> { + if (!buttonView.isPressed()) { + return; + } + if (isChecked) { + showSaveBatteryAlert(); + } else { + saveBattery(getContext(), false); + } + })); + boolean enableEntry = !TetheringObservable.getInstance().getTetheringState().isVpnTetheringRunning(); + enableSaveBatteryEntry(enableEntry); + } + + private void enableSaveBatteryEntry(boolean enabled) { + if (saveBattery.isEnabled() == enabled) { + return; + } + saveBattery.setEnabled(enabled); + saveBattery.showSubtitle(!enabled); + } + + private void initAlwaysOnVpnEntry() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + IconTextEntry alwaysOnVpn = drawerView.findViewById(R.id.always_on_vpn); + alwaysOnVpn.setVisibility(VISIBLE); + alwaysOnVpn.setOnClickListener((buttonView) -> { + closeDrawer(); + if (getShowAlwaysOnDialog(getContext())) { + showAlwaysOnDialog(); + } else { + Intent intent = new Intent("android.net.vpn.SETTINGS"); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + } + }); + } + } + + private void initExcludeAppsEntry() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + IconTextEntry excludeApps = drawerView.findViewById(R.id.exclude_apps); + excludeApps.setVisibility(VISIBLE); + Set apps = PreferenceHelper.getExcludedApps(this.getContext()); + if (apps != null) { + updateExcludeAppsSubtitle(excludeApps, apps.size()); + } + FragmentManagerEnhanced fragmentManager = new FragmentManagerEnhanced(getActivity().getSupportFragmentManager()); + excludeApps.setOnClickListener((buttonView) -> { + closeDrawer(); + Fragment fragment = new ExcludeAppsFragment(); + setActionBarTitle(exclude_apps_fragment_title); + fragmentManager.replace(R.id.main_container, fragment, MainActivity.TAG); + }); + } + } + + private void initShowExperimentalHint() { + TextView textView = drawerLayout.findViewById(R.id.show_experimental_features); + textView.setText(showExperimentalFeatures(getContext()) ? R.string.hide_experimental : R.string.show_experimental); + textView.setOnClickListener(v -> { + boolean shown = showExperimentalFeatures(getContext()); + if (shown) { + tethering.setVisibility(GONE); + firewall.setVisibility(GONE); + experimentalFeatureFooter.setVisibility(GONE); + ((TextView) v).setText(R.string.show_experimental); + } else { + tethering.setVisibility(VISIBLE); + firewall.setVisibility(VISIBLE); + experimentalFeatureFooter.setVisibility(VISIBLE); + ((TextView) v).setText(R.string.hide_experimental); + } + PreferenceHelper.setShowExperimentalFeatures(getContext(), !shown); + }); + } + + private void initFirewallEntry() { + firewall = drawerView.findViewById(R.id.enableIPv6Firewall); + boolean show = showExperimentalFeatures(getContext()); + firewall.setVisibility(show ? VISIBLE : GONE); + firewall.setChecked(PreferenceHelper.useIpv6Firewall(getContext())); + firewall.setOnCheckedChangeListener((buttonView, isChecked) -> { + if (!buttonView.isPressed()) { + return; + } + PreferenceHelper.setUseIPv6Firewall(getContext(), isChecked); + if (VpnStatus.isVPNActive()) { + if (isChecked) { + firewallManager.startIPv6Firewall(); + } else { + firewallManager.stopIPv6Firewall(); + } + } + }); + } + + private void initTetheringEntry() { + tethering = drawerView.findViewById(R.id.tethering); + boolean show = showExperimentalFeatures(getContext()); + tethering.setVisibility(show ? VISIBLE : GONE); + tethering.setOnClickListener((buttonView) -> { + showTetheringAlert(); + }); + } + + private void initExperimentalFeatureFooter() { + experimentalFeatureFooter = drawerView.findViewById(R.id.experimental_features_footer); + boolean show = showExperimentalFeatures(getContext()); + experimentalFeatureFooter.setVisibility(show ? VISIBLE : GONE); + } + + private void initDonateEntry() { + if (ENABLE_DONATION) { + IconTextEntry donate = drawerView.findViewById(R.id.donate); + donate.setVisibility(VISIBLE); + donate.setOnClickListener((buttonView) -> { + closeDrawer(); + Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(DONATION_URL)); + startActivity(browserIntent); + + }); + } + } + + private void initLogEntry() { + IconTextEntry log = drawerView.findViewById(R.id.log); + FragmentManagerEnhanced fragmentManager = new FragmentManagerEnhanced(getActivity().getSupportFragmentManager()); + log.setOnClickListener((buttonView) -> { + closeDrawer(); + Fragment fragment = new LogFragment(); + setActionBarTitle(log_fragment_title); + fragmentManager.replace(R.id.main_container, fragment, MainActivity.TAG); + }); + } + + private void initAboutEntry() { + IconTextEntry about = drawerView.findViewById(R.id.about); + FragmentManagerEnhanced fragmentManager = new FragmentManagerEnhanced(getActivity().getSupportFragmentManager()); + about.setOnClickListener((buttonView) -> { + closeDrawer(); + Fragment fragment = new AboutFragment(); + setActionBarTitle(about_fragment_title); + fragmentManager.replace(R.id.main_container, fragment, MainActivity.TAG); + }); + } + + private void closeDrawer() { + if (drawerLayout != null) { + drawerLayout.closeDrawer(fragmentContainerView); + } + } + + private ActionBar setupActionBar() { + AppCompatActivity activity = (AppCompatActivity) getActivity(); + activity.setSupportActionBar(toolbar); + final ActionBar actionBar = activity.getSupportActionBar(); + actionBar.setDisplayHomeAsUpEnabled(true); + actionBar.setHomeButtonEnabled(true); + actionBar.setDisplayShowTitleEnabled(true); + return actionBar; + } + + private void openNavigationDrawerForFirstTimeUsers() { + if (userLearnedDrawer) { + return; + } + + drawerLayout.openDrawer(fragmentContainerView, false); + closeDrawerWithDelay(); + } + + @NonNull + private void closeDrawerWithDelay() { + final Handler navigationDrawerHandler = new Handler(); + navigationDrawerHandler.postDelayed(() -> { + if (!wasPaused) { + drawerLayout.closeDrawer(fragmentContainerView, true); + } else { + shouldCloseOnResume = true; + } + + }, TWO_SECONDS); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + if (showSaveBattery) { + outState.putBoolean(KEY_SHOW_SAVE_BATTERY_ALERT, true); + alertDialog.dismiss(); + } + } + + private void restoreFromSavedInstance(Bundle savedInstanceState) { + if (savedInstanceState != null && savedInstanceState.containsKey(KEY_SHOW_SAVE_BATTERY_ALERT)) { + showSaveBatteryAlert(); + } + } + + private void showSaveBatteryAlert() { + Activity activity = getActivity(); + if (activity == null) { + return; + } + + try { + AlertDialog.Builder alertBuilder = new AlertDialog.Builder(getActivity()); + showSaveBattery = true; + alertDialog = alertBuilder + .setTitle(activity.getString(R.string.save_battery)) + .setMessage(activity.getString(R.string.save_battery_message)) + .setPositiveButton((android.R.string.yes), (dialog, which) -> { + saveBattery(getContext(), true); + }) + .setNegativeButton(activity.getString(android.R.string.no), (dialog, which) -> saveBattery.setCheckedQuietly(false)) + .setOnDismissListener(dialog -> showSaveBattery = false) + .setOnCancelListener(dialog -> saveBattery.setCheckedQuietly(false)).show(); + } catch (IllegalStateException e) { + e.printStackTrace(); + } + } + + public void showTetheringAlert() { + try { + + FragmentTransaction fragmentTransaction = new FragmentManagerEnhanced( + getActivity().getSupportFragmentManager()).removePreviousFragment( + TetheringDialog.TAG); + DialogFragment newFragment = new TetheringDialog(); + newFragment.show(fragmentTransaction, TetheringDialog.TAG); + } catch (IllegalStateException | NullPointerException e) { + e.printStackTrace(); + } + } + + public void showAlwaysOnDialog() { + try { + + FragmentTransaction fragmentTransaction = new FragmentManagerEnhanced( + getActivity().getSupportFragmentManager()).removePreviousFragment( + AlwaysOnDialog.TAG); + DialogFragment newFragment = new AlwaysOnDialog(); + newFragment.show(fragmentTransaction, AlwaysOnDialog.TAG); + } catch (IllegalStateException | NullPointerException e) { + e.printStackTrace(); + } + + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + // Forward the new configuration the drawer toggle component. + drawerToggle.onConfigurationChanged(newConfig); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + if (drawerLayout != null && isDrawerOpen()) { + showGlobalContextActionBar(); + } + super.onCreateOptionsMenu(menu, inflater); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (drawerToggle.onOptionsItemSelected(item)) { + return true; + } + return super.onOptionsItemSelected(item); + } + + @Override + public void onDestroy() { + super.onDestroy(); + getRefWatcher(getActivity()).watch(this); + preferences.unregisterOnSharedPreferenceChangeListener(this); + } + + /** + * Per the navigation drawer design guidelines, updates the action bar to show the global app + * 'context', rather than just what's in the current screen. + */ + private void showGlobalContextActionBar() { + ActionBar actionBar = getActionBar(); + actionBar.setDisplayShowTitleEnabled(true); + actionBar.setTitle(R.string.app_name); + } + + private ActionBar getActionBar() { + return ((AppCompatActivity) getActivity()).getSupportActionBar(); + } + + private void setActionBarTitle(@StringRes int resId) { + ActionBar actionBar = getActionBar(); + if (actionBar != null) { + actionBar.setSubtitle(resId); + } + } + + private void hideActionBarSubTitle() { + ActionBar actionBar = getActionBar(); + if (actionBar != null) { + actionBar.setSubtitle(null); + } + } + + public void refresh() { + Provider currentProvider = ProviderObservable.getInstance().getCurrentProvider(); + account.setText(currentProvider.getName()); + initUseBridgesEntry(); + } + + private void updateExcludeAppsSubtitle(IconTextEntry excludeApps, int number) { + if (number > 0) { + excludeApps.setSubtitle(getContext().getResources().getQuantityString(R.plurals.subtitle_exclude_apps, number, number)); + excludeApps.setSubtitleColor(R.color.colorError); + } else { + excludeApps.hideSubtitle(); + } + } + + public void onAppsExcluded(int number) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + IconTextEntry excludeApps = drawerView.findViewById(R.id.exclude_apps); + updateExcludeAppsSubtitle(excludeApps, number); + } + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + if (key.equals(USE_PLUGGABLE_TRANSPORTS)) { + initUseBridgesEntry(); + } else if (key.equals(USE_IPv6_FIREWALL)) { + initFirewallEntry(); + } + } + + @Override + public void update(Observable o, Object arg) { + if (o instanceof TetheringObservable || o instanceof EipStatus) { + try { + getActivity().runOnUiThread(() -> + enableSaveBatteryEntry(!TetheringObservable.getInstance().getTetheringState().isVpnTetheringRunning())); + } catch (NullPointerException npe) { + // eat me + } + } + } +} -- cgit v1.2.3