/* * Created by Angel Leon (@gubatron), Alden Torres (aldenml) * Copyright (c) 2011, 2012, FrostWire(TM). All rights reserved. * * 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 <http://www.gnu.org/licenses/>. */ package com.bt.download.android.gui.views; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import android.app.Dialog; import android.content.Context; import android.util.Log; import android.util.SparseArray; import android.view.KeyEvent; import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnKeyListener; import android.view.View.OnLongClickListener; import android.view.ViewGroup; import android.widget.BaseAdapter; import android.widget.CheckBox; import android.widget.CompoundButton; import android.widget.CompoundButton.OnCheckedChangeListener; import android.widget.Filter; import android.widget.Filterable; import android.widget.LinearLayout; import com.bt.download.android.R; import com.frostwire.util.Ref; /** * * We extend from ListAdapter to populate our ListViews. * This one allows us to click and long click on the elements of our ListViews. * * @author gubatron * @author aldenml * * @param <T> */ public abstract class AbstractListAdapter<T> extends BaseAdapter implements Filterable { private static String TAG = "FW.AbstractListAdapter"; private final WeakReference<Context> context; private final int viewItemId; private final OnClickListener viewOnClickListener; private final ViewOnLongClickListener viewOnLongClickListener; private final ViewOnKeyListener viewOnKeyListener; private final CheckboxOnCheckedChangeListener checkboxOnCheckedChangeListener; private ListAdapterFilter<T> filter; private boolean checkboxesVisibility; private boolean showMenuOnClick; private final List<Dialog> dialogs; protected List<T> list; protected Set<T> checked; protected List<T> visualList; public AbstractListAdapter(Context context, int viewItemId, List<T> list, Set<T> checked) { this.context = new WeakReference<Context>(context); this.viewItemId = viewItemId; this.viewOnClickListener = new ViewOnClickListener(); this.viewOnLongClickListener = new ViewOnLongClickListener(); this.viewOnKeyListener = new ViewOnKeyListener(); this.checkboxOnCheckedChangeListener = new CheckboxOnCheckedChangeListener(); this.dialogs = new ArrayList<Dialog>(); this.list = list.equals(Collections.emptyList()) ? new ArrayList<T>() : list; this.checked = checked; this.visualList = list; } public AbstractListAdapter(Context context, int viewItemId, List<T> list) { this(context, viewItemId, list, new HashSet<T>()); } public AbstractListAdapter(Context context, int viewItemId) { this(context, viewItemId, new ArrayList<T>(), new HashSet<T>()); } public int getViewItemId() { return viewItemId; } public boolean hasStableIds() { return true; } public boolean areAllItemsEnabled() { return true; } public boolean isEnabled(int position) { return true; } public int getItemViewType(int position) { return 0; } public int getViewTypeCount() { return 1; } public boolean isEmpty() { return getCount() == 0; } public Set<T> getChecked() { return checked; } public void clearChecked() { if (checked != null && checked.size() > 0) { checked.clear(); notifyDataSetChanged(); } } public void checkAll() { checked.clear(); if (visualList != null) { checked.addAll(visualList); } notifyDataSetChanged(); } public Context getContext() { Context result = null; if (Ref.alive(context)) { result = context.get(); } return result; } /** This will return the count for the current file type */ public int getCount() { return visualList == null ? 0 : visualList.size(); } /** Should return the total count for all file types. */ public int getTotalCount() { return list == null ? 0 : list.size(); } public T getItem(int position) { return visualList.get(position); } public long getItemId(int position) { return position; } public void setList(List<T> list) { this.list = list.equals(Collections.emptyList()) ? new ArrayList<T>() : list; this.visualList = this.list; this.checked.clear(); notifyDataSetInvalidated(); } public void addList(List<T> g, boolean checked) { visualList.addAll(g); if (visualList != list) { list.addAll(g); } if (checked) { this.checked.addAll(g); } notifyDataSetChanged(); } /** * Adds new results to the existing list. * @param g */ public void addList(List<T> g) { addList(g, false); } public void addItem(T item) { addItem(item, true); } public void addItem(T item, boolean visible) { if (visible) { visualList.add(item); if (visualList != list) { list.add(item); } } else { if (visualList == list) { visualList = new ArrayList<T>(list); } list.add(item); } notifyDataSetChanged(); } public void deleteItem(T item) { visualList.remove(item); if (visualList != list) { list.remove(item); } if (checked.contains(item)) { checked.remove(item); } notifyDataSetChanged(); } public void updateList(List<T> g) { list = g; visualList = g; checked.clear(); notifyDataSetChanged(); } public void clear() { if (list != null) { list.clear(); } if (visualList != null) { visualList.clear(); } if (checked != null) { checked.clear(); } notifyDataSetInvalidated(); } public List<T> getList() { return list; } /** * Inflates the view out of the XML. * * Sets click and long click listeners in case you need them. (Override onItemClicked and onItemLongClicked) * * Let's the adapter know that the view has been created, in case you need to go deeper and create * more advanced click behavior or even add new Views during runtime. * * It will also bind the data to the view, you can refer to it if you need it by doing a .getTag() * */ public View getView(int position, View view, ViewGroup parent) { T item = getItem(position); Context ctx = getContext(); if (view == null && ctx != null) { // every list view item is wrapped in a generic container which has a hidden checkbox on the left hand side. view = View.inflate(ctx, R.layout.view_selectable_list_item, null); LinearLayout container = findView(view, R.id.view_selectable_list_item_container); View.inflate(ctx, viewItemId, container); } try { initTouchFeedback(view, item); initCheckBox(view, item); populateView(view, item); } catch (Throwable e) { Log.e(TAG, "Fatal error getting view: " + e.getMessage(), e); } return view; } public Filter getFilter() { return new AbstractListAdapterFilter(this, filter); } /** * So that results can be filtered. This discriminator should define which fields of T are the ones eligible for filtering. * @param discriminator */ public void setAdapterFilter(ListAdapterFilter<T> filter) { this.filter = filter; } public boolean getCheckboxesVisibility() { return checkboxesVisibility; } public void setCheckboxesVisibility(boolean checkboxesVisibility) { this.checkboxesVisibility = checkboxesVisibility; notifyDataSetChanged(); } public boolean getShowMenuOnClick() { return showMenuOnClick; } public void setShowMenuOnClick(boolean showMenuOnClick) { this.showMenuOnClick = showMenuOnClick; } public void dismissDialogs() { for (Dialog dialog : dialogs) { try { dialog.dismiss(); } catch (Throwable e) { Log.w(TAG, "Error dismissing dialog", e); } } } /** * Implement this method to refresh the UI contents of the List Item with the data. * @param view * @param data */ protected abstract void populateView(View view, T data); /** * Override this method if you want to do something when the overall List Item is clicked. * @param v */ protected void onItemClicked(View v) { } /** * Override this method if you want to do something when the overall List Item is long clicked. * @param v */ protected boolean onItemLongClicked(View v) { return false; } /** * Override this method if you want to do something when the DPAD or ENTER key is pressed and released. * This is some sort of master click. * * @param v * @return if handled */ protected boolean onItemKeyMaster(View v) { return false; } protected void onItemChecked(View v, boolean isChecked) { } /** * Helper function. * * @param <TView> * @param view * @param id * @return */ @SuppressWarnings("unchecked") protected final <TView extends View> TView findView(View view, int id) { return (TView) getView(view, getHolder(view), id); } private SparseArray<View> getHolder(View view) { @SuppressWarnings("unchecked") SparseArray<View> h = (SparseArray<View>) view.getTag(R.id.abstract_list_adapter_holder_tag_id); if (h == null) { h = new SparseArray<View>(); view.setTag(R.id.abstract_list_adapter_holder_tag_id, h); } return h; } private View getView(View view, SparseArray<View> h, int id) { View v = null; int index = h.indexOfKey(id); if (index < 0) { v = view.findViewById(id); h.put(id, v); } else { v = h.valueAt(index); } return v; } /** * If you want to create a menu per item, return here the menu adapter. * The menu will be created automatically and the vent long click will be eaten. */ protected MenuAdapter getMenuAdapter(View view) { return null; } protected Dialog trackDialog(Dialog dialog) { dialogs.add(dialog); return dialog; } /** * Sets up the behavior of a possible checkbox to check this item. * * Takes in consideration: * - Only so many views are created and reused by the ListView * - Setting the correct checked/unchecked value without triggering the onCheckedChanged event. * * @see getChecked() * * @param view * @param item */ private void initCheckBox(View view, T item) { CheckBox checkbox = findView(view, R.id.view_selectable_list_item_checkbox); if (checkbox != null) { checkbox.setVisibility((checkboxesVisibility) ? View.VISIBLE : View.GONE); // so we won't re-trigger a onCheckedChangeListener, we do this because views are re-used. checkbox.setOnCheckedChangeListener(null); checkbox.setChecked(checkboxesVisibility && checked.contains(item)); checkbox.setTag(item); checkbox.setOnCheckedChangeListener(checkboxOnCheckedChangeListener); } } private void initTouchFeedback(View v, T item) { if (v instanceof CheckBox) { return; } v.setOnClickListener(viewOnClickListener); v.setOnLongClickListener(viewOnLongClickListener); v.setOnKeyListener(viewOnKeyListener); v.setTag(item); if (v instanceof ViewGroup) { ViewGroup vg = (ViewGroup) v; int count = vg.getChildCount(); for (int i = 0; i < count; i++) { View child = vg.getChildAt(i); initTouchFeedback(child, item); } } } private final class ViewOnClickListener implements OnClickListener { public void onClick(View v) { if (showMenuOnClick) { MenuAdapter adapter = getMenuAdapter(v); if (adapter != null) { trackDialog(new MenuBuilder(adapter).show()); return; } } onItemClicked(v); } } private final class ViewOnLongClickListener implements OnLongClickListener { public boolean onLongClick(View v) { MenuAdapter adapter = getMenuAdapter(v); if (adapter != null) { trackDialog(new MenuBuilder(adapter).show()); return true; } return onItemLongClicked(v); } } private final class ViewOnKeyListener implements OnKeyListener { public boolean onKey(View v, int keyCode, KeyEvent event) { boolean handled = false; switch (keyCode) { case KeyEvent.KEYCODE_DPAD_CENTER: case KeyEvent.KEYCODE_ENTER: if (event.getAction() == KeyEvent.ACTION_UP) { handled = onItemKeyMaster(v); } } return handled; } } private final class CheckboxOnCheckedChangeListener implements OnCheckedChangeListener { @SuppressWarnings("unchecked") public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { T item = (T) buttonView.getTag(); if (isChecked && !checked.contains(item)) { checked.add(item); } else { checked.remove(item); } onItemChecked(buttonView, isChecked); } } private final class AbstractListAdapterFilter extends Filter { private final AbstractListAdapter<T> adapter; private final ListAdapterFilter<T> filter; public AbstractListAdapterFilter(AbstractListAdapter<T> adapter, ListAdapterFilter<T> filter) { this.adapter = adapter; this.filter = filter; } @Override protected FilterResults performFiltering(CharSequence constraint) { List<T> list = adapter.getList(); FilterResults result = new FilterResults(); if (filter == null) { /** || StringUtils.isNullOrEmpty(constraint.toString(), true)) { */ result.values = list; result.count = list.size(); } else { List<T> filtered = new ArrayList<T>(); int size = list.size(); for (int i = 0; i < size; i++) { T obj = list.get(i); if (filter.accept(obj, constraint)) { filtered.add(obj); } } result.values = filtered; result.count = filtered.size(); } return result; } @SuppressWarnings("unchecked") @Override protected void publishResults(CharSequence constraint, FilterResults results) { adapter.visualList = (List<T>) results.values; notifyDataSetInvalidated(); } } }