/**
* This work is licensed under the Creative Commons Attribution-NonCommercial-
* NoDerivs 3.0 Unported License. To view a copy of this license, visit
* http://creativecommons.org/licenses/by-nc-nd/3.0/ or send a letter to
* Creative Commons, 444 Castro Street, Suite 900, Mountain View, California,
* 94041, USA.
*
* Use of this work is permitted only in accordance with license rights granted.
* Materials provided "AS IS"; no representations or warranties provided.
*
* Copyright � 2012 Marcus Parkkinen, Aki K�kel�, Fredrik �hs.
**/
package edu.chalmers.dat255.audiobookplayer.view;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import android.app.Activity;
import android.content.Context;
import android.database.Cursor;
import android.os.Bundle;
import android.os.Environment;
import android.provider.MediaStore;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.ImageButton;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import edu.chalmers.dat255.audiobookplayer.R;
import edu.chalmers.dat255.audiobookplayer.constants.Constants;
import edu.chalmers.dat255.audiobookplayer.util.BookCreator;
/**
* This class is used to display and add new books to the bookshelf. It lists
* all audio files and their file trees up to the ExternalStorageDirectory.
*
* @author Fredrik �hs
*
*/
public class BrowserActivity extends Activity {
protected static final String TAG = "BrowserActivity";
/**
* The different possible file types when browsing.
*
* @author Fredrik �hs
*
*/
public enum FILETYPE {
FILE {
public String toString() {
return "File";
}
},
FOLDER {
public String toString() {
return "Folder";
}
},
PARENT {
public String toString() {
return "Parent Folder";
}
};
}
private BrowserArrayAdapter adapter;
private ListView listView;
// checkedItems is used to save the checkedState of files while navigating
// through the file tree.
private Set<File> checkedItems;
private File currentDirectory;
private Map<TypedFile, List<TypedFile>> childMap;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_browser);
// generate the childMap
childMap = populateChildMap();
// set up the components
setUpComponents();
// get the file to the root of the file system
File f = new File(Environment.getExternalStorageDirectory()
.getAbsolutePath());
// fill the list view from the root
fill(f);
}
@Override
public void onBackPressed() {
Log.i(TAG, "Back pressed in Browser Activity");
finish();
}
/**
* Sets up the components of this activity.
*/
private void setUpComponents() {
checkedItems = new TreeSet<File>();
listView = (ListView) findViewById(R.id.browserList);
listView.setOnItemClickListener(new OnItemClickListener() {
public void onItemClick(AdapterView<?> arg0, View arg1,
int position, long arg3) {
TypedFile file = adapter.getItem(position);
// if a folder or the 'parent directory' is clicked, open it
if (file.getType().equals(FILETYPE.FOLDER)
|| file.getType().equals(FILETYPE.PARENT)) {
fill(file);
}
// otherwise, call onclick method
else {
onFileClick(file);
}
}
});
// set listener for button "Create Book"
Button createBookButton = (Button) findViewById(R.id.createBook);
createBookButton.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
createBook();
}
});
// create and listen to the back button
ImageButton backButton = (ImageButton) findViewById(R.id.backButton);
backButton.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
// finish this activity
finish();
Log.d(TAG, "Backed from browser.");
}
});
}
/**
* Private class used to create a book from the currently selected items.
*/
private void createBook() {
// check that some items are checked
if (!checkedItems.isEmpty()) {
// create a list of tracks to add
List<String> tracks = new ArrayList<String>();
String name = null;
for (File f : checkedItems) {
// only add tracks, not folders
if (f.isFile()) {
if (tracks.size() == 0) {
// this name is but a backup in the case that
// there is no album id3
name = f.getParentFile().getName();
}
tracks.add(f.getAbsolutePath());
}
}
// if we manage to create a book (the book is valid) then continue
// as normal
if (BookCreator.getInstance().createBookToBookshelf(tracks, name,
null)) {
// create a toast to notify the user that a book has been
// added.
Toast.makeText(BrowserActivity.this, "Book added: " + name,
Toast.LENGTH_SHORT).show();
// empty checkedItems and fill the list with unchecked items
checkedItems = new TreeSet<File>();
// refill the ListView from currentDirectory
fill(currentDirectory);
}
}
}
/**
* Fills the ListView with the children to root.
*
* @param root
* The folder which contents is to be listed.
*/
private void fill(File root) {
// in case root is not a directory, no items will be listed (should
// never happen)
if (root.isFile()) {
return;
}
currentDirectory = root;
List<TypedFile> directories = new ArrayList<TypedFile>();
// store files separately for correct sorting
List<TypedFile> files = new ArrayList<TypedFile>();
// check that the file exists in the previously populated childMap
if (childMap.get(root) != null) {
// add all files under root in childMap
for (TypedFile f : childMap.get(root)) {
if (f.isDirectory()) {
directories.add(f);
} else {
files.add(f);
}
}
// sort found directories and files
Collections.sort(directories);
Collections.sort(files);
// add all files under the directories
directories.addAll(files);
// adds an item listed as ".." of type parent topmost in the list
if (!root.getAbsolutePath()
.equals(Environment.getExternalStorageDirectory()
.getAbsolutePath())) {
directories.add(0,
new TypedFile(FILETYPE.PARENT, root.getParent()));
}
// create a new adapter with the found files/directories and add it
// to the listview
adapter = new BrowserArrayAdapter(this, R.layout.file_view,
directories, checkedItems, childMap);
listView.setAdapter(adapter);
} else {
// notify the user that no files were found on the system
Toast t = Toast.makeText(getApplicationContext(),
Constants.Message.NO_AUDIO_FILES_FOUND, Toast.LENGTH_SHORT);
t.show();
}
}
/**
* Finds all the tracks on the device and adds them and their parents to a
* map
*
* @return The map containing the files and their parents.
*/
private Map<TypedFile, List<TypedFile>> populateChildMap() {
/*
* this will prevent files such as notifications and ringtones to appear
* in the list
*/
String filtering = MediaStore.Audio.Media.IS_MUSIC + " != 0";
/*
* path to the audiofile
*/
String[] projection = { MediaStore.Audio.Media.DATA };
// this cursor will point at the paths of all music
Cursor cursor = this.managedQuery(
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection,
filtering, null, null);
Map<File, File> parentMap = new TreeMap<File, File>();
// cursor.moveToNext will iterate through all music on the device
while (cursor.moveToNext()) {
File child = new File(cursor.getString(cursor
.getColumnIndexOrThrow(MediaStore.Audio.Media.DATA)));
// loop through all tracks and put them with their parents as value
while (!child.getAbsolutePath()
.equals(Environment.getExternalStorageDirectory()
.getAbsolutePath())) {
if (parentMap.containsKey(child)) {
/*
* if the file is already in the map, so is all its parents
* and therefore further looping is redundant
*/
break;
}
parentMap.put(child, child.getParentFile());
child = child.getParentFile();
}
}
// reverse the list for easier iterating
Map<TypedFile, List<TypedFile>> fileMap = new TreeMap<TypedFile, List<TypedFile>>();
for (Entry<File, File> entry : parentMap.entrySet()) {
// if the map does not contain the parent, add the parent with a new
// list
TypedFile parent = new TypedFile(FILETYPE.FOLDER, entry.getValue()
.getAbsolutePath());
if (!fileMap.containsKey(parent)) {
fileMap.put(parent, new ArrayList<TypedFile>());
}
// add a child to the parents list of children
File child = entry.getKey();
if (child.isDirectory()) {
fileMap.get(parent)
.add(new TypedFile(FILETYPE.FOLDER, child
.getAbsolutePath()));
} else {
fileMap.get(parent).add(
new TypedFile(FILETYPE.FILE, child.getAbsolutePath()));
}
}
return fileMap;
}
/**
* Method to be called when a file of type file is clicked.
*
* @param file
* The file that was clicked.
*/
private void onFileClick(TypedFile file) {
Toast.makeText(this, "File Clicked: " + file.getName(),
Toast.LENGTH_SHORT).show();
}
/**
* Adapter to the browser.
*
* @author Fredrik �hs
*
*/
private class BrowserArrayAdapter extends ArrayAdapter<TypedFile> {
private Context c;
private int id;
private List<TypedFile> files;
private Set<File> checkedItems;
private Map<TypedFile, List<TypedFile>> childMap;
/**
* Constructor.
*
* @param context
* The current context.
* @param textViewResourceId
* The resource ID for the layout file used to display the
* information.
* @param filesToDisplay
* The list of the files to be displayed.
* @param checkedItems
* Storage for the items that are checked at the moment.
* @param childMap
* A map containing information regarding all the children
* which will be checked if a folder is checked.
*/
public BrowserArrayAdapter(Context context, int textViewResourceId,
List<TypedFile> filesToDisplay, Set<File> checkedItems,
Map<TypedFile, List<TypedFile>> childMap) {
super(context, textViewResourceId, filesToDisplay);
this.c = context;
this.id = textViewResourceId;
this.files = filesToDisplay;
this.checkedItems = checkedItems;
this.childMap = childMap;
}
/**
* Creates the view of each listview item
*/
public View getView(int position, View convertView, ViewGroup parent) {
View view = convertView;
// if the view is null,
if (view == null) {
LayoutInflater vi = (LayoutInflater) c
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
// inflate the view into itself
view = vi.inflate(id, null);
}
// get file as final since it wont change and it's needed in an
// anonymous class
final TypedFile file = files.get(position);
if (file != null) {
// get the TextView in the ListView item
TextView tv1 = (TextView) view.findViewById(R.id.TextView01);
TextView tv2 = (TextView) view.findViewById(R.id.TextView02);
if (tv1 != null) {
tv1.setText(file.getName());
}
if (tv2 != null) {
tv2.setText(file.getType().toString());
}
}
// set the state of the checkbox according to the checkedItems list
CheckBox cb = (CheckBox) view.findViewById(R.id.checkBox);
boolean checkState = checkedItems.contains(file);
cb.setChecked(checkState);
// set the on click listener of the checkbox
cb.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
// checks all children if a folder, or the file otherwise
checkAllChildren(file, ((CheckBox) v).isChecked());
}
});
return view;
}
/**
* (Un)check all the files within file recursively (in case it's a
* folder).
*
* @param file
* The file (folder) to check.
* @param checkState
* Whether to check or to uncheck the files.
*/
private void checkAllChildren(File file, boolean checkState) {
// if file is a regular file
if (file.isFile()) {
// check it and return
checkItem(file, checkState);
return;
}
// the get returns null if file is not contained within the map
List<TypedFile> list = childMap.get(file);
// if childMap contains the file, instantiate the list
if (list != null) {
for (File f : list) {
// check the item
checkItem(f, checkState);
// recurse
checkAllChildren(f, checkState);
}
}
}
/**
* Adds or removes the file from the checkedItems list
*
* @param file
* The file to add or remove.
* @param checkState
* Whether to add or remove the file.
*/
private void checkItem(File file, boolean checkState) {
if (checkState) {
checkedItems.add(file);
} else {
checkedItems.remove(file);
}
}
}
/**
* Simple class which extends File by also having a FILETYPE which
* identifies it in the list.
*
* @author Fredrik �hs
*
*/
private class TypedFile extends File {
private static final long serialVersionUID = 1L;
private FILETYPE type;
/**
* Constructor
*
* @param type
* The filetype of the file.
* @param path
* The path to the file.
*/
public TypedFile(FILETYPE type, String path) {
super(path);
this.type = type;
}
/**
*
* @return the type of the file.
*/
public FILETYPE getType() {
return type;
}
@Override
public String getName() {
// the name of a parent folder should be ..
if (type.equals(FILETYPE.PARENT)) {
return "..";
}
return super.getName();
}
}
}