From 921ee995ef0f0e7f2076ac3538fed289bcb82ba9 Mon Sep 17 00:00:00 2001 From: cyBerta Date: Thu, 2 Jan 2020 23:06:36 +0100 Subject: implement basic UI for VPN tethering --- .../drawer/NavigationDrawerFragment.java | 30 +++- .../main/java/se/leap/bitmaskclient/eip/EIP.java | 4 + .../java/se/leap/bitmaskclient/eip/EipCommand.java | 11 ++ .../bitmaskclient/fragments/TetheringDialog.java | 164 +++++++++++++++++---- .../leap/bitmaskclient/utils/PreferenceHelper.java | 27 ++++ .../bitmaskclient/views/IconCheckboxEntry.java | 91 ++++-------- app/src/main/res/layout/d_list_selection.xml | 26 +--- app/src/main/res/layout/f_drawer_main.xml | 10 ++ .../res/layout/v_icon_select_text_list_item.xml | 45 +++--- 9 files changed, 274 insertions(+), 134 deletions(-) diff --git a/app/src/main/java/se/leap/bitmaskclient/drawer/NavigationDrawerFragment.java b/app/src/main/java/se/leap/bitmaskclient/drawer/NavigationDrawerFragment.java index 3144d62c..85c982df 100644 --- a/app/src/main/java/se/leap/bitmaskclient/drawer/NavigationDrawerFragment.java +++ b/app/src/main/java/se/leap/bitmaskclient/drawer/NavigationDrawerFragment.java @@ -60,6 +60,7 @@ import se.leap.bitmaskclient.fragments.AboutFragment; import se.leap.bitmaskclient.fragments.AlwaysOnDialog; import se.leap.bitmaskclient.fragments.ExcludeAppsFragment; import se.leap.bitmaskclient.fragments.LogFragment; +import se.leap.bitmaskclient.fragments.TetheringDialog; import se.leap.bitmaskclient.utils.PreferenceHelper; import se.leap.bitmaskclient.views.IconSwitchEntry; import se.leap.bitmaskclient.views.IconTextEntry; @@ -236,6 +237,7 @@ public class NavigationDrawerFragment extends Fragment implements SharedPreferen initUseBridgesEntry(); initSaveBatteryEntry(); initAlwaysOnVpnEntry(); + initTetheringEntry(); initExcludeAppsEntry(); initDonateEntry(); initLogEntry(); @@ -321,6 +323,19 @@ public class NavigationDrawerFragment extends Fragment implements SharedPreferen } } + private void initTetheringEntry() { + IconTextEntry tethering = drawerView.findViewById(R.id.tethering); + if (PreferenceHelper.hasSuPermission(getContext())) { + tethering.setVisibility(VISIBLE); + tethering.setOnClickListener((buttonView) -> { + showTetheringAlert(); + }); + } else { + tethering.setVisibility(GONE); + } + + } + private void initExcludeAppsEntry() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { IconTextEntry excludeApps = drawerView.findViewById(R.id.exclude_apps); @@ -442,13 +457,26 @@ public class NavigationDrawerFragment extends Fragment implements SharedPreferen saveBattery(getContext(), true); }) .setNegativeButton(activity.getString(android.R.string.no), (dialog, which) -> saveBattery.setCheckedQuietly(false)) - .setOnDismissListener(dialog -> showEnableExperimentalFeature = 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 { diff --git a/app/src/main/java/se/leap/bitmaskclient/eip/EIP.java b/app/src/main/java/se/leap/bitmaskclient/eip/EIP.java index 1d67cd12..1186b54f 100644 --- a/app/src/main/java/se/leap/bitmaskclient/eip/EIP.java +++ b/app/src/main/java/se/leap/bitmaskclient/eip/EIP.java @@ -61,6 +61,7 @@ import static de.blinkt.openvpn.core.connection.Connection.TransportType.OPENVPN import static se.leap.bitmaskclient.Constants.BROADCAST_GATEWAY_SETUP_OBSERVER_EVENT; import static se.leap.bitmaskclient.Constants.BROADCAST_RESULT_KEY; import static se.leap.bitmaskclient.Constants.EIP_ACTION_CHECK_CERT_VALIDITY; +import static se.leap.bitmaskclient.Constants.EIP_ACTION_CONFIGURE_TETHERING; import static se.leap.bitmaskclient.Constants.EIP_ACTION_IS_RUNNING; import static se.leap.bitmaskclient.Constants.EIP_ACTION_START; import static se.leap.bitmaskclient.Constants.EIP_ACTION_START_ALWAYS_ON_VPN; @@ -190,6 +191,9 @@ public final class EIP extends JobIntentService implements Observer { disconnect(); earlyRoutes(); break; + case EIP_ACTION_CONFIGURE_TETHERING: + Log.d(TAG, "TODO: implement tethering configuration"); + break; } } diff --git a/app/src/main/java/se/leap/bitmaskclient/eip/EipCommand.java b/app/src/main/java/se/leap/bitmaskclient/eip/EipCommand.java index eb266e3c..d2667e42 100644 --- a/app/src/main/java/se/leap/bitmaskclient/eip/EipCommand.java +++ b/app/src/main/java/se/leap/bitmaskclient/eip/EipCommand.java @@ -6,6 +6,7 @@ import android.content.Intent; import android.os.ResultReceiver; import android.support.annotation.NonNull; import android.support.annotation.VisibleForTesting; +import android.support.v4.content.ContextCompat; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -13,6 +14,7 @@ import org.jetbrains.annotations.Nullable; import se.leap.bitmaskclient.Provider; import static se.leap.bitmaskclient.Constants.EIP_ACTION_CHECK_CERT_VALIDITY; +import static se.leap.bitmaskclient.Constants.EIP_ACTION_CONFIGURE_TETHERING; import static se.leap.bitmaskclient.Constants.EIP_ACTION_START; import static se.leap.bitmaskclient.Constants.EIP_ACTION_START_BLOCKING_VPN; import static se.leap.bitmaskclient.Constants.EIP_ACTION_STOP; @@ -90,4 +92,13 @@ public class EipCommand { execute(context, EIP_ACTION_CHECK_CERT_VALIDITY, resultReceiver, null); } + public static void configureTethering(@NonNull Context context) { + execute(context, EIP_ACTION_CONFIGURE_TETHERING); + } + + @VisibleForTesting + public static void configureTethering(@NonNull Context context, ResultReceiver resultReceiver) { + execute(context, EIP_ACTION_CONFIGURE_TETHERING); + } + } diff --git a/app/src/main/java/se/leap/bitmaskclient/fragments/TetheringDialog.java b/app/src/main/java/se/leap/bitmaskclient/fragments/TetheringDialog.java index 90e3c5a1..96f7f0d4 100644 --- a/app/src/main/java/se/leap/bitmaskclient/fragments/TetheringDialog.java +++ b/app/src/main/java/se/leap/bitmaskclient/fragments/TetheringDialog.java @@ -2,48 +2,113 @@ package se.leap.bitmaskclient.fragments; import android.app.Dialog; import android.content.Intent; -import android.os.Build; +import android.graphics.drawable.Drawable; import android.os.Bundle; +import android.provider.Settings; import android.support.annotation.NonNull; -import android.support.annotation.Nullable; import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatDialogFragment; import android.support.v7.widget.AppCompatTextView; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.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.widget.CheckBox; +import android.view.ViewGroup; import butterknife.ButterKnife; import butterknife.InjectView; import se.leap.bitmaskclient.R; -import se.leap.bitmaskclient.views.IconTextView; - -import static se.leap.bitmaskclient.utils.PreferenceHelper.saveShowAlwaysOnDialog; - +import se.leap.bitmaskclient.eip.EipCommand; +import se.leap.bitmaskclient.utils.PreferenceHelper; +import se.leap.bitmaskclient.views.IconCheckboxEntry; /** * Created by cyberta on 25.02.18. */ - - public class TetheringDialog extends AppCompatDialogFragment { public final static String TAG = TetheringDialog.class.getName(); - @InjectView(R.id.do_not_show_again) - CheckBox doNotShowAgainCheckBox; + @InjectView(R.id.tvTitle) + AppCompatTextView title; @InjectView(R.id.user_message) - IconTextView userMessage; + AppCompatTextView userMessage; - @InjectView(R.id.block_vpn_user_message) - AppCompatTextView blockVpnUserMessage; + @InjectView(R.id.selection_list_view) + RecyclerView selectionListView; + DialogListAdapter adapter; + private DialogListAdapter.ViewModel[] dataset; + public static class DialogListAdapter extends RecyclerView.Adapter { - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); + interface OnItemClickListener { + void onItemClick(ViewModel item); + } + + private ViewModel[] dataSet; + private OnItemClickListener clickListener; + + public 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; + + ViewModel(Drawable image, String text, boolean checked) { + Log.d(TAG, "init ViewModel with string: " + text + " checked: " + checked); + + this.image = image; + this.text = text; + this.checked = checked; + } + } + + public static class ViewHolder extends RecyclerView.ViewHolder { + + ViewHolder(IconCheckboxEntry v) { + super(v); + } + + public void bind(ViewModel model, OnItemClickListener onClickListener) { + Log.d(TAG, "bind ViewModel"); + + ((IconCheckboxEntry) this.itemView).bind(model); + this.itemView.setOnClickListener(v -> { + model.checked = !model.checked; + ((IconCheckboxEntry) itemView).setChecked(model.checked); + onClickListener.onItemClick(model); + }); + } + } } @NonNull @@ -51,26 +116,67 @@ public class TetheringDialog extends AppCompatDialogFragment { 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); + View view = inflater.inflate(R.layout.d_list_selection, null); ButterKnife.inject(this, view); - userMessage.setIcon(R.drawable.ic_settings); - userMessage.setText(getString(R.string.always_on_vpn_user_message)); + 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())); - 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); + PreferenceHelper.wifiTethering(getContext(), dataset[0].checked); + PreferenceHelper.usbTethering(getContext(), dataset[1].checked); + PreferenceHelper.bluetoothTethering(getContext(), dataset[2].checked); + EipCommand.configureTethering(getContext()); }) .setNegativeButton(R.string.cancel, (dialog, id) -> dialog.cancel()); return builder.create(); } + + public void onItemClick(DialogListAdapter.ViewModel item) { + + } + + private CharSequence createUserMessage() { + String tetheringMessage = getString(R.string.tethering_message); + String systemSettings = getString(R.string.tethering_system_settings); + String systemSettingsMessage = getString(R.string.tethering_enabled_message, systemSettings); + String wholeMessage = systemSettingsMessage + "\n\n" + tetheringMessage; + int startIndex = wholeMessage.indexOf(systemSettings, 0); + int endIndex = startIndex + systemSettings.length(); + + Spannable spannable = new SpannableString(wholeMessage); + spannable.setSpan(new ClickableSpan() { + @Override + public void onClick(@NonNull View widget) { + 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.getWifiTethering(getContext())), + new DialogListAdapter.ViewModel(getContext().getResources().getDrawable(R.drawable.ic_usb), + getContext().getString(R.string.tethering_usb), + PreferenceHelper.getUsbTethering(getContext())), + new DialogListAdapter.ViewModel(getContext().getResources().getDrawable(R.drawable.ic_bluetooth), + getContext().getString(R.string.tethering_bluetooth), + PreferenceHelper.getBluetoothTethering(getContext())) + }; + } + } diff --git a/app/src/main/java/se/leap/bitmaskclient/utils/PreferenceHelper.java b/app/src/main/java/se/leap/bitmaskclient/utils/PreferenceHelper.java index de2058c7..25e4b797 100644 --- a/app/src/main/java/se/leap/bitmaskclient/utils/PreferenceHelper.java +++ b/app/src/main/java/se/leap/bitmaskclient/utils/PreferenceHelper.java @@ -32,6 +32,9 @@ import static se.leap.bitmaskclient.Constants.PROVIDER_PRIVATE_KEY; import static se.leap.bitmaskclient.Constants.PROVIDER_VPN_CERTIFICATE; import static se.leap.bitmaskclient.Constants.SHARED_PREFERENCES; import static se.leap.bitmaskclient.Constants.SU_PERMISSION; +import static se.leap.bitmaskclient.Constants.TETHERING_BLUETOOTH; +import static se.leap.bitmaskclient.Constants.TETHERING_USB; +import static se.leap.bitmaskclient.Constants.TETHERING_WIFI; import static se.leap.bitmaskclient.Constants.USE_PLUGGABLE_TRANSPORTS; /** @@ -146,6 +149,30 @@ public class PreferenceHelper { return getBoolean(context, DEFAULT_SHARED_PREFS_BATTERY_SAVER, false); } + public static void usbTethering(Context context, boolean isEnabled) { + putBoolean(context, TETHERING_USB, isEnabled); + } + + public static boolean getUsbTethering(Context context) { + return getBoolean(context, TETHERING_USB, false); + } + + public static void wifiTethering(Context context, boolean isEnabled) { + putBoolean(context, TETHERING_WIFI, isEnabled); + } + + public static boolean getWifiTethering(Context context) { + return getBoolean(context, TETHERING_WIFI, false); + } + + public static void bluetoothTethering(Context context, boolean isEnabled) { + putBoolean(context, TETHERING_BLUETOOTH, isEnabled); + } + + public static boolean getBluetoothTethering(Context context) { + return getBoolean(context, TETHERING_BLUETOOTH, false); + } + public static void saveShowAlwaysOnDialog(Context context, boolean showAlwaysOnDialog) { putBoolean(context, ALWAYS_ON_SHOW_DIALOG, showAlwaysOnDialog); } diff --git a/app/src/main/java/se/leap/bitmaskclient/views/IconCheckboxEntry.java b/app/src/main/java/se/leap/bitmaskclient/views/IconCheckboxEntry.java index cd151885..933d391b 100644 --- a/app/src/main/java/se/leap/bitmaskclient/views/IconCheckboxEntry.java +++ b/app/src/main/java/se/leap/bitmaskclient/views/IconCheckboxEntry.java @@ -2,45 +2,51 @@ package se.leap.bitmaskclient.views; import android.annotation.TargetApi; import android.content.Context; -import android.content.res.TypedArray; import android.graphics.drawable.Drawable; -import android.support.annotation.ColorRes; -import android.support.annotation.DrawableRes; import android.support.annotation.Nullable; -import android.support.annotation.StringRes; +import android.support.v4.content.ContextCompat; +import android.support.v4.graphics.drawable.DrawableCompat; +import android.support.v7.widget.AppCompatImageView; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; -import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; +import butterknife.ButterKnife; +import butterknife.InjectView; import se.leap.bitmaskclient.R; +import se.leap.bitmaskclient.fragments.TetheringDialog; -public class IconTextEntry extends LinearLayout { +public class IconCheckboxEntry extends LinearLayout { - private TextView textView; - private ImageView iconView; - private TextView subtitleView; + @InjectView(android.R.id.text1) + TextView textView; - public IconTextEntry(Context context) { + @InjectView(R.id.material_icon) + AppCompatImageView iconView; + + @InjectView(R.id.checked_icon) + AppCompatImageView checkedIcon; + + public IconCheckboxEntry(Context context) { super(context); initLayout(context, null); } - public IconTextEntry(Context context, @Nullable AttributeSet attrs) { + public IconCheckboxEntry(Context context, @Nullable AttributeSet attrs) { super(context, attrs); initLayout(context, attrs); } - public IconTextEntry(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + public IconCheckboxEntry(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initLayout(context, attrs); } @TargetApi(21) - public IconTextEntry(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + public IconCheckboxEntry(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); initLayout(context, attrs); } @@ -48,59 +54,24 @@ public class IconTextEntry extends LinearLayout { void initLayout(Context context, AttributeSet attrs) { LayoutInflater inflater = (LayoutInflater) context .getSystemService(Context.LAYOUT_INFLATER_SERVICE); - View rootview = inflater.inflate(R.layout.v_icon_text_list_item, this, true); - textView = rootview.findViewById(android.R.id.text1); - subtitleView = rootview.findViewById(R.id.subtitle); - iconView = rootview.findViewById(R.id.material_icon); - - if (attrs != null) { - TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.IconTextEntry); - - String entryText = typedArray.getString(R.styleable.IconTextEntry_text); - if (entryText != null) { - textView.setText(entryText); - } - - String subtitle = typedArray.getString(R.styleable.IconTextEntry_subtitle); - if (subtitle != null) { - subtitleView.setText(subtitle); - subtitleView.setVisibility(VISIBLE); - } - - Drawable drawable = typedArray.getDrawable(R.styleable.IconTextEntry_icon); - if (drawable != null) { - iconView.setImageDrawable(drawable); - } - - typedArray.recycle(); - } - + View rootview = inflater.inflate(R.layout.v_icon_select_text_list_item, this, true); + ButterKnife.inject(this, rootview); - } - - public void setText(@StringRes int id) { - textView.setText(id); - } - - public void setSubtitle(String text) { - subtitleView.setText(text); - subtitleView.setVisibility(VISIBLE); - } - - public void hideSubtitle() { - subtitleView.setVisibility(GONE); - } + Drawable checkIcon = DrawableCompat.wrap(getResources().getDrawable(R.drawable.ic_check_bold)); + DrawableCompat.setTint(checkIcon, ContextCompat.getColor(getContext(), R.color.colorSuccess)); + checkedIcon.setImageDrawable(checkIcon); - public void setSubtitleColor(@ColorRes int color) { - subtitleView.setTextColor(getContext().getResources().getColor(color)); } - public void setText(CharSequence text) { - textView.setText(text); + public void bind(TetheringDialog.DialogListAdapter.ViewModel model) { + textView.setText(model.text); + iconView.setImageDrawable(model.image); + setChecked(model.checked); } - public void setIcon(@DrawableRes int id) { - iconView.setImageResource(id); + public void setChecked(boolean checked) { + checkedIcon.setVisibility(checked ? VISIBLE : GONE); + checkedIcon.setContentDescription(checked ? "selected" : "unselected"); } } diff --git a/app/src/main/res/layout/d_list_selection.xml b/app/src/main/res/layout/d_list_selection.xml index a9a84c0e..ef963303 100644 --- a/app/src/main/res/layout/d_list_selection.xml +++ b/app/src/main/res/layout/d_list_selection.xml @@ -1,5 +1,5 @@ - @@ -18,46 +18,34 @@ android:layout_marginTop="@dimen/add_button_margin" android:layout_marginLeft="@dimen/activity_horizontal_margin" android:layout_marginRight="@dimen/activity_horizontal_margin" - android:text="@string/always_on_vpn" android:textAllCaps="true" android:textAppearance="@style/TextAppearance.AppCompat.Title" android:textStyle="bold" /> - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/f_drawer_main.xml b/app/src/main/res/layout/f_drawer_main.xml index f6c9b2bb..505bd714 100644 --- a/app/src/main/res/layout/f_drawer_main.xml +++ b/app/src/main/res/layout/f_drawer_main.xml @@ -88,6 +88,16 @@ android:visibility="gone" /> + + + - + -