/*- * Copyright (C) 2011 Peter Baldwin * * 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 org.peterbaldwin.vlcremote.fragment; import android.app.Activity; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.os.Bundle; import android.support.v4.app.LoaderManager; import android.support.v4.content.Loader; import android.text.TextUtils; import android.view.ContextMenu; import android.view.ContextMenu.ContextMenuInfo; import android.view.Gravity; import android.view.LayoutInflater; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView.AdapterContextMenuInfo; import android.widget.EditText; import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import org.peterbaldwin.client.android.vlcremote.R; import org.peterbaldwin.vlcremote.loader.DirectoryLoader; import org.peterbaldwin.vlcremote.loader.LibraryDirectoryLoader; import org.peterbaldwin.vlcremote.loader.LibraryNameLoader; import org.peterbaldwin.vlcremote.model.Directory; import org.peterbaldwin.vlcremote.model.File; import org.peterbaldwin.vlcremote.model.Preferences; import org.peterbaldwin.vlcremote.model.Reloadable; import org.peterbaldwin.vlcremote.model.Reloader; import org.peterbaldwin.vlcremote.model.Remote; import org.peterbaldwin.vlcremote.model.Tags; import org.peterbaldwin.vlcremote.net.xml.XmlContentHandler; import org.peterbaldwin.vlcremote.widget.DirectoryAdapter; public class BrowseFragment extends MediaListFragment implements LoaderManager.LoaderCallbacks<Remote<Directory>>, Reloadable { private interface Data { int DIRECTORY = 2; int LIBRARIES = 3; int LIBRARY_DIRECTORY = 4; } public interface State { final String DIRECTORY = "vlc:directory"; } private DirectoryAdapter mAdapter; private String mDirectory = "~"; private Preferences mPreferences; private TextView mTitle; private TextView mEmpty; @Override public void onAttach(Activity activity) { super.onAttach(activity); ((Reloader) activity).addReloadable(Tags.FRAGMENT_BROWSE, this); } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setHasOptionsMenu(true); mPreferences = Preferences.get(getActivity()); if (savedInstanceState == null) { mDirectory = mPreferences.getBrowseDirectory(); } else { mDirectory = savedInstanceState.getString(State.DIRECTORY); } } @Override public View onCreateView(LayoutInflater inflater, ViewGroup root, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.browse, root, false); mTitle = (TextView) view.findViewById(android.R.id.title); mEmpty = (TextView) view.findViewById(android.R.id.empty); return view; } @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putString(State.DIRECTORY, mDirectory); } @Override public void setEmptyText(CharSequence text) { mEmpty.setText(text); } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); mAdapter = new DirectoryAdapter(getActivity()); setListAdapter(mAdapter); registerForContextMenu(getListView()); if (getMediaServer() != null) { getLoaderManager().initLoader(getDirectoryType(mDirectory), Bundle.EMPTY, this); } } @Override public void onListItemClick(ListView l, View v, int position, long id) { File file = mAdapter.getItem(position); if (file.isDirectory()) { openDirectory(file.getNormalizedPath()); } else if(file.isLibrary()) { openDirectory(file.getNormalizedPath(), Data.LIBRARIES); } else if(file.isLibraryName() || file.isLibraryDir()) { openDirectory(file.getNormalizedPath(), Data.LIBRARY_DIRECTORY); } else { getMediaServer().status().command.input.play(file.getMrl(), file.getOptions()); } } @Override public void reload(Bundle args) { if(getActivity() != null) { String dir = args != null && args.containsKey(State.DIRECTORY) ? args.getString(State.DIRECTORY) : mDirectory; openDirectory(dir, getDirectoryType(dir)); } } private int getDirectoryType(String path) { if("library://".equals(path)) { return Data.LIBRARIES; } else if(path.startsWith("library://")) { return Data.LIBRARY_DIRECTORY; } return Data.DIRECTORY; } private void openDirectory(File file) { String path = file.getNormalizedPath(); openDirectory(path, getDirectoryType(path)); } private void openDirectory(String path, int directoryType) { mDirectory = path; mAdapter.clear(); getLoaderManager().restartLoader(directoryType, Bundle.EMPTY, this); } public void openDirectory(String path) { openDirectory(path, Data.DIRECTORY); } private File getFile(ContextMenuInfo menuInfo) { if (menuInfo instanceof AdapterContextMenuInfo) { AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo; if (info.position < mAdapter.getCount()) { return mAdapter.getItem(info.position); } } return null; } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.menu_refresh_directory: openDirectory(mDirectory, getDirectoryType(mDirectory)); return true; case R.id.menu_parent: openParentDirectory(); return true; case R.id.menu_libraries: openDirectory(File.LIBRARIES.getPath(), Data.LIBRARIES); return true; case R.id.menu_home: mDirectory = mPreferences.getHomeDirectory(); openDirectory(mDirectory, getDirectoryType(mDirectory)); return true; case R.id.menu_set_home: mPreferences.setHomeDirectory(mDirectory); showSetHomeToast(); return true; case R.id.menu_size_large: mPreferences.setTextSize(Preferences.TEXT_LARGE); mAdapter.notifyDataSetChanged(); return true; case R.id.menu_size_medium: mPreferences.setTextSize(Preferences.TEXT_MEDIUM); mAdapter.notifyDataSetChanged(); return true; case R.id.menu_size_small: mPreferences.setTextSize(Preferences.TEXT_SMALL); mAdapter.notifyDataSetChanged(); return true; default: return super.onOptionsItemSelected(item); } } private void openParentDirectory() { for (int position = 0, n = mAdapter.getCount(); position < n; position++) { File file = mAdapter.getItem(position); if (file.isParent()) { switch(getDirectoryType(file.getPath())) { case Data.LIBRARIES: openDirectory(File.LIBRARIES.getPath(), Data.LIBRARIES); return; case Data.LIBRARY_DIRECTORY: openDirectory(file.getNormalizedPath(), Data.LIBRARY_DIRECTORY); return; default: openDirectory(file.getNormalizedPath()); return; } } } if(!mAdapter.isEmpty()) { // Open the root directory if no parent. openDirectory(Directory.ROOT_DIRECTORY); } } private void showSetHomeToast() { Context context = getActivity(); CharSequence message = getString(R.string.sethome, getTitle()); Toast toast = Toast.makeText(context, message, Toast.LENGTH_LONG); toast.setGravity(Gravity.CENTER, 0, 0); toast.show(); } @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { super.onCreateContextMenu(menu, v, menuInfo); MenuInflater inflater = getActivity().getMenuInflater(); inflater.inflate(R.menu.browse_context, menu); File sel = getFile(menuInfo); boolean isBrowsable = sel != null && sel.isBrowsable(); boolean isPlayable = sel != null && !sel.isParent(); boolean isAddable = sel != null && isBrowsable && !(sel.isParent() || sel.isLibrary() || sel.isLibraryName() || sel.isLibraryDir()); boolean isLibraryContext = sel != null && sel.isLibraryName() && !sel.isParent(); menu.findItem(R.id.browse_context_open).setVisible(isBrowsable); menu.findItem(R.id.browse_context_play).setVisible(isPlayable); menu.findItem(R.id.browse_context_enqueue).setVisible(isPlayable); menu.findItem(R.id.browse_context_add_library).setVisible(isBrowsable && isAddable); menu.findItem(R.id.browse_context_remove_library).setVisible(isLibraryContext); menu.findItem(R.id.browse_context_remove_from_library).setVisible(isLibraryContext); menu.findItem(R.id.browse_context_stream).setVisible(false); } @Override public boolean onContextItemSelected(MenuItem item) { File file = getFile(item.getMenuInfo()); if(file != null) { switch (item.getItemId()) { case R.id.browse_context_open: openDirectory(file); return true; case R.id.browse_context_play: List<String> dirs = new ArrayList<String>(mAdapter.getRealPaths(file)); if(dirs.isEmpty()) { Toast.makeText(getActivity(), "Error: unable to find real path", Toast.LENGTH_SHORT).show(); return true; } getMediaServer().status().command.input.play(File.getMrl(dirs.get(0), file.getExtension()), file.getOptions()); for(int i = 1; i < dirs.size(); i++) { getMediaServer().status().command.input.enqueue(File.getMrl(dirs.get(i), file.getExtension())); } return true; // case R.id.browse_context_stream: // getMediaServer().status().command.input.play(file.getMrl(), // file.getStreamingOptions()); // Intent intent = file.getIntentForStreaming(getMediaServer().getAuthority()); // startActivity(intent); // return true; case R.id.browse_context_enqueue: for(String dir : mAdapter.getRealPaths(file)) { getMediaServer().status().command.input.enqueue(File.getMrl(dir, file.getExtension())); } // delay reloading playlist to give vlc time to queue and read metadata ((Reloader) getActivity()).reloadDelayed(Tags.FRAGMENT_PLAYLIST, null, 100); return true; case R.id.browse_context_add_library: displayAddToLibraryDialog(file); return true; case R.id.browse_context_remove_library: mPreferences.removeLibrary(file.getName()); openDirectory(mDirectory, Data.LIBRARIES); return true; case R.id.browse_context_remove_from_library: displayRemoveFromLibraryDialog(file); return true; } } return super.onContextItemSelected(item); } private void addDirectoryToLibrary(File file, String libraryName) { Set<String> libraryDirs = mPreferences.getLibraryDirectories(libraryName); String path = file.getNormalizedPath(); libraryDirs.add(path); mPreferences.setLibrary(libraryName, libraryDirs); String m = getString(R.string.toast_added_to_library, path, libraryName); Toast.makeText(getActivity(), m, Toast.LENGTH_SHORT).show(); } private void displayAddToLibraryDialog(final File file) { final List<String> libraries = new ArrayList<String>(mPreferences.getLibraries()); libraries.add(0, getString(R.string.context_add_library_new)); new AlertDialog.Builder(getActivity()) .setItems(libraries.toArray(new String[0]), new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { if(which == 0) { displayAddToNewLibraryDialog(file); } else { addDirectoryToLibrary(file, libraries.get(which)); } dialog.dismiss(); } }) .setTitle(R.string.context_add_library) .show(); } private void displayAddToNewLibraryDialog(final File file) { final Set<String> libraries = mPreferences.getLibraries(); final EditText e = new EditText(getActivity()); e.setHint(R.string.hint_library_add); final AlertDialog d = new AlertDialog.Builder(getActivity()) .setView(e) .setPositiveButton(R.string.ok, null) // set later .setNegativeButton(R.string.cancel, null) .setTitle(R.string.title_dialog_add_library) .create(); d.setOnShowListener(new DialogInterface.OnShowListener() { @Override public void onShow(final DialogInterface dialog) { d.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if(e.getText().toString().isEmpty()) { Toast.makeText(getActivity(), "Library name cannot be empty", Toast.LENGTH_SHORT).show(); } else if(libraries.contains(e.getText().toString())) { Toast.makeText(getActivity(), "Library name already exists", Toast.LENGTH_SHORT).show(); } else { addDirectoryToLibrary(file, e.getText().toString()); dialog.dismiss(); } } }); } }); d.show(); } private void displayRemoveFromLibraryDialog(final File file) { final List<String> libraryDirs = new ArrayList<String>(mPreferences.getLibraryDirectories(file.getName())); new AlertDialog.Builder(getActivity()) .setItems(libraryDirs.toArray(new String[0]), new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { libraryDirs.remove(which); if(libraryDirs.isEmpty()) { mPreferences.removeLibrary(file.getName()); openDirectory(mDirectory, Data.LIBRARIES); } else { mPreferences.setLibrary(file.getName(), new HashSet<String>(libraryDirs)); } dialog.dismiss(); } }) .setTitle(R.string.title_dialog_remove_from_library) .show(); } @Override public Loader<Remote<Directory>> onCreateLoader(int id, Bundle args) { mPreferences.setBrowseDirectory(mDirectory); setEmptyText(getText(R.string.loading)); switch(id) { case Data.LIBRARIES: return new LibraryNameLoader(getActivity(), getMediaServer()); case Data.LIBRARY_DIRECTORY: return new LibraryDirectoryLoader(getActivity(), getMediaServer(), mDirectory); default: return new DirectoryLoader(getActivity(), getMediaServer(), mDirectory); } } @Override public void onLoadFinished(Loader<Remote<Directory>> loader, Remote<Directory> result) { mAdapter.setDirectory(result.data); setEmptyText(getText(R.string.connection_error)); setTitle(result.data != null ? File.getNormalizedPath(mDirectory) : null); boolean isXMLError = result.error != null && XmlContentHandler.ERROR_INVALID_XML.equals(result.error.getMessage()); if (isEmptyDirectory(result.data) || isXMLError) { handleEmptyDirectory(); } } private void setTitle(CharSequence title) { mTitle.setText(TextUtils.isEmpty(title) ? "Root Directory" : title); } public CharSequence getTitle() { return mTitle.getText(); } private boolean isEmptyDirectory(Directory directory) { return directory != null ? directory.isEmpty() : false; } private void handleEmptyDirectory() { showEmptyDirectoryError(); String path = File.getNormalizedPath(mDirectory.concat("/..")); openDirectory(path, getDirectoryType(path)); } private void showEmptyDirectoryError() { Toast.makeText(getActivity(), R.string.browse_empty, Toast.LENGTH_LONG).show(); } /** {@inheritDoc} */ @Override public void onLoaderReset(Loader<Remote<Directory>> loader) { mAdapter.setDirectory(null); } // TODO: Automatically reload directory when connection is restored }