android: Add list fragment for the selection of apps
authorTobias Brunner <tobias@strongswan.org>
Tue, 27 Jun 2017 13:10:41 +0000 (15:10 +0200)
committerTobias Brunner <tobias@strongswan.org>
Mon, 3 Jul 2017 08:27:54 +0000 (10:27 +0200)
src/frontends/android/app/src/main/java/org/strongswan/android/ui/SelectedApplicationsListFragment.java [new file with mode: 0644]

diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/SelectedApplicationsListFragment.java b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/SelectedApplicationsListFragment.java
new file mode 100644 (file)
index 0000000..38c7494
--- /dev/null
@@ -0,0 +1,267 @@
+/*
+ * Copyright (C) 2017 Tobias Brunner
+ * HSR Hochschule fuer Technik Rapperswil
+ *
+ * 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 2 of the License, or (at your
+ * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
+ *
+ * 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.
+ */
+
+package org.strongswan.android.ui;
+
+import android.Manifest;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.v4.app.ListFragment;
+import android.support.v4.app.LoaderManager;
+import android.support.v4.content.AsyncTaskLoader;
+import android.support.v4.content.Loader;
+import android.support.v7.widget.SearchView;
+import android.text.TextUtils;
+import android.util.Pair;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.Filter;
+import android.widget.ListView;
+
+import org.strongswan.android.R;
+import org.strongswan.android.data.VpnProfileDataSource;
+import org.strongswan.android.ui.adapter.SelectedApplicationEntry;
+import org.strongswan.android.ui.adapter.SelectedApplicationsAdapter;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+public class SelectedApplicationsListFragment extends ListFragment implements LoaderManager.LoaderCallbacks<Pair<List<SelectedApplicationEntry>, List<String>>>, SearchView.OnQueryTextListener
+{
+       private SelectedApplicationsAdapter mAdapter;
+       private SortedSet<String> mSelection;
+
+       @Override
+       public void onActivityCreated(@Nullable Bundle savedInstanceState)
+       {
+               super.onActivityCreated(savedInstanceState);
+               setHasOptionsMenu(true);
+
+               getListView().setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
+
+               mAdapter = new SelectedApplicationsAdapter(getActivity());
+               setListAdapter(mAdapter);
+               setListShown(false);
+
+               ArrayList<String> selection;
+               if (savedInstanceState == null)
+               {
+                       selection = getActivity().getIntent().getStringArrayListExtra(VpnProfileDataSource.KEY_SELECTED_APPS_LIST);
+               }
+               else
+               {
+                       selection = savedInstanceState.getStringArrayList(VpnProfileDataSource.KEY_SELECTED_APPS_LIST);
+               }
+               mSelection = new TreeSet<>(selection);
+
+               getLoaderManager().initLoader(0, null, this);
+       }
+
+       @Override
+       public void onSaveInstanceState(Bundle outState)
+       {
+               super.onSaveInstanceState(outState);
+               outState.putStringArrayList(VpnProfileDataSource.KEY_SELECTED_APPS_LIST, new ArrayList<>(mSelection));
+       }
+
+       /**
+        * Returns the package names of all selected apps
+        */
+       public ArrayList<String> getSelectedApplications()
+       {
+               return new ArrayList<>(mSelection);
+       }
+
+       /**
+        * Track check state as ListView is unable to do that when using filters
+        */
+       @Override
+       public void onListItemClick(ListView l, View v, int position, long id)
+       {
+               super.onListItemClick(l, v, position, id);
+               SelectedApplicationEntry item = (SelectedApplicationEntry)getListView().getItemAtPosition(position);
+               item.setSelected(!item.isSelected());
+               if (item.isSelected())
+               {
+                       mSelection.add(item.getInfo().packageName);
+               }
+               else
+               {
+                       mSelection.remove(item.getInfo().packageName);
+               }
+       }
+
+       /**
+        * Manually set the check state as ListView is unable to track that when using filters
+        */
+       private void setCheckState()
+       {
+               for (int i = 0; i < getListView().getCount(); i++)
+               {
+                       SelectedApplicationEntry item = mAdapter.getItem(i);
+                       getListView().setItemChecked(i, item.isSelected());
+               }
+       }
+
+       @Override
+       public void onCreateOptionsMenu(Menu menu, MenuInflater inflater)
+       {
+               MenuItem item = menu.add(R.string.search);
+               item.setIcon(android.R.drawable.ic_menu_search);
+               item.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
+
+               SearchView sv = new SearchView(getActivity());
+               sv.setOnQueryTextListener(this);
+               item.setActionView(sv);
+
+               super.onCreateOptionsMenu(menu, inflater);
+       }
+
+       @Override
+       public Loader<Pair<List<SelectedApplicationEntry>, List<String>>> onCreateLoader(int id, Bundle args)
+       {
+               return new InstalledPackagesLoader(getActivity(), mSelection);
+       }
+
+       @Override
+       public void onLoadFinished(Loader<Pair<List<SelectedApplicationEntry>, List<String>>> loader, Pair<List<SelectedApplicationEntry>, List<String>> data)
+       {
+               mAdapter.setData(data.first);
+               mSelection.removeAll(data.second);
+               setCheckState();
+
+               if (isResumed())
+               {
+                       setListShown(true);
+               }
+               else
+               {
+                       setListShownNoAnimation(true);
+               }
+       }
+
+       @Override
+       public void onLoaderReset(Loader<Pair<List<SelectedApplicationEntry>, List<String>>> loader)
+       {
+               mAdapter.setData(null);
+       }
+
+       @Override
+       public boolean onQueryTextSubmit(String query)
+       {
+               return true;
+       }
+
+       @Override
+       public boolean onQueryTextChange(String newText)
+       {
+               String search = TextUtils.isEmpty(newText) ? null : newText;
+               mAdapter.getFilter().filter(search, new Filter.FilterListener()
+               {
+                       @Override
+                       public void onFilterComplete(int count)
+                       {
+                               setCheckState();
+                       }
+               });
+               return true;
+       }
+
+       public static class InstalledPackagesLoader extends AsyncTaskLoader<Pair<List<SelectedApplicationEntry>, List<String>>>
+       {
+               private final PackageManager mPackageManager;
+               private final SortedSet<String> mSelection;
+               private Pair<List<SelectedApplicationEntry>, List<String>> mData;
+
+               public InstalledPackagesLoader(Context context, SortedSet<String> selection)
+               {
+                       super(context);
+                       mPackageManager = context.getPackageManager();
+                       mSelection = selection;
+               }
+
+               @Override
+               public Pair<List<SelectedApplicationEntry>, List<String>> loadInBackground()
+               {
+                       List<SelectedApplicationEntry> apps = new ArrayList<>();
+                       SortedSet<String> seen = new TreeSet<>();
+                       for (ApplicationInfo info : mPackageManager.getInstalledApplications(PackageManager.GET_META_DATA))
+                       {
+                               /* skip apps that can't access the network anyway */
+                               if (mPackageManager.checkPermission(Manifest.permission.INTERNET, info.packageName) == PackageManager.PERMISSION_GRANTED)
+                               {
+                                       SelectedApplicationEntry entry = new SelectedApplicationEntry(mPackageManager, info);
+                                       entry.setSelected(mSelection.contains(info.packageName));
+                                       apps.add(entry);
+                                       seen.add(info.packageName);
+                               }
+                       }
+                       Collections.sort(apps);
+                       /* check for selected packages that don't exist anymore */
+                       List<String> missing = new ArrayList<>();
+                       for (String pkg : mSelection)
+                       {
+                               if (!seen.contains(pkg))
+                               {
+                                       missing.add(pkg);
+                               }
+                       }
+                       return new Pair<>(apps, missing);
+               }
+
+               @Override
+               protected void onStartLoading()
+               {
+                       if (mData != null)
+                       {       /* if we have data ready, deliver it directly */
+                               deliverResult(mData);
+                       }
+                       if (takeContentChanged() || mData == null)
+                       {
+                               forceLoad();
+                       }
+               }
+
+               @Override
+               public void deliverResult(Pair<List<SelectedApplicationEntry>, List<String>> data)
+               {
+                       if (isReset())
+                       {
+                               return;
+                       }
+                       mData = data;
+                       if (isStarted())
+                       {       /* if it is started we deliver the data directly,
+                                * otherwise this is handled in onStartLoading */
+                               super.deliverResult(data);
+                       }
+               }
+
+               @Override
+               protected void onReset()
+               {
+                       mData = null;
+                       super.onReset();
+               }
+       }
+}