/* * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ package com.nononsenseapps.filepicker; import android.app.Activity; import android.content.Context; import android.content.res.TypedArray; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.StyleRes; import android.support.v4.app.Fragment; import android.support.v4.app.LoaderManager; import android.support.v4.content.Loader; import android.support.v7.app.AppCompatActivity; import android.support.v7.util.SortedList; import android.support.v7.view.ContextThemeWrapper; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.Toolbar; import android.text.Editable; import android.text.TextWatcher; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.CheckBox; import android.widget.EditText; import android.widget.TextView; import android.widget.Toast; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import static com.nononsenseapps.filepicker.Utils.appendPath; /** * A fragment representing a list of Files. *

*

* Activities containing this fragment MUST implement the {@link * OnFilePickedListener} * interface. */ public abstract class AbstractFilePickerFragment extends Fragment implements LoaderManager.LoaderCallbacks>, NewItemFragment.OnNewFolderListener, LogicHandler { // The different preset modes of operation. This impacts the behaviour // and possible actions in the UI. public static final int MODE_FILE = 0; public static final int MODE_DIR = 1; public static final int MODE_FILE_AND_DIR = 2; public static final int MODE_NEW_FILE = 3; // Where to display on open. public static final String KEY_START_PATH = "KEY_START_PATH"; // See MODE_XXX constants above for possible values public static final String KEY_MODE = "KEY_MODE"; // If it should be possible to create directories. public static final String KEY_ALLOW_DIR_CREATE = "KEY_ALLOW_DIR_CREATE"; // Allow multiple items to be selected. public static final String KEY_ALLOW_MULTIPLE = "KEY_ALLOW_MULTIPLE"; // Allow an existing file to be selected under MODE_NEW_FILE public static final String KEY_ALLOW_EXISTING_FILE = "KEY_ALLOW_EXISTING_FILE"; // If file can be selected by clicking only and checkboxes are not visible public static final String KEY_SINGLE_CLICK = "KEY_SINGLE_CLICK"; // Used for saving state. protected static final String KEY_CURRENT_PATH = "KEY_CURRENT_PATH"; protected final HashSet mCheckedItems; protected final HashSet mCheckedVisibleViewHolders; protected int mode = MODE_FILE; protected T mCurrentPath = null; protected boolean allowCreateDir = false; protected boolean allowMultiple = false; protected boolean allowExistingFile = true; protected boolean singleClick = false; protected OnFilePickedListener mListener; protected FileItemAdapter mAdapter = null; protected TextView mCurrentDirView; protected EditText mEditTextFileName; protected RecyclerView recyclerView; protected LinearLayoutManager layoutManager; protected SortedList mFiles = null; protected Toast mToast = null; // Keep track if we are currently loading a directory, in case it takes a long time protected boolean isLoading = false; protected View mNewFileButtonContainer = null; protected View mRegularButtonContainer = null; /** * Mandatory empty constructor for the fragment manager to instantiate the * fragment (e.g. upon screen orientation changes). */ public AbstractFilePickerFragment() { mCheckedItems = new HashSet<>(); mCheckedVisibleViewHolders = new HashSet<>(); // Retain this fragment across configuration changes, to allow // asynctasks and such to be used with ease. setRetainInstance(true); } protected FileItemAdapter getAdapter() { return mAdapter; } protected FileItemAdapter getDummyAdapter() { return new FileItemAdapter<>(this); } /** * Set before making the fragment visible. This method will re-use the existing * arguments bundle in the fragment if it exists so extra arguments will not * be overwritten. This allows you to set any extra arguments in the fragment * constructor if you wish. *

* The key/value-pairs listed below will be overwritten however. * * @param startPath path to directory the picker will show upon start * @param mode what is allowed to be selected (dirs, files, both) * @param allowMultiple selecting a single item or several? * @param allowDirCreate can new directories be created? * @param allowExistingFile if selecting a "new" file, can existing files be chosen * @param singleClick selecting an item does not require a press on OK */ public void setArgs(@Nullable final String startPath, final int mode, final boolean allowMultiple, final boolean allowDirCreate, final boolean allowExistingFile, final boolean singleClick) { // Validate some assumptions so users don't get surprised (or get surprised early) if (mode == MODE_NEW_FILE && allowMultiple) { throw new IllegalArgumentException( "MODE_NEW_FILE does not support 'allowMultiple'"); } // Single click only makes sense if we are not selecting multiple items if (singleClick && allowMultiple) { throw new IllegalArgumentException("'singleClick' can not be used with 'allowMultiple'"); } // There might have been arguments set elsewhere, if so do not overwrite them. Bundle b = getArguments(); if (b == null) { b = new Bundle(); } if (startPath != null) { b.putString(KEY_START_PATH, startPath); } b.putBoolean(KEY_ALLOW_DIR_CREATE, allowDirCreate); b.putBoolean(KEY_ALLOW_MULTIPLE, allowMultiple); b.putBoolean(KEY_ALLOW_EXISTING_FILE, allowExistingFile); b.putBoolean(KEY_SINGLE_CLICK, singleClick); b.putInt(KEY_MODE, mode); setArguments(b); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { final Context contextThemeWrapper = new ContextThemeWrapper(getActivity(), getStyleId()); final LayoutInflater localInflater = inflater.cloneInContext(contextThemeWrapper); final View view = localInflater.inflate(R.layout.nnf_fragment_filepicker, container, false); Toolbar toolbar = (Toolbar) view.findViewById(R.id.nnf_picker_toolbar); if (toolbar != null) { setupToolbar(toolbar); } recyclerView = (RecyclerView) view.findViewById(android.R.id.list); // improve performance if you know that changes in content // do not change the size of the RecyclerView recyclerView.setHasFixedSize(true); // use a linear layout manager layoutManager = new LinearLayoutManager(getActivity()); recyclerView.setLayoutManager(layoutManager); // Set Item Decoration if exists configureItemDecoration(localInflater, recyclerView); // Set adapter mAdapter = new FileItemAdapter<>(this); recyclerView.setAdapter(mAdapter); view.findViewById(R.id.nnf_button_cancel) .setOnClickListener(new View.OnClickListener() { @Override public void onClick(final View v) { onClickCancel(v); } }); view.findViewById(R.id.nnf_button_ok).setOnClickListener(new View.OnClickListener() { @Override public void onClick(final View v) { onClickOk(v); } }); view.findViewById(R.id.nnf_button_ok_newfile).setOnClickListener( new View.OnClickListener() { @Override public void onClick(View v) { onClickOk(v); } }); mNewFileButtonContainer = view.findViewById(R.id.nnf_newfile_button_container); mRegularButtonContainer = view.findViewById(R.id.nnf_button_container); setModeView(view); mEditTextFileName = (EditText) view.findViewById(R.id.nnf_text_filename); mEditTextFileName.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { } @Override public void afterTextChanged(Editable s) { // deSelect anything selected since the user just modified the name clearSelections(); } }); mCurrentDirView = (TextView) view.findViewById(R.id.nnf_current_dir); // Restore state if (mCurrentPath != null && mCurrentDirView != null) { mCurrentDirView.setText(getFullPath(mCurrentPath)); } return view; } /** * Checks if a divider drawable has been defined in the current theme. If it has, will apply * an item decoration with the divider. If no divider has been specified, then does nothing. */ protected void configureItemDecoration(@NonNull LayoutInflater inflater, @NonNull RecyclerView recyclerView) { final TypedArray attributes = getActivity().obtainStyledAttributes(new int[]{R.attr.nnf_list_item_divider}); Drawable divider = attributes.getDrawable(0); attributes.recycle(); if (divider != null) { recyclerView.addItemDecoration(new DividerItemDecoration(divider)); } } /** * Called when the cancel-button is pressed. * * @param view which was clicked. Not used in default implementation. */ public void onClickCancel(@NonNull View view) { if (mListener != null) { mListener.onCancelled(); } } /** * Called when the ok-button is pressed. * * @param view which was clicked. Not used in default implementation. */ public void onClickOk(@NonNull View view) { if (mListener == null) { return; } // Some invalid cases first /*if (MODE_NEW_FILE == mode && !isValidFileName(getNewFileName())) { mToast = Toast.makeText(getActivity(), R.string.nnf_need_valid_filename, Toast.LENGTH_SHORT); mToast.show(); return; }*/ if ((allowMultiple || mode == MODE_FILE) && (mCheckedItems.isEmpty() || getFirstCheckedItem() == null)) { if (mToast == null) { mToast = Toast.makeText(getActivity(), R.string.nnf_select_something_first, Toast.LENGTH_SHORT); } mToast.show(); return; } // New file allows only a single file if (mode == MODE_NEW_FILE) { final String filename = getNewFileName(); final Uri result; if (filename.startsWith("/")) { // Return absolute paths directly result = toUri(getPath(filename)); } else { // Append to current directory result = toUri(getPath(appendPath(getFullPath(mCurrentPath), filename))); } mListener.onFilePicked(result); } else if (allowMultiple) { mListener.onFilesPicked(toUri(mCheckedItems)); } else if (mode == MODE_FILE) { //noinspection ConstantConditions mListener.onFilePicked(toUri(getFirstCheckedItem())); } else if (mode == MODE_DIR) { mListener.onFilePicked(toUri(mCurrentPath)); } else { // single FILE OR DIR if (mCheckedItems.isEmpty()) { mListener.onFilePicked(toUri(mCurrentPath)); } else { mListener.onFilePicked(toUri(getFirstCheckedItem())); } } } /** * @return filename as entered/picked by the user for the new file */ @NonNull protected String getNewFileName() { return mEditTextFileName.getText().toString(); } /** * Configure the toolbar anyway you like here. Default is to set it as the activity's * main action bar. Override if you already provide an action bar. * Not called if no toolbar was found. * * @param toolbar from layout with id "picker_toolbar" */ protected void setupToolbar(@NonNull Toolbar toolbar) { ((AppCompatActivity) getActivity()).setSupportActionBar(toolbar); } public @Nullable T getFirstCheckedItem() { //noinspection LoopStatementThatDoesntLoop for (T file : mCheckedItems) { return file; } return null; } protected @NonNull List toUri(@NonNull Iterable files) { ArrayList uris = new ArrayList<>(); for (T file : files) { uris.add(toUri(file)); } return uris; } public boolean isCheckable(@NonNull final T data) { final boolean checkable; if (isDir(data)) { checkable = ((mode == MODE_DIR && allowMultiple) || (mode == MODE_FILE_AND_DIR && allowMultiple)); } else { // File checkable = (mode == MODE_FILE || mode == MODE_FILE_AND_DIR || allowExistingFile); } return checkable; } @Override public void onAttach(Context context) { super.onAttach(context); try { mListener = (OnFilePickedListener) context; } catch (ClassCastException e) { throw new ClassCastException(context.toString() + " must implement OnFilePickedListener"); } } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setHasOptionsMenu(true); // Only if we have no state if (mCurrentPath == null) { if (savedInstanceState != null) { mode = savedInstanceState.getInt(KEY_MODE, mode); allowCreateDir = savedInstanceState .getBoolean(KEY_ALLOW_DIR_CREATE, allowCreateDir); allowMultiple = savedInstanceState .getBoolean(KEY_ALLOW_MULTIPLE, allowMultiple); allowExistingFile = savedInstanceState .getBoolean(KEY_ALLOW_EXISTING_FILE, allowExistingFile); singleClick = savedInstanceState .getBoolean(KEY_SINGLE_CLICK, singleClick); String path = savedInstanceState.getString(KEY_CURRENT_PATH); if (path != null) { mCurrentPath = getPath(path.trim()); } } else if (getArguments() != null) { mode = getArguments().getInt(KEY_MODE, mode); allowCreateDir = getArguments() .getBoolean(KEY_ALLOW_DIR_CREATE, allowCreateDir); allowMultiple = getArguments() .getBoolean(KEY_ALLOW_MULTIPLE, allowMultiple); allowExistingFile = getArguments() .getBoolean(KEY_ALLOW_EXISTING_FILE, allowExistingFile); singleClick = getArguments() .getBoolean(KEY_SINGLE_CLICK, singleClick); if (getArguments().containsKey(KEY_START_PATH)) { String path = getArguments().getString(KEY_START_PATH); if (path != null) { T file = getPath(path.trim()); if (isDir(file)) { mCurrentPath = file; } else { mCurrentPath = getParent(file); mEditTextFileName.setText(getName(file)); } } } } } // If still null if (mCurrentPath == null) { mCurrentPath = getRoot(); } refresh(mCurrentPath); } /** * Hides/Shows appropriate views depending on mode */ protected void setModeView(@NonNull View view) { boolean nf = mode == MODE_NEW_FILE; mNewFileButtonContainer.setVisibility(nf ? View.VISIBLE : View.GONE); mRegularButtonContainer.setVisibility(nf ? View.GONE : View.VISIBLE); if (!nf && singleClick) { view.findViewById(R.id.nnf_button_ok).setVisibility(View.GONE); } } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { inflater.inflate(R.menu.picker_actions, menu); MenuItem item = menu.findItem(R.id.nnf_action_createdir); item.setVisible(allowCreateDir); } @Override public boolean onOptionsItemSelected(MenuItem menuItem) { if (R.id.nnf_action_createdir == menuItem.getItemId()) { Activity activity = getActivity(); if (activity instanceof AppCompatActivity) { NewFolderFragment.showDialog(((AppCompatActivity) activity).getSupportFragmentManager(), AbstractFilePickerFragment.this); } return true; } else { return false; } } @Override public void onSaveInstanceState(Bundle b) { super.onSaveInstanceState(b); b.putString(KEY_CURRENT_PATH, mCurrentPath.toString()); b.putBoolean(KEY_ALLOW_MULTIPLE, allowMultiple); b.putBoolean(KEY_ALLOW_EXISTING_FILE, allowExistingFile); b.putBoolean(KEY_ALLOW_DIR_CREATE, allowCreateDir); b.putBoolean(KEY_SINGLE_CLICK, singleClick); b.putInt(KEY_MODE, mode); } @Override public void onDetach() { super.onDetach(); mListener = null; } /** * Refreshes the list. Call this when current path changes. This method also checks * if permissions are granted and requests them if necessary. See hasPermission() * and handlePermission(). By default, these methods do nothing. Override them if * you need to request permissions at runtime. * * @param nextPath path to list files for */ protected void refresh(@NonNull T nextPath) { if (hasPermission(nextPath)) { mCurrentPath = nextPath; isLoading = true; getLoaderManager() .restartLoader(0, null, AbstractFilePickerFragment.this); } else { handlePermission(nextPath); } } /** * If permission has not been granted yet, this method should request it. *

* Override only if you need to request a permission. * * @param path The path for which permission should be requested */ protected void handlePermission(@NonNull T path) { // Nothing to do by default } /** * If your implementation needs to request a specific permission to function, check if it * has been granted here. You should probably also override handlePermission() to request it. * * @param path the path for which permissions should be checked * @return true if permission has been granted, false otherwise. */ protected boolean hasPermission(@NonNull T path) { // Nothing to request by default return true; } /** * Instantiate and return a new Loader for the given ID. * * @param id The ID whose loader is to be created. * @param args Any arguments supplied by the caller. * @return Return a new Loader instance that is ready to start loading. */ @Override public Loader> onCreateLoader(final int id, final Bundle args) { return getLoader(); } /** * Called when a previously created loader has finished its load. * * @param loader The Loader that has finished. * @param data The data generated by the Loader. */ @Override public void onLoadFinished(final Loader> loader, final SortedList data) { isLoading = false; mCheckedItems.clear(); mCheckedVisibleViewHolders.clear(); mFiles = data; mAdapter.setList(data); if (mCurrentDirView != null) { mCurrentDirView.setText(getFullPath(mCurrentPath)); } } /** * Called when a previously created loader is being reset, and thus * making its data unavailable. The application should at this point * remove any references it has to the Loader's data. * * @param loader The Loader that is being reset. */ @Override public void onLoaderReset(final Loader> loader) { isLoading = false; mAdapter.setList(null); mFiles = null; } /** * @param position 0 - n, where the header has been subtracted * @param data the actual file or directory * @return an integer greater than 0 */ @Override public int getItemViewType(int position, @NonNull T data) { if (isCheckable(data)) { return LogicHandler.VIEWTYPE_CHECKABLE; } else { return LogicHandler.VIEWTYPE_DIR; } } @Override public void onBindHeaderViewHolder(@NonNull HeaderViewHolder viewHolder) { viewHolder.text.setText(".."); } /** * @param parent Containing view * @param viewType which the ViewHolder will contain * @return a view holder for a file or directory */ @NonNull @Override public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View v; final Context contextThemeWrapper = new ContextThemeWrapper(getActivity(), getStyleId()); final LayoutInflater localInflater = LayoutInflater.from(getActivity()).cloneInContext(contextThemeWrapper); switch (viewType) { case LogicHandler.VIEWTYPE_HEADER: v = localInflater.inflate(R.layout.nnf_filepicker_listitem_dir, parent, false); return new HeaderViewHolder(v); case LogicHandler.VIEWTYPE_CHECKABLE: v = localInflater.inflate(R.layout.nnf_filepicker_listitem_checkable, parent, false); return new CheckableViewHolder(v); case LogicHandler.VIEWTYPE_DIR: default: v = localInflater.inflate(R.layout.nnf_filepicker_listitem_dir, parent, false); return new DirViewHolder(v); } } @StyleRes protected abstract int getStyleId(); /** * @param vh to bind data from either a file or directory * @param position 0 - n, where the header has been subtracted * @param data the file or directory which this item represents */ @Override public void onBindViewHolder(@NonNull DirViewHolder vh, int position, @NonNull T data) { vh.file = data; vh.icon.setVisibility(isDir(data) ? View.VISIBLE : View.GONE); vh.text.setText(getName(data)); if (isCheckable(data)) { if (mCheckedItems.contains(data)) { mCheckedVisibleViewHolders.add((CheckableViewHolder) vh); ((CheckableViewHolder) vh).checkbox.setChecked(true); } else { //noinspection SuspiciousMethodCalls mCheckedVisibleViewHolders.remove(vh); ((CheckableViewHolder) vh).checkbox.setChecked(false); } } } /** * Animate de-selection of visible views and clear * selected set. */ public void clearSelections() { for (CheckableViewHolder vh : mCheckedVisibleViewHolders) { vh.checkbox.setChecked(false); } mCheckedVisibleViewHolders.clear(); mCheckedItems.clear(); } /** * Called when a header item ("..") is clicked. * * @param view that was clicked. Not used in default implementation. * @param viewHolder for the clicked view */ public void onClickHeader(@NonNull View view, @NonNull HeaderViewHolder viewHolder) { goUp(); } /** * Browses to the parent directory from the current directory. For example, if the current * directory is /foo/bar/, then goUp() will change the current directory to /foo/. It is up to * the caller to not call this in vain, e.g. if you are already at the root. *

* Currently selected items are cleared by this operation. */ public void goUp() { goToDir(getParent(mCurrentPath)); } /** * Called when a non-selectable item, typically a directory, is clicked. * * @param view that was clicked. Not used in default implementation. * @param viewHolder for the clicked view */ public void onClickDir(@NonNull View view, @NonNull DirViewHolder viewHolder) { if (isDir(viewHolder.file)) { goToDir(viewHolder.file); } } /** * Cab be used by the list to determine whether a file should be displayed or not. * Default behavior is to always display folders. If files can be selected, * then files are also displayed. In case a new file is supposed to be selected, * the {@link #allowExistingFile} determines if existing files are visible * * @param file either a directory or file. * @return True if item should be visible in the picker, false otherwise */ protected boolean isItemVisible(final T file) { return (isDir(file) || (mode == MODE_FILE || mode == MODE_FILE_AND_DIR) || (mode == MODE_NEW_FILE && allowExistingFile)); } /** * Browses to the designated directory. It is up to the caller verify that the argument is * in fact a directory. If another directory is in the process of being loaded, this method * will not start another load. *

* Currently selected items are cleared by this operation. * * @param file representing the target directory. */ public void goToDir(@NonNull T file) { if (!isLoading) { mCheckedItems.clear(); mCheckedVisibleViewHolders.clear(); refresh(file); } } /** * Long clicking a non-selectable item does nothing by default. * * @param view which was long clicked. Not used in default implementation. * @param viewHolder for the clicked view * @return true if the callback consumed the long click, false otherwise. */ public boolean onLongClickDir(@NonNull View view, @NonNull DirViewHolder viewHolder) { return false; } /** * Called when a selectable item is clicked. This might be either a file or a directory. * * @param view that was clicked. Not used in default implementation. * @param viewHolder for the clicked view */ public void onClickCheckable(@NonNull View view, @NonNull CheckableViewHolder viewHolder) { if (isDir(viewHolder.file)) { goToDir(viewHolder.file); } else { onLongClickCheckable(view, viewHolder); if (singleClick) { onClickOk(view); } } } /** * Long clicking a selectable item should toggle its selected state. Note that if only a * single item can be selected, then other potentially selected views on screen must be * de-selected. * * @param view which was long clicked. Not used in default implementation. * @param viewHolder for the clicked view * @return true if the callback consumed the long click, false otherwise. */ public boolean onLongClickCheckable(@NonNull View view, @NonNull CheckableViewHolder viewHolder) { if (MODE_NEW_FILE == mode) { mEditTextFileName.setText(getName(viewHolder.file)); } onClickCheckBox(viewHolder); return true; } /** * Called when a selectable item's checkbox is pressed. This should toggle its selected state. * Note that if only a single item can be selected, then other potentially selected views on * screen must be de-selected. The text box for new filename is also cleared. * * @param viewHolder for the item containing the checkbox. */ public void onClickCheckBox(@NonNull CheckableViewHolder viewHolder) { if (mCheckedItems.contains(viewHolder.file)) { viewHolder.checkbox.setChecked(false); mCheckedItems.remove(viewHolder.file); mCheckedVisibleViewHolders.remove(viewHolder); } else { if (!allowMultiple) { clearSelections(); } viewHolder.checkbox.setChecked(true); mCheckedItems.add(viewHolder.file); mCheckedVisibleViewHolders.add(viewHolder); } } /** * This interface must be implemented by activities that contain this * fragment to allow an interaction in this fragment to be communicated * to the activity and potentially other fragments contained in that * activity. *

* See the Android Training lesson Communicating with Other Fragments for more information. */ public interface OnFilePickedListener { void onFilePicked(@NonNull Uri file); void onFilesPicked(@NonNull List files); void onCancelled(); } public class HeaderViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { final TextView text; public HeaderViewHolder(View v) { super(v); v.setOnClickListener(this); text = (TextView) v.findViewById(android.R.id.text1); } /** * Called when a view has been clicked. * * @param v The view that was clicked. */ @Override public void onClick(View v) { onClickHeader(v, this); } } public class DirViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener { public View icon; public TextView text; public T file; public DirViewHolder(View v) { super(v); v.setOnClickListener(this); v.setOnLongClickListener(this); icon = v.findViewById(R.id.item_icon); text = (TextView) v.findViewById(android.R.id.text1); } /** * Called when a view has been clicked. * * @param v The view that was clicked. */ @Override public void onClick(View v) { onClickDir(v, this); } /** * Called when a view has been clicked and held. * * @param v The view that was clicked and held. * @return true if the callback consumed the long click, false otherwise. */ @Override public boolean onLongClick(View v) { return onLongClickDir(v, this); } } public class CheckableViewHolder extends DirViewHolder { public CheckBox checkbox; public CheckableViewHolder(View v) { super(v); boolean nf = mode == MODE_NEW_FILE; checkbox = (CheckBox) v.findViewById(R.id.checkbox); checkbox.setVisibility((nf || singleClick) ? View.GONE : View.VISIBLE); checkbox.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { onClickCheckBox(CheckableViewHolder.this); } }); } /** * Called when a view has been clicked. * * @param v The view that was clicked. */ @Override public void onClick(View v) { onClickCheckable(v, this); } /** * Called when a view has been clicked and held. * * @param v The view that was clicked and held. * @return true if the callback consumed the long click, false otherwise. */ @Override public boolean onLongClick(View v) { return onLongClickCheckable(v, this); } } }