/* * Copyright (C) 2011 Paul Burke * * 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.ipaulpro.afilechooser; import java.io.File; import java.io.FileFilter; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import android.app.ListActivity; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.net.Uri; import android.os.Bundle; import android.os.Environment; import android.text.TextUtils; import android.util.Log; import android.view.View; import android.widget.BaseAdapter; import android.widget.ListView; import com.ipaulpro.afilechooser.utils.FileUtils; /** * @author paulburke (ipaulpro) */ public class FileChooserActivity extends ListActivity { private static final boolean DEBUG = true; // Set to false to disable logging private static final String TAG = "ChooserActivity"; // The log tag public static final int REQUEST_CODE = 6384; // onActivityResult request code public static final String MIME_TYPE_ALL = "*/*"; // Filter for all MIME types private static final String PATH = "path"; private static final String BREADCRUMB = "breadcrumb"; private static final String POSTIION = "position"; private static final String HIDDEN_PREFIX = "."; private String mPath; // The current file path private ArrayList<String> mBreadcrumb = new ArrayList<String>(); // Path history private boolean mExternalStorageAvailable = false; private boolean mExternalStorageWriteable = false; private File mExternalDir; private ArrayList<File> mList = new ArrayList<File>(); /** * File (not directories) filter. */ private FileFilter mFileFilter = new FileFilter() { public boolean accept(File file) { final String fileName = file.getName(); // Return files only (not directories) and skip hidden files return file.isFile() && !fileName.startsWith(HIDDEN_PREFIX); } }; /** * Folder (directories) filter. */ private FileFilter mDirFilter = new FileFilter() { public boolean accept(File file) { final String fileName = file.getName(); // Return directories only and skip hidden directories return file.isDirectory() && !fileName.startsWith(HIDDEN_PREFIX); } }; /** * File and folder comparator. * TODO Expose sorting option method */ private Comparator<File> mComparator = new Comparator<File>() { public int compare(File f1, File f2) { // Sort alphabetically by lower case, which is much cleaner return f1.getName().toLowerCase().compareTo( f2.getName().toLowerCase()); } }; /** * External storage state broadcast receiver. */ private BroadcastReceiver mExternalStorageReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (DEBUG) Log.d(TAG, "External storage broadcast recieved: " + intent.getData()); updateExternalStorageState(); } }; /** * Activities extending FileChooserActivity must check against this, and implement * the associated Intent Filter in AndroidManifest.xml. * @return True if the Intent Action is android.intent.action.GET_CONTENT. */ protected boolean isIntentGetContent() { final Intent intent = getIntent(); final String action = intent.getAction(); if (DEBUG) Log.d(TAG, "Intent Action: "+action); return Intent.ACTION_GET_CONTENT.equals(action); } /** * Display the Intent Chooser. * @param title Chooser Dialog title. * @param type Explicit MIME data type filter. */ protected void showFileChooser(String title, String type) { if (TextUtils.isEmpty(title)) title = getString(R.string.select_file); if (TextUtils.isEmpty(type)) type = MIME_TYPE_ALL; // Implicitly allow the user to select a particular kind of data final Intent intent = new Intent(Intent.ACTION_GET_CONTENT); // Specify the MIME data type filter (Must be lower case) intent.setType(type.toLowerCase()); // Only return URIs that can be opened with ContentResolver intent.addCategory(Intent.CATEGORY_OPENABLE); // Display intent chooser try { startActivityForResult( Intent.createChooser(intent, title),REQUEST_CODE); } catch (android.content.ActivityNotFoundException e) { onFileError(e); } } /** * Convenience method to show the File Chooser with the default * title and have it return all file types. */ protected void showFileChooser() { showFileChooser(null, null); } /** * Fill the list with the current directory contents. */ private void fillList(int position) { if (DEBUG) Log.d(TAG, "Current path: "+this.mPath); // Set the cuttent path as the Activity title setTitle(this.mPath); // Clear the list adapter ((FileListAdapter) getListAdapter()).clear(); // Our current directory File instance final File pathDir = new File(mPath); // List file in this directory with the directory filter final File[] dirs = pathDir.listFiles(mDirFilter); if (dirs != null) { // Sort the folders alphabetically Arrays.sort(dirs, mComparator); // Add each folder to the File list for the list adapter for (File dir : dirs) mList.add(dir); } // List file in this directory with the file filter final File[] files = pathDir.listFiles(mFileFilter); if (files != null) { // Sort the files alphabetically Arrays.sort(files, mComparator); // Add each file to the File list for the list adapter for (File file : files) mList.add(file); } if (dirs == null && files == null) { if (DEBUG) Log.d(TAG, "Directory is empty"); } // Assign the File list items as our adapter items ((FileListAdapter) getListAdapter()).setListItems(mList); // Update the ListView ((FileListAdapter) getListAdapter()).notifyDataSetChanged(); // Jump to the top of the list getListView().setSelection(position); } /** * Keep track of the directory hierarchy. * @param add Add the current path to the directory stack. */ private void updateBreadcrumb(boolean add) { if (add) { // Add the current path to the stack this.mBreadcrumb.add(this.mPath); } else { if (this.mExternalDir.getAbsolutePath().equals(this.mPath)) { // If at the base directory, exit the Activity onFileSelectCancel(); finish(); } else { // Otherwise, remove the last path from the stack int size = this.mBreadcrumb.size(); if (size > 1) { this.mBreadcrumb.remove(size - 1); this.mPath = this.mBreadcrumb.get(size - 2); // Display the new directory contents fillList(0); } } } } /** * Update the external storage member variables. */ private void updateExternalStorageState() { String state = Environment.getExternalStorageState(); if (Environment.MEDIA_MOUNTED.equals(state)) { this.mExternalStorageAvailable = this.mExternalStorageWriteable = true; } else if (Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) { this.mExternalStorageAvailable = true; this.mExternalStorageWriteable = false; } else { this.mExternalStorageAvailable = this.mExternalStorageWriteable = false; } handleExternalStorageState(this.mExternalStorageAvailable, this.mExternalStorageWriteable); } /** * Register the external storage BroadcastReceiver. */ private void startWatchingExternalStorage() { IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_MEDIA_MOUNTED); filter.addAction(Intent.ACTION_MEDIA_REMOVED); registerReceiver(this.mExternalStorageReceiver, filter); if (isIntentGetContent()) updateExternalStorageState(); } /** * Unregister the external storage BroadcastReceiver. */ private void stopWatchingExternalStorage() { unregisterReceiver(this.mExternalStorageReceiver); } /** * Respond to a change in the external storage state * @param available * @param writeable */ private void handleExternalStorageState(boolean available, boolean writeable) { if (!available && isIntentGetContent()) { if (DEBUG) Log.d(TAG, "External Storage was disconnected"); onFileDisconnect(); finish(); } } /** * Called when a file is successfully selected by the user. * @param file The file selected. */ protected void onFileSelect(File file){ if (DEBUG) Log.d(TAG, "File selected: "+file.getAbsolutePath()); } /** * Called when there is an error selecting a file. * @param e The error encountered during file selection. */ protected void onFileError(Exception e){ if (DEBUG) Log.e(TAG, "Error selecting file", e); } /** * Called when the user backs out of the file selection process. */ protected void onFileSelectCancel(){ if (DEBUG) Log.d(TAG, "File selection canceled"); } /** * Called when the external storage (SD) is disconnected. */ protected void onFileDisconnect(){ if (DEBUG) Log.d(TAG, "External storage disconnected"); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Get the external storage directory. this.mExternalDir = Environment.getExternalStorageDirectory(); if (getListAdapter() == null) { // Assign the list adapter to the ListView setListAdapter(new FileListAdapter(this)); } if (savedInstanceState != null) { restoreMe(savedInstanceState); } else { // Set the external storage directory as the current path this.mPath = this.mExternalDir.getAbsolutePath(); // Add the current path to the breadcrumb updateBreadcrumb(true); if (isIntentGetContent()) { setContentView(R.layout.explorer); fillList(0); } } } @Override protected void onResume() { super.onResume(); // Set the Broadcast Receiver to listen for storage mount changes startWatchingExternalStorage(); } @Override protected void onPause() { super.onPause(); // Remove the Broadcast Receiver listening for storage mount changes stopWatchingExternalStorage(); } @Override public void onBackPressed() { updateBreadcrumb(false); } @Override protected void onListItemClick(ListView l, View v, int position, long id) { super.onListItemClick(l, v, position, id); // Get the file that was selected from the file list File file = this.mList.get(position); // Save the path as our current member variable this.mPath = file.getAbsolutePath(); if (DEBUG) Log.d(TAG, "Selected file: "+this.mPath); if (file != null) { if (file.isDirectory()) { // If the selected item is a folder, update UI updateBreadcrumb(true); fillList(0); } else { // Otherwise, return the URI of the selected file final Intent data = new Intent(); data.setData(Uri.fromFile(file)); setResult(RESULT_OK, data); finish(); } } } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { switch (requestCode) { case REQUEST_CODE: if (resultCode == RESULT_OK) { // If the file selection was successful try { // Get the URI of the selected file final Uri uri = data.getData(); // Create a file instance from the URI final File file = new File( FileUtils.getPath(this, uri)); // Expose the file onFileSelect(file); } catch (Exception e) { onFileError(e); } } else if (resultCode == RESULT_CANCELED) { onFileSelectCancel(); } break; } super.onActivityResult(requestCode, resultCode, data); } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); // Save the current path and breadcrumb when the activity is interrupted. outState.putString(PATH, mPath); outState.putStringArrayList(BREADCRUMB, mBreadcrumb); outState.putInt(POSTIION, getListView().getFirstVisiblePosition()); } /** * If the activity was interrupted, restore the previous path and breadcrumb * @param state */ private void restoreMe(Bundle state) { // Restore the previous path. Defaults to base external storage dir this.mPath = (state.containsKey(PATH)) ? state.getString(PATH) : mExternalDir.getAbsolutePath(); // Restore the previous breadcrumb this.mBreadcrumb = state.getStringArrayList(BREADCRUMB); fillList(state.getInt(POSTIION)); } }