From 72ae846ea9ce4c50441bdc347d86a39231176f3e Mon Sep 17 00:00:00 2001 From: Arne Schwabe Date: Wed, 6 May 2020 17:42:12 +0200 Subject: Convert ListView in allowed apps to RecyclerView Closes #693 --- .../de/blinkt/openvpn/fragments/PackageAdapter.kt | 237 ++++++++++++++++++ .../openvpn/fragments/Settings_Allowed_Apps.kt | 272 ++------------------- 2 files changed, 262 insertions(+), 247 deletions(-) create mode 100644 main/src/ui/java/de/blinkt/openvpn/fragments/PackageAdapter.kt (limited to 'main/src/ui/java/de/blinkt/openvpn/fragments') diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/PackageAdapter.kt b/main/src/ui/java/de/blinkt/openvpn/fragments/PackageAdapter.kt new file mode 100644 index 00000000..29fcffc7 --- /dev/null +++ b/main/src/ui/java/de/blinkt/openvpn/fragments/PackageAdapter.kt @@ -0,0 +1,237 @@ +/* + * Copyright (c) 2012-2020 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn.fragments + +import android.Manifest +import android.content.Context +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import android.text.TextUtils +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.* +import androidx.recyclerview.widget.RecyclerView +import de.blinkt.openvpn.R +import de.blinkt.openvpn.VpnProfile +import org.jetbrains.anko.runOnUiThread +import java.util.* + +internal class AppViewHolder(var rootView : View) : RecyclerView.ViewHolder(rootView) { + var mInfo: ApplicationInfo? = null + lateinit var appName: TextView + lateinit var appIcon: ImageView + //public TextView appSize; + //public TextView disabled; + lateinit var checkBox: CompoundButton + + companion object { + + fun create(inflater: LayoutInflater, parent: ViewGroup): AppViewHolder { + val view = 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. + val holder = AppViewHolder(view) + holder.appName = view.findViewById(R.id.app_name) as TextView + holder.appIcon = view.findViewById(R.id.app_icon) as ImageView + //holder.appSize = (TextView) convertView.findViewById(R.id.app_size); + //holder.disabled = (TextView) convertView.findViewById(R.id.app_disabled); + holder.checkBox = view.findViewById(R.id.app_selected) as CompoundButton + view.tag = holder + + + return holder + } + + fun createSettingsHolder(inflater: LayoutInflater, parent: ViewGroup): AppViewHolder { + + val settingsView = inflater.inflate(R.layout.allowed_application_settings, parent, false) + + val holder = AppViewHolder(settingsView) + settingsView.tag = holder + return holder + } + } + +} + +internal class PackageAdapter(c: Context, vp: VpnProfile) : RecyclerView.Adapter(), Filterable, CompoundButton.OnCheckedChangeListener { + private val mInflater: LayoutInflater = LayoutInflater.from(c) + private val mPm: PackageManager = c.packageManager + private var mPackages: Vector = Vector() + private val mFilter = ItemFilter() + private var mFilteredData: Vector = mPackages + private val mProfile = vp + + init { + setHasStableIds(true) + } + + + fun populateList(c: Context) { + val installedPackages = mPm.getInstalledApplications(PackageManager.GET_META_DATA) + + // Remove apps not using Internet + + var androidSystemUid = 0 + val apps = Vector() + + try { + val system = mPm.getApplicationInfo("android", PackageManager.GET_META_DATA) + androidSystemUid = system.uid + apps.add(system) + } catch (e: PackageManager.NameNotFoundException) { + } + + + for (app in installedPackages) { + + if (mPm.checkPermission(Manifest.permission.INTERNET, app.packageName) == PackageManager.PERMISSION_GRANTED && app.uid != androidSystemUid) { + + apps.add(app) + } + } + + Collections.sort(apps, ApplicationInfo.DisplayNameComparator(mPm)) + mPackages = apps + mFilteredData = apps + c.runOnUiThread { notifyDataSetChanged() } + } + + override fun getItemId(position: Int): Long { + if (position == 0) + return "settings".hashCode().toLong() + return mFilteredData[position - 1].packageName.hashCode().toLong() + } + + override fun onBindViewHolder(holder: AppViewHolder, position: Int) { + if (position == 0) { + bindSettingsView(holder) + } else + bindViewApp(position - 1, holder) + + } + internal fun changeDisallowText(allowTextView:TextView, selectedAreDisallowed: Boolean) { + if (selectedAreDisallowed) + allowTextView.setText(R.string.vpn_disallow_radio) + else + allowTextView.setText(R.string.vpn_allow_radio) + } + + private fun bindSettingsView(holder: AppViewHolder) { + val settingsView = holder.rootView + val allowTextView = settingsView.findViewById(R.id.default_allow_text) as TextView + + val vpnOnDefaultSwitch = settingsView.findViewById(R.id.default_allow) as Switch + + changeDisallowText(allowTextView, mProfile.mAllowedAppsVpnAreDisallowed) + vpnOnDefaultSwitch.isChecked = mProfile.mAllowedAppsVpnAreDisallowed + vpnOnDefaultSwitch.setOnCheckedChangeListener { _, isChecked -> + mProfile.mAllowedAppsVpnAreDisallowed = isChecked + notifyDataSetChanged() + } + + + + val vpnAllowBypassSwitch = settingsView.findViewById(R.id.allow_bypass) as Switch + + vpnAllowBypassSwitch.setOnCheckedChangeListener { _, isChecked -> mProfile.mAllowAppVpnBypass = isChecked } + + vpnAllowBypassSwitch.isChecked = mProfile.mAllowAppVpnBypass + } + + fun bindViewApp(position: Int, viewHolder: AppViewHolder){ + viewHolder.mInfo = mFilteredData[position] + val mInfo = mFilteredData[position] + + + var appName = mInfo.loadLabel(mPm) + + if (TextUtils.isEmpty(appName)) + appName = mInfo.packageName + viewHolder.appName.text = appName + viewHolder.appIcon.setImageDrawable(mInfo.loadIcon(mPm)) + viewHolder.checkBox.tag = mInfo.packageName + viewHolder.checkBox.setOnCheckedChangeListener(this) + + + viewHolder.checkBox.isChecked = mProfile.mAllowedAppsVpn.contains(mInfo.packageName) + } + + + override fun onCheckedChanged(buttonView: CompoundButton, isChecked: Boolean) { + val packageName = buttonView.tag as String + if (isChecked) { + mProfile.mAllowedAppsVpn.add(packageName) + } else { + mProfile.mAllowedAppsVpn.remove(packageName) + } + } + + override fun getFilter(): Filter { + return mFilter + } + + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AppViewHolder { + if (viewType == 0) + return AppViewHolder.createSettingsHolder(mInflater, parent) + else + return AppViewHolder.create(mInflater, parent) + } + + override fun getItemCount(): Int { + return mFilteredData.size + 1 + + } + + override fun getItemViewType(position: Int): Int { + return if (position == 0) 0 else 1 + } + + private inner class ItemFilter : Filter() { + override fun performFiltering(constraint: CharSequence): FilterResults { + + val filterString = constraint.toString().toLowerCase(Locale.getDefault()) + + val results = FilterResults() + + + val count = mPackages.size + val nlist = Vector(count) + + for (i in 0 until count) { + val pInfo = mPackages[i] + var appName = pInfo.loadLabel(mPm) + + if (TextUtils.isEmpty(appName)) + appName = pInfo.packageName + + if (appName is String) { + if (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 fun publishResults(constraint: CharSequence, results: FilterResults) { + + mFilteredData = results.values as Vector + notifyDataSetChanged() + } + + } + + +} \ No newline at end of file diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Allowed_Apps.kt b/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Allowed_Apps.kt index f745d2e0..2c113d71 100644 --- a/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Allowed_Apps.kt +++ b/main/src/ui/java/de/blinkt/openvpn/fragments/Settings_Allowed_Apps.kt @@ -5,47 +5,27 @@ package de.blinkt.openvpn.fragments -import android.Manifest -import android.content.Context -import android.content.pm.ApplicationInfo -import android.content.pm.PackageManager import android.os.Bundle -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.view.* 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.Switch import android.widget.TextView +import android.widget.Toast import androidx.appcompat.widget.SearchView import androidx.fragment.app.Fragment - -import java.util.Collections -import java.util.Locale -import java.util.Vector - +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.RecyclerView import de.blinkt.openvpn.R import de.blinkt.openvpn.VpnProfile import de.blinkt.openvpn.core.ProfileManager -import org.jetbrains.anko.runOnUiThread /** * Created by arne on 16.11.14. */ -class Settings_Allowed_Apps : Fragment(), AdapterView.OnItemClickListener, CompoundButton.OnCheckedChangeListener, View.OnClickListener { - private lateinit var mListView: ListView +class Settings_Allowed_Apps : Fragment(), AdapterView.OnItemClickListener, View.OnClickListener { + private lateinit var mListView: RecyclerView private lateinit var mProfile: VpnProfile - private lateinit var mDefaultAllowTextView: TextView - private lateinit var mListAdapter: PackageAdapter + private lateinit var packageAdapter: PackageAdapter private lateinit var mSettingsView: View @@ -58,18 +38,8 @@ class Settings_Allowed_Apps : Fragment(), AdapterView.OnItemClickListener, Compo } - override fun onCheckedChanged(buttonView: CompoundButton, isChecked: Boolean) { - val packageName = buttonView.tag as String - if (isChecked) { - mProfile.mAllowedAppsVpn.add(packageName) - } else { - mProfile.mAllowedAppsVpn.remove(packageName) - } - } - override fun onResume() { super.onResume() - changeDisallowText(mProfile.mAllowedAppsVpnAreDisallowed) } override fun onCreate(savedInstanceState: Bundle?) { @@ -87,21 +57,21 @@ class Settings_Allowed_Apps : Fragment(), AdapterView.OnItemClickListener, Compo val searchView = menu.findItem(R.id.app_search_widget).actionView as SearchView searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { override fun onQueryTextSubmit(query: String): Boolean { - mListView.setFilterText(query) - mListView.isTextFilterEnabled = true + packageAdapter.filter.filter(query) return true } - override fun onQueryTextChange(newText: String): Boolean { - mListView.setFilterText(newText) - mListView.isTextFilterEnabled = !TextUtils.isEmpty(newText) + override fun onQueryTextChange(query: String): Boolean { + packageAdapter.filter.filter(query) + //mListView.setFilterText(newText) + //mListView.isTextFilterEnabled = !TextUtils.isEmpty(newText) return true } }) searchView.setOnCloseListener { - mListView.clearTextFilter() - mListAdapter.filter.filter("") + //mListView.clearTextFilter() + packageAdapter.filter.filter("") false } @@ -113,213 +83,21 @@ class Settings_Allowed_Apps : Fragment(), AdapterView.OnItemClickListener, Compo override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val v = inflater.inflate(R.layout.allowed_vpn_apps, container, false) - mSettingsView = inflater.inflate(R.layout.allowed_application_settings, container, false) - mDefaultAllowTextView = mSettingsView.findViewById(R.id.default_allow_text) as TextView - - val vpnOnDefaultSwitch = mSettingsView.findViewById(R.id.default_allow) as Switch - - vpnOnDefaultSwitch.setOnCheckedChangeListener { _, isChecked -> - changeDisallowText(isChecked) - mProfile.mAllowedAppsVpnAreDisallowed = isChecked - } - - vpnOnDefaultSwitch.isChecked = mProfile.mAllowedAppsVpnAreDisallowed - - val vpnAllowBypassSwitch = mSettingsView.findViewById(R.id.allow_bypass) as Switch + mListView = v.findViewById(R.id.app_recycler_view) as RecyclerView - vpnAllowBypassSwitch.setOnCheckedChangeListener { _, isChecked -> mProfile.mAllowAppVpnBypass = isChecked } + packageAdapter = PackageAdapter(requireContext(), mProfile) + mListView.setHasFixedSize(true) + mListView.adapter = packageAdapter - vpnAllowBypassSwitch.isChecked = mProfile.mAllowAppVpnBypass - - mListView = v.findViewById(android.R.id.list) as ListView - - mListAdapter = PackageAdapter(requireContext(), mProfile) - mListView.adapter = mListAdapter - mListView.onItemClickListener = this - - mListView.emptyView = v.findViewById(R.id.loading_container) - - Thread(Runnable { mListAdapter.populateList(requireContext()) }).start() + Thread(Runnable { + packageAdapter.populateList(requireContext()) + activity?.runOnUiThread({ + (v.findViewById(R.id.loading_container)).visibility = View.GONE + (v.findViewById(R.id.app_recycler_view)).visibility = View.VISIBLE + }) + }).start() return v } - private fun changeDisallowText(selectedAreDisallowed: Boolean) { - if (selectedAreDisallowed) - mDefaultAllowTextView.setText(R.string.vpn_disallow_radio) - else - mDefaultAllowTextView.setText(R.string.vpn_allow_radio) - } - - internal class AppViewHolder { - var mInfo: ApplicationInfo? = null - var rootView: View? = null - lateinit var appName: TextView - lateinit var appIcon: ImageView - //public TextView appSize; - //public TextView disabled; - lateinit var checkBox: CompoundButton - - companion object { - - fun createOrRecycle(inflater: LayoutInflater, oldview: View?, parent: ViewGroup): AppViewHolder { - var convertView = oldview - 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. - val holder = AppViewHolder() - holder.rootView = convertView - holder.appName = convertView.findViewById(R.id.app_name) as TextView - holder.appIcon = convertView.findViewById(R.id.app_icon) as ImageView - //holder.appSize = (TextView) convertView.findViewById(R.id.app_size); - //holder.disabled = (TextView) convertView.findViewById(R.id.app_disabled); - holder.checkBox = convertView.findViewById(R.id.app_selected) as CompoundButton - convertView.tag = holder - - - return holder - } else { - // Get the ViewHolder back to get fast access to the TextView - // and the ImageView. - return convertView.tag as AppViewHolder - } - } - } - - } - - internal inner class PackageAdapter(c: Context, vp: VpnProfile) : BaseAdapter(), Filterable { - private val mInflater: LayoutInflater = LayoutInflater.from(c) - private val mPm: PackageManager = c.packageManager - private var mPackages: Vector = Vector() - private val mFilter = ItemFilter() - private var mFilteredData: Vector = mPackages - private val mProfile = vp - - - fun populateList(c: Context) { - val installedPackages = mPm.getInstalledApplications(PackageManager.GET_META_DATA) - - // Remove apps not using Internet - - var androidSystemUid = 0 - val apps = Vector() - - try { - val system = mPm.getApplicationInfo("android", PackageManager.GET_META_DATA) - androidSystemUid = system.uid - apps.add(system) - } catch (e: PackageManager.NameNotFoundException) { - } - - - for (app in installedPackages) { - - if (mPm.checkPermission(Manifest.permission.INTERNET, app.packageName) == PackageManager.PERMISSION_GRANTED && app.uid != androidSystemUid) { - - apps.add(app) - } - } - - Collections.sort(apps, ApplicationInfo.DisplayNameComparator(mPm)) - mPackages = apps - mFilteredData = apps - c.runOnUiThread { notifyDataSetChanged() } - } - - override fun getCount(): Int { - return mFilteredData.size + 1 - } - - override fun getItem(position: Int): Any { - return mFilteredData[position - 1] - } - - override fun getItemId(position: Int): Long { - if (position == 0) - return "settings".hashCode().toLong() - return mFilteredData[position - 1].packageName.hashCode().toLong() - } - - override fun getView(position: Int, convertView: View?, parent: ViewGroup): View? { - if (position == 0) { - return mSettingsView - } else - return getViewApp(position - 1, convertView, parent) - - } - - fun getViewApp(position: Int, convertView: View?, parent: ViewGroup): View? { - val viewHolder = AppViewHolder.createOrRecycle(mInflater, convertView, parent) - viewHolder.mInfo = mFilteredData[position] - val mInfo = mFilteredData[position] - - - var appName = mInfo.loadLabel(mPm) - - if (TextUtils.isEmpty(appName)) - appName = mInfo.packageName - viewHolder.appName.text = appName - viewHolder.appIcon.setImageDrawable(mInfo.loadIcon(mPm)) - viewHolder.checkBox.tag = mInfo.packageName - viewHolder.checkBox.setOnCheckedChangeListener(this@Settings_Allowed_Apps) - - - viewHolder.checkBox.isChecked = mProfile.mAllowedAppsVpn.contains(mInfo.packageName) - return viewHolder.rootView - } - - override fun getFilter(): Filter { - return mFilter - } - - override fun getViewTypeCount(): Int { - return 2; - } - - override fun getItemViewType(position: Int): Int { - return if (position == 0) 0 else 1 - } - - private inner class ItemFilter : Filter() { - override fun performFiltering(constraint: CharSequence): FilterResults { - - val filterString = constraint.toString().toLowerCase(Locale.getDefault()) - - val results = FilterResults() - - - val count = mPackages.size - val nlist = Vector(count) - - for (i in 0 until count) { - val pInfo = mPackages[i] - var appName = pInfo.loadLabel(mPm) - - if (TextUtils.isEmpty(appName)) - appName = pInfo.packageName - - if (appName is String) { - if (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 fun publishResults(constraint: CharSequence, results: FilterResults) { - mFilteredData = results.values as Vector - notifyDataSetChanged() - } - - } - } } -- cgit v1.2.3