ConfigListFragment: Use a floating action menu

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
This commit is contained in:
Samuel Holland 2017-11-28 20:14:47 -06:00
parent 19f0089559
commit 4a672fc05d
8 changed files with 137 additions and 72 deletions

View File

@ -22,5 +22,6 @@ android {
} }
dependencies { dependencies {
implementation 'com.getbase:floatingactionbutton:1.10.1'
implementation fileTree(dir: 'libs', include: ['*.jar']) implementation fileTree(dir: 'libs', include: ['*.jar'])
} }

View File

@ -9,7 +9,6 @@ import android.os.Bundle;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import com.wireguard.android.backends.VpnService;
import com.wireguard.config.Config; import com.wireguard.config.Config;
/** /**
@ -18,7 +17,6 @@ import com.wireguard.config.Config;
public class ConfigActivity extends BaseConfigActivity { public class ConfigActivity extends BaseConfigActivity {
private static final String KEY_EDITOR_STATE = "editorState"; private static final String KEY_EDITOR_STATE = "editorState";
private static final int REQUEST_IMPORT = 1;
private static final String TAG_DETAIL = "detail"; private static final String TAG_DETAIL = "detail";
private static final String TAG_EDIT = "edit"; private static final String TAG_EDIT = "edit";
private static final String TAG_LIST = "list"; private static final String TAG_LIST = "list";
@ -133,18 +131,11 @@ public class ConfigActivity extends BaseConfigActivity {
} }
} }
@Override
public void onActivityResult(final int requestCode, final int resultCode, final Intent data) {
if (requestCode == REQUEST_IMPORT) {
if (resultCode == RESULT_OK)
VpnService.getInstance().importFrom(data.getData());
} else {
super.onActivityResult(requestCode, resultCode, data);
}
}
@Override @Override
public void onBackPressed() { public void onBackPressed() {
final ConfigListFragment listFragment = (ConfigListFragment) fragments.get(TAG_LIST);
if (listFragment.isVisible() && listFragment.tryCollapseMenu())
return;
super.onBackPressed(); super.onBackPressed();
// The visible fragment is now the one that was on top of the back stack, if there was one. // The visible fragment is now the one that was on top of the back stack, if there was one.
if (isEditing()) if (isEditing())
@ -200,19 +191,10 @@ public class ConfigActivity extends BaseConfigActivity {
// The back arrow in the action bar should act the same as the back button. // The back arrow in the action bar should act the same as the back button.
onBackPressed(); onBackPressed();
return true; return true;
case R.id.menu_action_add:
startActivity(new Intent(this, AddActivity.class));
return true;
case R.id.menu_action_edit: case R.id.menu_action_edit:
// Try to make the editing fragment visible. // Try to make the editing fragment visible.
setIsEditing(true); setIsEditing(true);
return true; return true;
case R.id.menu_action_import:
final Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("*/*");
startActivityForResult(intent, REQUEST_IMPORT);
return true;
case R.id.menu_action_save: case R.id.menu_action_save:
// This menu item is handled by the editing fragment. // This menu item is handled by the editing fragment.
return false; return false;

View File

@ -1,12 +1,15 @@
package com.wireguard.android; package com.wireguard.android;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Intent;
import android.content.res.Resources; import android.content.res.Resources;
import android.os.Bundle; import android.os.Bundle;
import android.view.ActionMode; import android.view.ActionMode;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.Menu; import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.AbsListView; import android.widget.AbsListView;
@ -26,28 +29,49 @@ import java.util.List;
*/ */
public class ConfigListFragment extends BaseConfigFragment { public class ConfigListFragment extends BaseConfigFragment {
private ListView listView; private static final int REQUEST_IMPORT = 1;
private ConfigListFragmentBinding binding;
@Override
public void onActivityResult(final int requestCode, final int resultCode, final Intent data) {
if (requestCode == REQUEST_IMPORT) {
if (resultCode == Activity.RESULT_OK)
VpnService.getInstance().importFrom(data.getData());
} else {
super.onActivityResult(requestCode, resultCode, data);
}
}
@Override @Override
public void onCreate(final Bundle savedInstanceState) { public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
}
@Override
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
inflater.inflate(R.menu.config_list, menu);
} }
@Override @Override
public View onCreateView(final LayoutInflater inflater, final ViewGroup parent, public View onCreateView(final LayoutInflater inflater, final ViewGroup parent,
final Bundle savedInstanceState) { final Bundle savedInstanceState) {
final ConfigListFragmentBinding binding = binding = ConfigListFragmentBinding.inflate(inflater, parent, false);
ConfigListFragmentBinding.inflate(inflater, parent, false);
binding.setConfigs(VpnService.getInstance().getConfigs()); binding.setConfigs(VpnService.getInstance().getConfigs());
listView = binding.configList; binding.addFromFile.setOnClickListener(new View.OnClickListener() {
listView.setMultiChoiceModeListener(new ConfigListModeListener()); @Override
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() { public void onClick(final View view) {
final Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("*/*");
startActivityForResult(intent, REQUEST_IMPORT);
binding.addMenu.collapse();
}
});
binding.addFromScratch.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(final View view) {
startActivity(new Intent(getActivity(), AddActivity.class));
binding.addMenu.collapse();
}
});
binding.configList.setMultiChoiceModeListener(new ConfigListModeListener());
binding.configList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override @Override
public void onItemClick(final AdapterView<?> parent, final View view, public void onItemClick(final AdapterView<?> parent, final View view,
final int position, final long id) { final int position, final long id) {
@ -55,16 +79,29 @@ public class ConfigListFragment extends BaseConfigFragment {
setCurrentConfig(config); setCurrentConfig(config);
} }
}); });
listView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() { binding.configList.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
@Override @Override
public boolean onItemLongClick(final AdapterView<?> parent, final View view, public boolean onItemLongClick(final AdapterView<?> parent, final View view,
final int position, final long id) { final int position, final long id) {
setConfigChecked(null); setConfigChecked(null);
listView.setChoiceMode(AbsListView.CHOICE_MODE_MULTIPLE_MODAL); binding.configList.setChoiceMode(AbsListView.CHOICE_MODE_MULTIPLE_MODAL);
listView.setItemChecked(position, true); binding.configList.setItemChecked(position, true);
return true; return true;
} }
}); });
binding.configList.setOnTouchListener(new View.OnTouchListener() {
@Override
@SuppressLint("ClickableViewAccessibility")
public boolean onTouch(final View view, final MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN &&
binding.configList.pointToPosition((int) event.getX(), (int) event.getY())
== ListView.INVALID_POSITION) {
binding.addMenu.collapse();
return true;
}
return false;
}
});
binding.executePendingBindings(); binding.executePendingBindings();
setConfigChecked(getCurrentConfig()); setConfigChecked(getCurrentConfig());
return binding.getRoot(); return binding.getRoot();
@ -75,31 +112,38 @@ public class ConfigListFragment extends BaseConfigFragment {
final BaseConfigActivity activity = ((BaseConfigActivity) getActivity()); final BaseConfigActivity activity = ((BaseConfigActivity) getActivity());
if (activity != null) if (activity != null)
activity.setCurrentConfig(config); activity.setCurrentConfig(config);
if (listView != null) if (binding != null)
setConfigChecked(config); setConfigChecked(config);
} }
@Override @Override
public void onDestroyView() { public void onDestroyView() {
super.onDestroyView(); super.onDestroyView();
listView = null; binding = null;
} }
private void setConfigChecked(final Config config) { private void setConfigChecked(final Config config) {
if (config != null) { if (config != null) {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked") final ObservableMapAdapter<String, Config> adapter =
final ObservableMapAdapter<String, Config> adapter = (ObservableMapAdapter<String, Config>) binding.configList.getAdapter();
(ObservableMapAdapter<String, Config>) listView.getAdapter();
final int position = adapter.getPosition(config.getName()); final int position = adapter.getPosition(config.getName());
if (position >= 0) if (position >= 0)
listView.setItemChecked(position, true); binding.configList.setItemChecked(position, true);
} else { } else {
final int position = listView.getCheckedItemPosition(); final int position = binding.configList.getCheckedItemPosition();
if (position >= 0) if (position >= 0)
listView.setItemChecked(position, false); binding.configList.setItemChecked(position, false);
} }
} }
public boolean tryCollapseMenu() {
if (binding != null && binding.addMenu.isExpanded()) {
binding.addMenu.collapse();
return true;
}
return false;
}
private class ConfigListModeListener implements AbsListView.MultiChoiceModeListener { private class ConfigListModeListener implements AbsListView.MultiChoiceModeListener {
private final List<Config> configsToRemove = new LinkedList<>(); private final List<Config> configsToRemove = new LinkedList<>();
@ -124,11 +168,11 @@ public class ConfigListFragment extends BaseConfigFragment {
public void onItemCheckedStateChanged(final ActionMode mode, final int position, public void onItemCheckedStateChanged(final ActionMode mode, final int position,
final long id, final boolean checked) { final long id, final boolean checked) {
if (checked) if (checked)
configsToRemove.add((Config) listView.getItemAtPosition(position)); configsToRemove.add((Config) binding.configList.getItemAtPosition(position));
else else
configsToRemove.remove(listView.getItemAtPosition(position)); configsToRemove.remove(binding.configList.getItemAtPosition(position));
final int count = configsToRemove.size(); final int count = configsToRemove.size();
final Resources resources = listView.getContext().getResources(); final Resources resources = binding.getRoot().getContext().getResources();
mode.setTitle(resources.getQuantityString(R.plurals.list_delete_title, count, count)); mode.setTitle(resources.getQuantityString(R.plurals.list_delete_title, count, count));
} }
@ -141,10 +185,10 @@ public class ConfigListFragment extends BaseConfigFragment {
@Override @Override
public void onDestroyActionMode(final ActionMode mode) { public void onDestroyActionMode(final ActionMode mode) {
configsToRemove.clear(); configsToRemove.clear();
listView.post(new Runnable() { binding.configList.post(new Runnable() {
@Override @Override
public void run() { public void run() {
listView.setChoiceMode(AbsListView.CHOICE_MODE_SINGLE); binding.configList.setChoiceMode(AbsListView.CHOICE_MODE_SINGLE);
// Restore the previous selection (before entering the action mode). // Restore the previous selection (before entering the action mode).
setConfigChecked(getCurrentConfig()); setConfigChecked(getCurrentConfig());
} }

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="4dp" />
<padding
android:bottom="4dp"
android:left="8dp"
android:right="8dp"
android:top="4dp" />
<solid android:color="?android:attr/colorPrimary" />
</shape>

View File

@ -10,11 +10,45 @@
type="com.wireguard.android.bindings.ObservableSortedMap&lt;String, com.wireguard.config.Config&gt;" /> type="com.wireguard.android.bindings.ObservableSortedMap&lt;String, com.wireguard.config.Config&gt;" />
</data> </data>
<ListView <RelativeLayout
android:id="@+id/config_list"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent">
android:choiceMode="singleChoice"
app:items="@{configs}" <ListView
app:layout="@{@layout/config_list_item}" /> android:id="@+id/config_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:choiceMode="singleChoice"
app:items="@{configs}"
app:layout="@{@layout/config_list_item}" />
<com.getbase.floatingactionbutton.FloatingActionsMenu
android:id="@+id/add_menu"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
android:layout_marginBottom="8dp"
android:layout_marginEnd="8dp"
app:fab_labelStyle="@style/fab_label"
app:fab_labelsPosition="left">
<com.getbase.floatingactionbutton.FloatingActionButton
android:id="@+id/add_from_file"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:fab_icon="@drawable/ic_action_save"
app:fab_size="mini"
app:fab_title="@string/add_from_file" />
<com.getbase.floatingactionbutton.FloatingActionButton
android:id="@+id/add_from_scratch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:fab_icon="@drawable/ic_action_edit"
app:fab_size="mini"
app:fab_title="@string/add_from_scratch" />
</com.getbase.floatingactionbutton.FloatingActionsMenu>
</RelativeLayout>
</layout> </layout>

View File

@ -1,15 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/menu_action_add"
android:alphabeticShortcut="n"
android:icon="@drawable/ic_action_add"
android:showAsAction="always"
android:title="@string/add" />
<item
android:id="@+id/menu_action_import"
android:alphabeticShortcut="o"
android:icon="@drawable/ic_action_open"
android:showAsAction="ifRoom"
android:title="@string/import_config" />
</menu>

View File

@ -4,8 +4,9 @@
<item quantity="one">%d configuration selected</item> <item quantity="one">%d configuration selected</item>
<item quantity="other">%d configurations selected</item> <item quantity="other">%d configurations selected</item>
</plurals> </plurals>
<string name="add">Add empty config</string>
<string name="add_activity_title">New WireGuard configuration</string> <string name="add_activity_title">New WireGuard configuration</string>
<string name="add_from_file">Add from file</string>
<string name="add_from_scratch">Add from scratch</string>
<string name="add_peer">Add peer</string> <string name="add_peer">Add peer</string>
<string name="addresses">Addresses</string> <string name="addresses">Addresses</string>
<string name="allowed_ips">Allowed IPs</string> <string name="allowed_ips">Allowed IPs</string>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:android="http://schemas.android.com/apk/res/android">
<style name="fab_label" parent="android:TextAppearance.DeviceDefault.Inverse">
<item name="android:background">@drawable/fab_label_background</item>
</style>
</resources>