Java tutorial
/* * Copyright (C) 2010-2014 The MPDroid Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.audiokernel.euphonyrmt.fragments; import com.mobeta.android.dslv.DragSortController; import com.mobeta.android.dslv.DragSortListView; import com.audiokernel.euphonyrmt.MPDApplication; import com.audiokernel.euphonyrmt.MainMenuActivity; import com.audiokernel.euphonyrmt.R; import com.audiokernel.euphonyrmt.helpers.AlbumCoverDownloadListener; import com.audiokernel.euphonyrmt.helpers.AlbumInfo; import com.audiokernel.euphonyrmt.helpers.CoverAsyncHelper; import com.audiokernel.euphonyrmt.helpers.CoverDownloadListener; import com.audiokernel.euphonyrmt.helpers.QueueControl; import com.audiokernel.euphonyrmt.library.SimpleLibraryActivity; import com.audiokernel.euphonyrmt.models.AbstractPlaylistMusic; import com.audiokernel.euphonyrmt.models.PlaylistSong; import com.audiokernel.euphonyrmt.models.PlaylistStream; import com.audiokernel.euphonyrmt.tools.Tools; import com.audiokernel.euphonyrmt.views.holders.PlayQueueViewHolder; import org.a0z.mpd.MPD; import org.a0z.mpd.MPDPlaylist; import org.a0z.mpd.MPDStatus; import org.a0z.mpd.event.StatusChangeListener; import org.a0z.mpd.exception.MPDException; import org.a0z.mpd.item.Item; import org.a0z.mpd.item.Music; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.os.Build; import android.os.Bundle; import android.support.annotation.LayoutRes; import android.support.v4.app.FragmentActivity; import android.support.v4.app.ListFragment; import android.util.Log; import android.util.SparseBooleanArray; import android.view.ActionMode; 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.view.inputmethod.InputMethodManager; import android.widget.AbsListView; import android.widget.AbsListView.MultiChoiceModeListener; import android.widget.ArrayAdapter; import android.widget.BaseAdapter; import android.widget.EditText; import android.widget.ImageView; import android.widget.ListAdapter; import android.widget.ListView; import android.widget.PopupMenu; import android.widget.PopupMenu.OnMenuItemClickListener; import android.widget.SearchView; import android.widget.SearchView.OnQueryTextListener; import android.widget.TextView; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Locale; import static android.text.TextUtils.isEmpty; /** * A fragment for showing the media player's current playlist queue. */ public class QueueFragment extends ListFragment implements StatusChangeListener, OnMenuItemClickListener { protected static final boolean DEBUG = false; // Minimum number of songs in the queue before the fastscroll thumb is shown protected static final int MIN_SONGS_BEFORE_FASTSCROLL = 50; private static final String TAG = "QueueFragment"; protected final MPDApplication mApp = MPDApplication.getInstance(); protected final boolean mLightTheme = mApp.isLightThemeSelected(); protected ActionMode mActionMode; protected FragmentActivity mActivity; protected DragSortController mController; protected String mFilter = null; protected final DragSortListView.DropListener mDropListener = new DragSortListView.DropListener() { @Override public void drop(final int from, final int to) { if (from != to && mFilter == null) { final AbstractPlaylistMusic itemFrom = mSongList.get(from); final int songID = itemFrom.getSongId(); QueueControl.run(QueueControl.MOVE, songID, to); } } }; protected int mLastPlayingID = -1; protected DragSortListView mList; protected String mPlaylistToSave = ""; protected Integer mPopupSongID; protected final View.OnClickListener mItemMenuButtonListener = new View.OnClickListener() { @Override public void onClick(final View v) { mPopupSongID = (Integer) v.getTag(); final PopupMenu popupMenu = new PopupMenu(mActivity, v); popupMenu.getMenuInflater().inflate(R.menu.mpd_playlistcnxmenu, popupMenu.getMenu()); if (getPlaylistItemSong(mPopupSongID).isStream()) { popupMenu.getMenu().findItem(R.id.PLCX_goto).setVisible(false); } popupMenu.setOnMenuItemClickListener(QueueFragment.this); popupMenu.show(); } }; protected ViewGroup mRootView; protected SearchView mSearchView; protected ArrayList<AbstractPlaylistMusic> mSongList; @Override public void connectionStateChanged(final boolean connected, final boolean connectionLost) { } protected AbstractPlaylistMusic getPlaylistItemSong(final int songID) { AbstractPlaylistMusic song = null; for (final AbstractPlaylistMusic music : mSongList) { if (music.getSongId() == songID) { song = music; break; } } return song; } protected boolean isFiltered(final String item) { final String processedItem; if (item == null) { processedItem = ""; } else { processedItem = item.toLowerCase(Locale.getDefault()); } return processedItem.contains(mFilter); } @Override public void libraryStateChanged(final boolean updating, final boolean dbChanged) { } @Override public void onActivityCreated(final Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); mActivity = getActivity(); refreshListColorCacheHint(); } /* * Create Menu for Playlist View * @see android.app.Activity#onCreateOptionsMenu(android.view.Menu) */ @Override public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); inflater.inflate(R.menu.mpd_playlistmenu, menu); menu.removeItem(R.id.PLM_EditPL); } @Override public View onCreateView(final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState) { super.onCreateView(inflater, container, savedInstanceState); mRootView = container; final View view = inflater.inflate(R.layout.playlist_activity, container, false); mSearchView = (SearchView) view.findViewById(R.id.search); mSearchView.setOnQueryTextListener(new OnQueryTextListener() { @Override public boolean onQueryTextChange(final String newText) { mFilter = newText; if (newText != null && newText.isEmpty()) { mFilter = null; } if (mFilter != null) { mFilter = mFilter.toLowerCase(); } mList.setDragEnabled(mFilter == null); update(false); return false; } @Override public boolean onQueryTextSubmit(final String query) { // Hide the keyboard and give focus to the list final InputMethodManager imm = (InputMethodManager) mActivity .getSystemService(Context.INPUT_METHOD_SERVICE); imm.hideSoftInputFromWindow(mSearchView.getWindowToken(), 0); mList.requestFocus(); return true; } }); mList = (DragSortListView) view.findViewById(android.R.id.list); mList.requestFocus(); mList.setDropListener(mDropListener); mController = new DragSortController(mList); mController.setDragHandleId(R.id.cover); mController.setRemoveEnabled(false); mController.setSortEnabled(true); mController.setDragInitMode(1); mList.setFloatViewManager(mController); mList.setOnTouchListener(mController); mList.setDragEnabled(true); refreshListColorCacheHint(); mList.setChoiceMode(AbsListView.CHOICE_MODE_MULTIPLE_MODAL); mList.setMultiChoiceModeListener(new MultiChoiceModeListener() { @Override public boolean onActionItemClicked(final ActionMode mode, final MenuItem item) { final SparseBooleanArray checkedItems = mList.getCheckedItemPositions(); final int count = mList.getCount(); final ListAdapter adapter = mList.getAdapter(); final int itemId = item.getItemId(); int j = 0; int[] positions = null; boolean result = true; if (itemId == R.id.menu_delete) { positions = new int[mList.getCheckedItemCount()]; for (int i = 0; i < count && j < positions.length; i++) { if (checkedItems.get(i)) { positions[j] = ((Music) adapter.getItem(i)).getSongId(); j++; } } } else if (itemId == R.id.menu_crop) { positions = new int[mList.getCount() - mList.getCheckedItemCount()]; for (int i = 0; i < count && j < positions.length; i++) { if (!checkedItems.get(i)) { positions[j] = ((Music) adapter.getItem(i)).getSongId(); j++; } } } else { result = false; } if (j > 0) { QueueControl.run(QueueControl.REMOVE_BY_ID, positions); mode.finish(); } return result; } @Override public boolean onCreateActionMode(final ActionMode mode, final Menu menu) { final MenuInflater inflater = mode.getMenuInflater(); inflater.inflate(R.menu.mpd_queuemenu, menu); return true; } @Override public void onDestroyActionMode(final ActionMode mode) { mActionMode = null; mController.setSortEnabled(true); } @Override public void onItemCheckedStateChanged(final ActionMode mode, final int position, final long id, final boolean checked) { final int selectCount = mList.getCheckedItemCount(); if (selectCount == 0) { mode.finish(); } if (selectCount == 1) { mode.setTitle(R.string.actionSongSelected); } else { mode.setTitle(getString(R.string.actionSongsSelected, selectCount)); } } @Override public boolean onPrepareActionMode(final ActionMode mode, final Menu menu) { mActionMode = mode; mController.setSortEnabled(false); return false; } }); return view; } @Override public void onListItemClick(final ListView l, final View v, final int position, final long id) { super.onListItemClick(l, v, position, id); final int song = ((Music) l.getAdapter().getItem(position)).getSongId(); QueueControl.run(QueueControl.SKIP_TO_ID, song); } @Override public boolean onMenuItemClick(final MenuItem item) { final Intent intent; final AbstractPlaylistMusic music; switch (item.getItemId()) { case R.id.PLCX_playNext: QueueControl.run(QueueControl.MOVE_TO_NEXT, mPopupSongID); Tools.notifyUser("Song moved to next in list"); break; case R.id.PLCX_moveFirst: // Move song to first in playlist QueueControl.run(QueueControl.MOVE, mPopupSongID, 0); Tools.notifyUser("Song moved to first in list"); break; case R.id.PLCX_moveLast: QueueControl.run(QueueControl.MOVE_TO_LAST, mPopupSongID); Tools.notifyUser("Song moved to last in list"); break; case R.id.PLCX_removeFromPlaylist: QueueControl.run(QueueControl.REMOVE_BY_ID, mPopupSongID); if (isAdded()) { Tools.notifyUser(R.string.deletedSongFromPlaylist); } break; case R.id.PLCX_removeAlbumFromPlaylist: if (DEBUG) { Log.d(TAG, "Remove Album " + mPopupSongID); } QueueControl.run(QueueControl.REMOVE_ALBUM_BY_ID, mPopupSongID); if (isAdded()) { Tools.notifyUser(R.string.deletedSongFromPlaylist); } break; case R.id.PLCX_goToArtist: music = getPlaylistItemSong(mPopupSongID); if (music == null || isEmpty(music.getArtist())) { break; } intent = new Intent(mActivity, SimpleLibraryActivity.class); intent.putExtra("artist", music.getArtistAsArtist()); startActivityForResult(intent, -1); break; case R.id.PLCX_goToAlbum: music = getPlaylistItemSong(mPopupSongID); if (music == null || isEmpty(music.getArtist()) || isEmpty(music.getAlbum())) { break; } intent = new Intent(mActivity, SimpleLibraryActivity.class); intent.putExtra("album", music.getAlbumAsAlbum()); startActivityForResult(intent, -1); break; case R.id.PLCX_goToFolder: music = getPlaylistItemSong(mPopupSongID); if (music == null || isEmpty(music.getFullPath())) { break; } intent = new Intent(mActivity, SimpleLibraryActivity.class); intent.putExtra("folder", music.getParent()); startActivityForResult(intent, -1); break; default: break; } return true; } @Override public boolean onOptionsItemSelected(final MenuItem item) { super.onOptionsItemSelected(item); // Menu actions... boolean result = true; if (item.getItemId() == R.id.PLM_Clear) { QueueControl.run(QueueControl.CLEAR); mSongList.clear(); if (isAdded()) { Tools.notifyUser(R.string.playlistCleared); } ((BaseAdapter) getListAdapter()).notifyDataSetChanged(); } else if (item.getItemId() == R.id.PLM_Save) { List<Item> playLists; try { playLists = mApp.oMPDAsyncHelper.oMPD.getPlaylists(); } catch (final IOException | MPDException e) { Log.e(TAG, "Failed to receive list of playlists.", e); playLists = new ArrayList<>(0); } Collections.sort(playLists); final String[] playlistsArray = new String[playLists.size() + 1]; for (int p = 0; p < playLists.size(); p++) { playlistsArray[p] = playLists.get(p).getName(); // old playlists } playlistsArray[playlistsArray.length - 1] = getResources().getString(R.string.newPlaylist); // "new playlist" mPlaylistToSave = playlistsArray[playlistsArray.length - 1]; new AlertDialog.Builder(mActivity) // dialog with list of playlists .setTitle(R.string.playlistName).setSingleChoiceItems(playlistsArray, playlistsArray.length - 1, new DialogInterface.OnClickListener() { @Override public void onClick(final DialogInterface dialog, final int which) { mPlaylistToSave = playlistsArray[which]; } }) .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { @Override public void onClick(final DialogInterface dialog, final int which) { savePlaylist(mPlaylistToSave); } }).setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { @Override public void onClick(final DialogInterface dialog, final int which) { // Do nothing. } }).create().show(); } else { result = false; } return result; } @Override public void onPause() { mApp.oMPDAsyncHelper.removeStatusChangeListener(this); super.onPause(); } @Override public void onResume() { super.onResume(); mApp.oMPDAsyncHelper.addStatusChangeListener(this); new Thread(new Runnable() { @Override public void run() { update(); } }).start(); } @Override public void playlistChanged(final MPDStatus mpdStatus, final int oldPlaylistVersion) { update(); } @Override public void randomChanged(final boolean random) { } protected void refreshListColorCacheHint() { if (mList != null) { if (mLightTheme) { mList.setCacheColorHint(getResources().getColor(android.R.color.background_light)); } else { mList.setCacheColorHint(getResources().getColor(R.color.nowplaying_background)); } } } protected void refreshPlaylistItemView(final AbstractPlaylistMusic... playlistSongs) { final int start = mList.getFirstVisiblePosition(); for (int i = start; i <= mList.getLastVisiblePosition(); i++) { final AbstractPlaylistMusic playlistMusic = (AbstractPlaylistMusic) mList.getAdapter().getItem(i); for (final AbstractPlaylistMusic song : playlistSongs) { if (playlistMusic.getSongId() == song.getSongId()) { final View view = mList.getChildAt(i - start); mList.getAdapter().getView(i, view, mList); } } } } @Override public void repeatChanged(final boolean repeating) { } void savePlaylist(final String name) { if (name.equals(getResources().getString(R.string.newPlaylist))) { // if "new playlist", show dialog with EditText for new playlist: final EditText input = new EditText(mActivity); new AlertDialog.Builder(mActivity).setTitle(R.string.newPlaylistPrompt).setView(input) .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { @Override public void onClick(final DialogInterface dialog, final int which) { final String name = input.getText().toString().trim(); if (!name.isEmpty() && !name.equals(MPD.STREAMS_PLAYLIST)) { // TODO: Need to warn user if they attempt to save to MPD.STREAMS_PLAYLIST savePlaylist(name); } } }).setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { @Override public void onClick(final DialogInterface dialog, final int which) { // Do nothing. } }).create().show(); } else if (!name.isEmpty()) { // actually save: QueueControl.run(QueueControl.SAVE_PLAYLIST, name); } } public void scrollToNowPlaying() { final int songPos = mApp.oMPDAsyncHelper.oMPD.getStatus().getSongPos(); if (songPos == -1) { Log.d(TAG, "Missing list item."); } else { if (mActivity instanceof MainMenuActivity) { ((MainMenuActivity) mActivity).showQueue(); } final ListView listView = getListView(); listView.requestFocusFromTouch(); listView.setSelection(songPos); listView.clearFocus(); } } @Override public void stateChanged(final MPDStatus mpdStatus, final int oldState) { } @Override public void stickerChanged(final MPDStatus mpdStatus) { } @Override public void trackChanged(final MPDStatus mpdStatus, final int oldTrack) { if (mSongList != null) { // Mark running track... for (final AbstractPlaylistMusic song : mSongList) { final int newPlay; if (song.getSongId() == mpdStatus.getSongId()) { if (mLightTheme) { newPlay = R.drawable.ic_media_play_light; } else { newPlay = R.drawable.ic_media_play; } } else { newPlay = 0; } if (song.getCurrentSongIconRefID() != newPlay) { song.setCurrentSongIconRefID(newPlay); refreshPlaylistItemView(song); } } } } void update() { update(true); } /** * Update the current playlist fragment. * * @param forcePlayingIDRefresh Force the current track to refresh. */ void update(final boolean forcePlayingIDRefresh) { // Save the scroll bar position to restore it after update final MPDPlaylist playlist = mApp.oMPDAsyncHelper.oMPD.getPlaylist(); final List<Music> musics = playlist.getMusicList(); final ArrayList<AbstractPlaylistMusic> newSongList = new ArrayList<>(musics.size()); if (mLastPlayingID == -1 || forcePlayingIDRefresh) { mLastPlayingID = mApp.oMPDAsyncHelper.oMPD.getStatus().getSongId(); } // The position in the song list of the currently played song int listPlayingID = -1; // Copy list to avoid concurrent exception for (final Music music : new ArrayList<>(musics)) { if (music == null) { continue; } final AbstractPlaylistMusic item; if (music.isStream()) { item = new PlaylistStream(music); } else { item = new PlaylistSong(music); } if (mFilter != null) { if (!(isFiltered(item.getAlbumArtist()) || isFiltered(item.getAlbum()) || isFiltered(item.getTitle()))) { continue; } } if (item.getSongId() == mLastPlayingID) { if (mLightTheme) { item.setCurrentSongIconRefID(R.drawable.ic_media_play_light); } else { item.setCurrentSongIconRefID(R.drawable.ic_media_play); } /** * Lie a little. Scroll to the previous song than the one playing. * That way it shows that there are other songs before it. */ listPlayingID = newSongList.size() - 1; } else { item.setCurrentSongIconRefID(0); } newSongList.add(item); } updateScrollbar(newSongList, listPlayingID); } public void updateCover(final AlbumInfo albumInfo) { final List<AbstractPlaylistMusic> musicsToBeUpdated = new ArrayList<>(mSongList.size()); for (final AbstractPlaylistMusic playlistMusic : mSongList) { final AlbumInfo abstractAlbumInfo = new AlbumInfo(playlistMusic); if (abstractAlbumInfo.equals(albumInfo)) { playlistMusic.setForceCoverRefresh(true); musicsToBeUpdated.add(playlistMusic); } } refreshPlaylistItemView(musicsToBeUpdated.toArray(new AbstractPlaylistMusic[musicsToBeUpdated.size()])); } /** * Updates the scrollbar. * * @param newSongList The updated list of songs for the playlist. * @param listPlayingID The current playing playlist id. */ protected void updateScrollbar(final ArrayList newSongList, final int listPlayingID) { mActivity.runOnUiThread(new Runnable() { /** * This is a helper method to workaround shortcomings of the fast scroll API. * * @param scrollbarStyle The {@code View} scrollbar style. * @param isAlwaysVisible The visibility of the scrollbar. */ private void refreshFastScrollStyle(final int scrollbarStyle, final boolean isAlwaysVisible) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { mList.setFastScrollAlwaysVisible(isAlwaysVisible); mList.setScrollBarStyle(scrollbarStyle); } else { mList.setScrollBarStyle(scrollbarStyle); mList.setFastScrollAlwaysVisible(isAlwaysVisible); } } @Override public void run() { final int firstVisibleElementIndex = mList.getFirstVisiblePosition(); final View firstVisibleItem = mList.getChildAt(0); final int firstVisiblePosition; final ArrayAdapter songs = new QueueAdapter(mActivity, R.layout.playlist_queue_item, newSongList); if (firstVisibleItem != null) { firstVisiblePosition = firstVisibleItem.getTop(); } else { firstVisiblePosition = 0; } setListAdapter(songs); mSongList = newSongList; songs.notifyDataSetChanged(); /** * Note : Setting the scrollbar style before setting the fast scroll state is very * important pre-KitKat, because of a bug. It is also very important post-KitKat * because it needs the opposite order or it won't show the FastScroll. * * This is so stupid I don't even .... argh. */ if (newSongList.size() >= MIN_SONGS_BEFORE_FASTSCROLL) { refreshFastScrollStyle(View.SCROLLBARS_INSIDE_INSET, true); } else { refreshFastScrollStyle(View.SCROLLBARS_INSIDE_OVERLAY, false); } if (mActionMode != null) { mActionMode.finish(); } // Restore the scroll bar position if (firstVisibleElementIndex == 0 || firstVisiblePosition == 0) { /** * Only scroll if there is a valid song to scroll to. 0 is a valid song but * does not require scroll anyway. Also, only scroll if it's the first update. * You don't want your playlist to scroll itself while you are looking at other * stuff. */ if (listPlayingID > 0 && getView() != null) { setSelection(listPlayingID); } } else { mList.setSelectionFromTop(firstVisibleElementIndex, firstVisiblePosition); } } }); } @Override public void volumeChanged(final MPDStatus mpdStatus, final int oldVolume) { } private class QueueAdapter extends ArrayAdapter<AbstractPlaylistMusic> { QueueAdapter(final Context context, @LayoutRes final int resource, final List<AbstractPlaylistMusic> data) { super(context, resource, data); } @Override public View getView(final int position, final View convertView, final ViewGroup parent) { final PlayQueueViewHolder viewHolder; final View view; if (convertView == null) { view = LayoutInflater.from(getContext()).inflate(R.layout.playlist_queue_item, mRootView); viewHolder = new PlayQueueViewHolder(); viewHolder.mArtist = (TextView) view.findViewById(android.R.id.text2); viewHolder.mTitle = (TextView) view.findViewById(android.R.id.text1); viewHolder.mPlay = (ImageView) view.findViewById(R.id.picture); viewHolder.mAlbumCover = (ImageView) view.findViewById(R.id.cover); viewHolder.mCoverHelper = new CoverAsyncHelper(); int height = viewHolder.mAlbumCover.getHeight(); // If the list is not displayed yet, the height is 0. // This is a problem, so set a fallback one. final int fallbackHeight = 128; if (height == 0) { height = fallbackHeight; } viewHolder.mCoverHelper.setCoverMaxSize(height); final CoverDownloadListener acd = new AlbumCoverDownloadListener(viewHolder.mAlbumCover); final AlbumCoverDownloadListener oldAcd = (AlbumCoverDownloadListener) viewHolder.mAlbumCover .getTag(R.id.AlbumCoverDownloadListener); if (oldAcd != null) { oldAcd.detach(); } viewHolder.mAlbumCover.setTag(R.id.AlbumCoverDownloadListener, acd); viewHolder.mAlbumCover.setTag(R.id.CoverAsyncHelper, viewHolder.mCoverHelper); viewHolder.mCoverHelper.addCoverDownloadListener(acd); viewHolder.mMenuButton = view.findViewById(R.id.menu); viewHolder.mMenuButton.setOnClickListener(mItemMenuButtonListener); view.setTag(viewHolder); } else { viewHolder = (PlayQueueViewHolder) convertView.getTag(); view = convertView; } final AbstractPlaylistMusic music = getItem(position); viewHolder.mArtist.setText(music.getPlaylistSubLine()); viewHolder.mTitle.setText(music.getPlayListMainLine()); viewHolder.mMenuButton.setTag(music.getSongId()); viewHolder.mPlay.setImageResource(music.getCurrentSongIconRefID()); final AlbumInfo albumInfo = new AlbumInfo(music); if (music.isForceCoverRefresh() || viewHolder.mAlbumCover.getTag() == null || !viewHolder.mAlbumCover.getTag().equals(albumInfo.getKey())) { if (!music.isForceCoverRefresh()) { final int noCoverResource = AlbumCoverDownloadListener.getNoCoverResource(); viewHolder.mAlbumCover.setImageResource(noCoverResource); } music.setForceCoverRefresh(false); viewHolder.mCoverHelper.downloadCover(albumInfo, false); } return view; } } }