Smartphones

Android coder's guide to browsing external storage

Android developer William Francis presents a simple list-based file browser activity that he used in one of his projects.

One of the standout capabilities on the Android platform is support for external storage. The ability to store data, media, and even applications on a removable SD card makes the platform more flexible and future-proof than others competing in the same space. As a developer, I find myself frequently taking advantage of the device's external storage and, for the most part, I am pleased with the libraries Google has provided me for tackling file i/o.

I say for the most part because there is one area where I find the SDK lacking. I would really like to see a file browser widget -- that is, a component that simplifies the job of tying the user interface to the underlying Dalvik file implementation. I see compelling reasons for Google to provide a file browsing widget. Chief among my reasoning is that an "official" component would provide a consistent user experience across an ecosystem that is incredibly diverse in terms of hardware, and particularly screen resolutions. Alas, until such a widget becomes available, we developers must make do.

The tutorial that follows demonstrates a simple list-based file browser activity I've used in one of my projects. You can download the entire source here, or follow along step-by-step.

1.       Create a new Android project in Eclipse targeted at Android 1.6 or greater.

2.       In your /res/drawable folder, we will need some images. I've included four image files: file.png, folder.png, root.png, and up.png. These files are used later in our custom adapter to serve as icons.

3.       We need some layouts, so add a main.xml to your /res/layout folder. The layout contains nothing more than a list view.

main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<ListView
android:id="@+id/android:list"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:dividerHeight="1dip"/>
</LinearLayout>

4.       We need to define the individual rows in our list view, and we'll do this in /res/layout/list_row.xml. To keep things simple, we'll just add one text view for the filename and one image view to host our icon.

list_row.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="?android:attr/listPreferredItemHeight"
android:padding="6dip">
<TableLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<TableRow>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="left"
android:padding="2dip"
android:id="@+id/fileicon"/>
<TextView
android:id="@+id/filename"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14sp"
android:padding="2dip"
android:textStyle="bold"
android:gravity="left|bottom"
android:layout_gravity="left|bottom"/>
</TableRow>
</TableLayout>
</LinearLayout>

5.       We move on to our /src folder. We'll want to create a custom list view adapter that handles displaying the filename and assigning the appropriate icon. Note that position 0 and position 1 in our list will always be special cases, as these will be our navigation buttons.

CustomAdapter.java
package com.authorwjf;
import java.io.File;
import java.util.ArrayList;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.TextView;
public class CustomAdapter extends ArrayAdapter<File> {
private ArrayList<File> items;
private Context c = null;
/**
* Standard Data Adapter Construction
*/
public CustomAdapter(Context context, int textViewResourceId, ArrayList<File> items) {
super(context, textViewResourceId, items);
this.items = items;
this.c = context;
}
/**
* Code invoked when container notifies data set of change.
*/

@Override

public View getView(int position, View convertView, ViewGroup parent) {
View v = convertView;
if (v == null) {
LayoutInflater vi = (LayoutInflater)c.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
v = vi.inflate(R.layout.list_row, null);
}
TextView filename = null;
ImageView fileicon = null;
File f = items.get(position);
if (f != null) {
filename = (TextView) v.findViewById(R.id.filename);
fileicon = (ImageView) v.findViewById(R.id.fileicon);
}
if (filename != null) {
if (position == 0) {
filename.setText(f.getAbsolutePath());
} else if (position == 1) {
filename.setText(f.getAbsolutePath());
} else {
filename.setText(f.getName());
}
}
if (fileicon != null) {
if (position == 0) {
fileicon.setImageResource(R.drawable.root);
} else if (position == 1) {
fileicon.setImageResource(R.drawable.up);
} else if (f.isDirectory()) {
fileicon.setImageResource(R.drawable.folder);
} else {
fileicon.setImageResource(R.drawable.file);
}
}
return v;
}
}

6.       We need to implement the Main.java file to extend our list view and handle the list item clicks. The method refreshFileList handles the management of our data adapter content. It's worth taking a closer look at the onSaveInstanceState override as well; this is where we preserve the root, current, and last node. If you removed this function and the associated logic in the onCreate method, you'd find that rotating the phone would return the user to the root directory, which while not a fatal error, is certainly an inconvenience.

Main.java
package com.authorwjf;
import java.io.File;
import java.util.ArrayList;
import android.app.ListActivity;
import android.os.Bundle;
import android.os.Environment;
import android.view.View;
import android.widget.ListView;
import android.widget.Toast;
public class Main extends ListActivity {
private File mCurrentNode = null;
private File mLastNode = null;
private File mRootNode = null;
private ArrayList<File> mFiles = new ArrayList<File>();
private CustomAdapter mAdapter = null;
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
mAdapter = new CustomAdapter(this, R.layout.list_row, mFiles);
setListAdapter(mAdapter);
if (savedInstanceState != null) {
mRootNode = (File)savedInstanceState.getSerializable("root_node");
mLastNode = (File)savedInstanceState.getSerializable("last_node");
mCurrentNode = (File)savedInstanceState.getSerializable("current_node");
}
refreshFileList();

}

private void refreshFileList() {
if (mRootNode == null) mRootNode = new File(Environment.getExternalStorageDirectory().toString());
if (mCurrentNode == null) mCurrentNode = mRootNode;
mLastNode = mCurrentNode;
File[] files = mCurrentNode.listFiles();
mFiles.clear();
mFiles.add(mRootNode);
mFiles.add(mLastNode);
if (files!=null) {
for (int i = 0; i< files.length; i++) mFiles.add(files[i]);
}
mAdapter.notifyDataSetChanged();
}
@Override
protected void onSaveInstanceState(Bundle outState) {
outState.putSerializable("root_node", mRootNode);
outState.putSerializable("current_node", mCurrentNode);
outState.putSerializable("last_node", mLastNode);
super.onSaveInstanceState(outState);
}
/**
* Listview on click handler.
*/
@Override
public void onListItemClick(ListView parent, View v, int position, long id){
File f = (File) parent.getItemAtPosition(position);
if (position == 1) {
if (mCurrentNode.compareTo(mRootNode)!=0) {
mCurrentNode = f.getParentFile();
refreshFileList();
}
} else if (f.isDirectory()) {
mCurrentNode = f;
refreshFileList();
} else {
Toast.makeText(this, "You selected: "+f.getName()+"!", Toast.LENGTH_SHORT).show();
}
}
}

Congratulations! You now have your very own external storage browser. For the sake of simplicity, the implementation is pretty barebones. The steps needed to add more features like file size, modified date, etc. should be apparent. It might also be interesting to use a grid view implementation instead of a list view, especially for browsing files on Android based tablets. Feel free to use the discussion thread to share your tweaks with other readers.

About

William J Francis began programming computers at age eleven. Specializing in embedded and mobile platforms, he has more than 20 years of professional software engineering under his belt, including a four year stint in the US Army's Military Intellige...

0 comments

Editor's Picks