/** * Copyright (c) 2021 LEAP Encryption Access Project and contributors * * 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.graphics.drawable.Drawable; import android.os.Bundle; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.widget.AppCompatButton; import androidx.appcompat.widget.AppCompatImageView; import androidx.appcompat.widget.AppCompatTextView; import androidx.core.content.ContextCompat; import androidx.core.graphics.drawable.DrawableCompat; import androidx.fragment.app.Fragment; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import java.util.List; import java.util.Observable; import java.util.Observer; import se.leap.bitmaskclient.R; import se.leap.bitmaskclient.base.MainActivity; import se.leap.bitmaskclient.base.models.Location; import se.leap.bitmaskclient.base.utils.PreferenceHelper; import se.leap.bitmaskclient.base.views.IconSwitchEntry; import se.leap.bitmaskclient.base.views.LocationIndicator; import se.leap.bitmaskclient.eip.EipCommand; import se.leap.bitmaskclient.eip.EipStatus; import se.leap.bitmaskclient.eip.GatewaysManager; import static android.content.Context.MODE_PRIVATE; import static android.view.View.INVISIBLE; import static android.view.View.VISIBLE; import static se.leap.bitmaskclient.base.MainActivity.ACTION_SHOW_VPN_FRAGMENT; import static se.leap.bitmaskclient.base.models.Constants.PREFERRED_CITY; import static se.leap.bitmaskclient.base.models.Constants.SHARED_PREFERENCES; import static se.leap.bitmaskclient.base.models.Constants.USE_BRIDGES; import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getPreferredCity; import static se.leap.bitmaskclient.base.utils.PreferenceHelper.setPreferredCity; public class GatewaySelectionFragment extends Fragment implements SharedPreferences.OnSharedPreferenceChangeListener, Observer { private static final String TAG = GatewaySelectionFragment.class.getSimpleName(); private RecyclerView recyclerView; private LocationListAdapter locationListAdapter; private IconSwitchEntry autoSelectionSwitch; private AppCompatButton vpnButton; private GatewaysManager gatewaysManager; private SharedPreferences preferences; private EipStatus eipStatus; public GatewaySelectionFragment() { // Required empty public constructor } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); gatewaysManager = new GatewaysManager(getContext()); preferences = getContext().getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE); eipStatus = EipStatus.getInstance(); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Inflate the layout for this fragment return inflater.inflate(R.layout.f_gateway_selection, container, false); } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); initRecyclerView(); initAutoSelectionSwitch(); initVpnButton(); eipStatus.addObserver(this); preferences.registerOnSharedPreferenceChangeListener(this); } @Override public void onDestroyView() { super.onDestroyView(); preferences.unregisterOnSharedPreferenceChangeListener(this); eipStatus.deleteObserver(this); } private void initRecyclerView() { recyclerView = (RecyclerView) getActivity().findViewById(R.id.gatewaySelection_list); recyclerView.setHasFixedSize(true); LinearLayoutManager layoutManager = new LinearLayoutManager(this.getContext()); recyclerView.setLayoutManager(layoutManager); locationListAdapter = new LocationListAdapter(gatewaysManager.getGatewayLocations()); recyclerView.setAdapter(locationListAdapter); recyclerView.setVisibility(getPreferredCity(getContext()) == null ? INVISIBLE : VISIBLE); } private void initAutoSelectionSwitch() { autoSelectionSwitch = getActivity().findViewById(R.id.automatic_gateway_switch); autoSelectionSwitch.setSingleLine(false); autoSelectionSwitch.setSubtitle(getString(R.string.gateway_selection_warning, getString(R.string.app_name))); autoSelectionSwitch.setChecked(getPreferredCity(getContext()) == null); autoSelectionSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> { recyclerView.setVisibility(!isChecked ? VISIBLE : View.GONE); Log.d(TAG, "autoselection enabled: " + isChecked); if (isChecked) { PreferenceHelper.setPreferredCity(getContext(), null); locationListAdapter.resetSelection(); } setVpnButtonState(); }); } private void initVpnButton() { vpnButton = getActivity().findViewById(R.id.vpn_button); setVpnButtonState(); vpnButton.setOnClickListener(v -> { EipCommand.startVPN(getContext(), false); Intent intent = new Intent(getContext(), MainActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP); intent.setAction(ACTION_SHOW_VPN_FRAGMENT); startActivity(intent); }); } private void setVpnButtonState() { if (eipStatus.isDisconnected()) { vpnButton.setText(R.string.vpn_button_turn_on); } else { vpnButton.setText(R.string.reconnect); } vpnButton.setEnabled( (locationListAdapter.selectedLocation != null && locationListAdapter.selectedLocation.selected) || autoSelectionSwitch.isChecked()); } @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { if (USE_BRIDGES.equals(key)) { locationListAdapter.updateData(gatewaysManager.getGatewayLocations()); setVpnButtonState(); } else if (PREFERRED_CITY.equals(key)) { setVpnButtonState(); } } @Override public void update(Observable o, Object arg) { if (o instanceof EipStatus) { eipStatus = (EipStatus) o; Activity activity = getActivity(); if (activity != null) { activity.runOnUiThread(this::setVpnButtonState); } } } static class LocationListAdapter extends RecyclerView.Adapter { private static final String TAG = LocationListAdapter.class.getSimpleName(); private List values; private Location selectedLocation = null; static class ViewHolder extends RecyclerView.ViewHolder { public AppCompatTextView locationLabel; public LocationIndicator locationIndicator; public AppCompatImageView checkedIcon; public View layout; public ViewHolder(View v) { super(v); layout = v; locationLabel = (AppCompatTextView) v.findViewById(R.id.location); locationIndicator = (LocationIndicator) v.findViewById(R.id.quality); checkedIcon = (AppCompatImageView) v.findViewById(R.id.checked_icon); } } public void add(int position, Location item) { values.add(position, item); notifyItemInserted(position); } public void remove(int position) { values.remove(position); notifyItemRemoved(position); } public void resetSelection() { if (selectedLocation != null) { selectedLocation.selected = false; notifyDataSetChanged(); } } public void updateData(List data) { values = data; notifyDataSetChanged(); } public LocationListAdapter(List data) { values = data; } @NonNull @Override public LocationListAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { LayoutInflater inflater = LayoutInflater.from( parent.getContext()); View v = inflater.inflate(R.layout.v_select_text_list_item, parent, false); return new ViewHolder(v); } // Replace the contents of a view (invoked by the layout manager) @Override public void onBindViewHolder(ViewHolder holder, final int position) { final Location location = values.get(position); holder.locationLabel.setText(location.name); holder.layout.setOnClickListener(v -> { Log.d(TAG, "view at position clicked: " + position); if (selectedLocation == null) { selectedLocation = location; selectedLocation.selected = true; } else if (selectedLocation.name.equals(location.name)){ selectedLocation.selected = !selectedLocation.selected; } else { selectedLocation.selected = false; selectedLocation = location; selectedLocation.selected = true; } setPreferredCity(holder.layout.getContext(), selectedLocation.selected ? selectedLocation.name : null); holder.checkedIcon.setVisibility(selectedLocation.selected ? VISIBLE : INVISIBLE); notifyDataSetChanged(); }); Drawable checkIcon = DrawableCompat.wrap(holder.layout.getContext().getResources().getDrawable(R.drawable.ic_check_bold)).mutate(); DrawableCompat.setTint(checkIcon, ContextCompat.getColor(holder.layout.getContext(), R.color.colorSuccess)); holder.checkedIcon.setImageDrawable(checkIcon); holder.checkedIcon.setVisibility(location.selected ? VISIBLE : INVISIBLE); holder.locationIndicator.setLoad(GatewaysManager.Load.getLoadByValue(location.averageLoad)); if (location.selected) { selectedLocation = location; } } public String getQualityString(double quality) { if (quality == 0) { return ""; } else if (quality < 0.25) { return "BAD"; } else if (quality < 0.6) { return "AVERAGE"; } else if (quality < 0.8){ return "GOOD"; } else { return "EXCELLENT"; } } @Override public int getItemCount() { return values.size(); } } }