Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Mobile / Android / Android4x

An Android File Save Dialogue

4.67/5 (21 votes)
14 Jan 2014CPOL9 min read 53.9K   1.3K  
A general purpose file save popup for Android.

Introduction

Android was originally designed to run on devices with limited resources and little or no need for file operations so features such as standard file save and open dialogues were not included in the API. However there are some applications where it would be convenient to save files and to allow the user to specify where on the device the file(s) should be saved and, having saved files, it wouldn't be a bad idea to provide the user with a way of getting at them.

Both popups are implemented as single classes each of which defines its UI in code rather than using an XML layout file. All that is required is to drop the required class(es) into the appropriate src sub-directory of your project(s), update the package path in the file and then call it from the activity, or activities, that require a file save or select feature.

Background

The standard approach to creating custom popups in Android is to use an AlertDialog builder which provides a two button positive/negative framework (e.g. OK/Cancel) and a three button positive/negative/neutral framework (e.g. OK/Cancel/Apply). The basics are explained here:

Android Dialogs - Background
AlertDialog

Screenshots

These screenshots are from the emulator.

File Save

Image 1

Screen shot for an empty directory showing:

  • Back arrow to parent directory
  • Path to current directory.
  • Edit area to enter a filename.
  • The default extension for the file to be created.
Image 2

Screen shot for a directory showing sub-directories

File Select

Image 3

Screen shot for filtered select showing:

  • Directories and XML files.
  • The path for the selected file.

Using the code

Activity Signature

Any activity that needs the file save/select features will have to implement the file fragments' callback interfaces. For example:
Java
// Activity requiring save and select.
public class MainActivity 
	extends Activity 
    implements FileSaveFragment.Callbacks,
               FileSelectFragment.Callbacks {

FileSaveFragment Callbacks

MethodReturnsParametersComment
onCanSave booleanString absolutePath
String filename
Invoked immediately before onConfirmSave when the user hits the [positive] button. Use this to validate user input. If false is returned onConfirmSave is not called.
onConfirmSavevoidString absolutePath
String filename
If the user clicks the [negative] button then both path and name are null.

FileSelectFragment Callbacks

MethodReturnsParametersComment
isValid booleanString absolutePath
String filename
Invoked immediately before onConfirmSelect when the user hits the [positive] button. This callback can be used to check that the selected file is suitable whilst the popup is still in view. If false is returned onConfirmSelect is not called.
onConfirmSelectvoidString absolutePath
String filename
If the user clicks the [negative] button then both path and name are null. In directory selection mode name will always be null.

File Save

Instantiation

The instantiation method takes a default extension string and a number of resource IDs. You will, obviously, have to provide appropriate resources.

ParamRemark
defaultExtensionA string. Can be null.
resourceID_OKA string resource ID for the OK (positive) button caption.
resourceID_CancelA string resource ID for the Cancel (negative) button caption.
resourceID_TitleA string resource ID for the popup's title bar.
resourceID_EditHintA string resource ID for the filename edit widget.
resourceID_IconA drawable resource ID for the popup's title bar icon.
Java
String xml = getResources().getString(R.string.file_extension_xml);
String fragTag = getResources().getString(R.string.tag_fragment_FileSave);

// Get an instance supplying a default extension, captions and
// icon appropriate to the calling application/activity.
FileSaveFragment fsf = FileSaveFragment.newInstance(xml,
                                                    R.string.alert_ok,
                                                    R.string.alert_cancel,
                                                    R.string.alert_file_save_as,
                                                    R.string.hint_filename_unadorned,
                                                    R.drawable.ic_launcher);
fsf.show(getFragmentManager(), fragTag);

Validation Helper Methods

Tricky this. Do you provide validation in the popup or just a callback hook? I've gone for the callback because it lets the client programmer validate as much or as little as required and doesn't dictate how feedback is displayed to the user. By way of a compromise a few static helper methods have been added to FileSaveFragment to reduce the workload required when handling the onCanSave() callback. See the example below.

MethodParamsReturnsRemark
FileExistsString absolutePath
String filename 
BooleanSimple, "Is it there or not?" test.
IsAlphaNumericString filename BooleanTrue if the filename contains only alpha-numeric characters. Simplifies checking for invalid characters at the cost of forbidding hyphens, spaces and underscores.
ExtensionString filename StringThe characters following the final full stop in the file's name.
NameNoExtensionString filename StringThe filename up to, but not including, the final full stop in the name.

Callbacks

Java
// Act on a validated [positive] button click or a [negative] button
// click. On [negative] click path and name are both null.
public void onConfirmSave(String absolutePath, String fileName) {
  if (absolutePath != null && fileName != null) {
    // Recommend that file save for large amounts of data is handled
    // by an AsyncTask.
    mySaveMethod(absolutePath, fileName);
  }
}
Java
// Example validation showing use of provided helper methods.
public boolean onCanSave(String absolutePath, String fileName) {

boolean canSave = true;

// Catch the really stupid case.
if (absolutePath == null || absolutePath.length() ==0 ||
    fileName == null || fileName.length() == 0) {
  canSave = false;
  showToast(R.string.alert_supply_filename, Toast.LENGTH_SHORT);
}

// Do we have a filename if the extension is thrown away?
if (canSave) {
  String copyName = FileSaveFragment.NameNoExtension(fileName);
  if (copyName == null || copyName.length() == 0 ) {
    canSave = false;
    showToast(R.string.alert_supply_filename, Toast.LENGTH_SHORT);
  }
}

// Allow only alpha-numeric names. Simplify dealing with reserved path
// characters.
if (canSave) {
  if (!FileSaveFragment.IsAlphaNumeric(fileName)) {
    canSave = false;
    showToast(R.string.alert_bad_filename_chars, Toast.LENGTH_SHORT);
  }
}

// No overwrite of an existing file.
if (canSave) {
  if (FileSaveFragment.FileExists(absolutePath, fileName)) {
    canSave = false;
    showToast(R.string.alert_file_exists, Toast.LENGTH_SHORT);
  }
}

return canSave;
}

File or Directory Select

Instantiation

The instantiation method takes a selection mode enum and a number of resource IDs. You will, obviously, have to provide appropriate resources. A filename filter may also be specified.

ParamRemark
selectionModeMode.FileSelector or Mode.DirectorySelector. In directory selection mode only directories are displayed. In both modes any FilenameFilter supplied will be honoured.
resourceID_OKA string resource ID for the OK (positive) button caption.
resourceID_CancelA string resource ID for the Cancel (negative) button caption.
resourceID_Title A string resource ID for the popup's title bar.
resourceID_IconA drawable resource ID for the popup's title bar icon.
resourceID_DirectoryA drawable resource ID to indicate a directory.
resourceID_FileA drawable resource ID to indicate a file.

MethodParamsRemark
newInstanceSee above. 
setFilterFilenameFilterOptional. An instance of a filename filter that filters files/directories to be displayed according to application requirements. Static helper method ::FileTypeFilter provides simple file extension filtering.

Java
String fragTag = getResources().getString(R.string.tag_fragment_FileSelect);

// Set up a selector for file selection rather than directory selection.
FileSelectFragment fsf = FileSelectFragment.newInstance(Mode.FileSelector,
                                                        R.string.alert_ok,
                                                        R.string.alert_cancel,
                                                        R.string.alert_file_select,
                                                        R.drawable.ic_launcher,
                                                        R.drawable.ic_dir,
                                                        R.drawable.ic_file);

// Restrict selection to *.xml files
ArrayList<String> allowedExtensions = new ArrayList<String>();
allowedExtensions.add(".xml");
fsf.setFilter(FileSelectFragment.FiletypeFilter(allowedExtensions));

fsf.show(getFragmentManager(), fragTag);

Callbacks

Java
public void onConfirmSelect(String absolutePath, String fileName) {
  if (absolutePath != null && fileName != null) {
    // Recommend that long/intensive file processes be handled by an
    // Async task.
    myFileProcess(absolutePath, fileName);
  }
}

// Check that the file's content is acceptable.
public boolean isValid(String absolutePath, String fileName) {
  return fileHeaderCheck(absolutePath, fileName);
}

The isValid callback is provided so that where a user is selecting from files with a common type but different internal layout (think XML) the application can run a quick sanity check before allowing the user to carry on.

An alternative would be to supply a custom FilenameFilter that only presents users with files of the correct internal format regardless of filename or extension; either by MIME type or by inspection of content. Care has to be taken with this approach to avoid blocking the UI thread.

Anatomy

There are only minor differences between the file save and the file select so we'll only discuss file save here.

The contents of the current directory are shown in a listview which allows us to scroll through the list. We will need the following for the user interface:

  • An overall popup layout
  • An array adapter that will display the content of each directory.
  • A way to update the content of the listview when a user picks an item in a directory.

Overall Layout

The standard (and preferred) way of creating a user interface in Android is to use an XML layout file, but because we want to keep the number of files required to an absolute minimum (i.e. one per popup) the UI is defined in code in the popup's onCreateDialog method. In outline we have:

  • Create a vertical layout root view
  • Add a listview and divider to the root view
  • Add a horizontal layout for the file entry area to the root view
  • Add the path, edit and default extension widgets to the horizontal layout view.
  • Initial population of the listview and other widgets.
  • Use AlertDialog.Builder to create the popup using the root view.
Java
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {

    // Set up the container view.
    LinearLayout.LayoutParams rootLayout = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                                                                         ViewGroup.LayoutParams.WRAP_CONTENT, 
                                                                         0.0F);
    root = new LinearLayout(getActivity());
    root.setOrientation(LinearLayout.VERTICAL);
    root.setLayoutParams(rootLayout);

    // Set up initial sub-directory list.
    currentDirectory = Environment.getExternalStorageDirectory();
    directoryList = getSubDirectories(currentDirectory);
    DirectoryDisplay displayFormat = new DirectoryDisplay(getActivity(), directoryList);

    /* Now set up the directory listview.
     * Fix the height of the listview at 150px, enough to show 3 or 4 entries at a time.
     * Don't want the popup shrinking and growing all the time. Tried it. 
     * Most disconcerting.
     * */
    LinearLayout.LayoutParams listViewLayout = new LinearLayout.LayoutParams(ViewGroup.
                                                                             LayoutParams.MATCH_PARENT,
                                                                             150,
                                                                             0.0F);
    directoryView = new ListView(getActivity());
    directoryView.setLayoutParams(listViewLayout);
    directoryView.setAdapter(displayFormat);
    directoryView.setOnItemClickListener(this);
    root.addView(directoryView);
    
    View horizDivider = new View(getActivity()); 
    horizDivider.setBackgroundColor(Color.CYAN);
    root.addView(horizDivider,
             new ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, 2));    

    /*
     * Now set up the filename entry area.
     * 
     * {current path}/ [Enter Filename         ] {default extension}
     * 
     * */
    LinearLayout nameArea = new LinearLayout(getActivity());
    nameArea.setOrientation(LinearLayout.HORIZONTAL);
    nameArea.setLayoutParams(rootLayout);
    root.addView(nameArea);
    
    currentPath = new TextView(getActivity());
    currentPath.setText(currentDirectory.getAbsolutePath() + "/");
    nameArea.addView(currentPath);
    
    /*
     * We want the filename input area to be as large as possible, but still leave
     * enough room to show the path and any default extension that may be supplied
     * so we give it a weight of 1.
     * */
    LinearLayout.LayoutParams fileNameLayout = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
                                                                             ViewGroup.LayoutParams.WRAP_CONTENT,
                                                                             1.0F );
    fileName = new EditText(getActivity());
    fileName.setHint(resourceID_EditHint);
    fileName.setGravity(Gravity.LEFT);
    fileName.setInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
    fileName.setLayoutParams(fileNameLayout);
    nameArea.addView(fileName);
    
    // Display the default extension if one has been supplied. 
    if (defaultExtension != null ) {
    	TextView defaultExt = new TextView(getActivity());
    	defaultExt.setText(defaultExtension);
    	defaultExt.setGravity(Gravity.LEFT);
    	defaultExt.setPadding(2, 0, 6, 0);
    	nameArea.addView(defaultExt);
    }
    
    // Use the standard AlertDialog builder to create the popup. 
    //     Usuall Android custom and practice is to chain calls from the builder, but
    //     this can become an unreadable and unmaintainable mess very quickly so I don't.
    Builder popupBuilder = new AlertDialog.Builder(getActivity());
    popupBuilder.setView(root);
    popupBuilder.setIcon(resourceID_Icon);
    popupBuilder.setTitle(resourceID_Title);

    popupBuilder.setPositiveButton(resourceID_OK,
                                   new DialogInterface.OnClickListener() {
        public void onClick(DialogInterface dialog, int whichButton) {
          // Empty method. Method defined in onStart();
          // See later notes in this article.
        }
    });

    popupBuilder.setNegativeButton(resourceID_Cancel,
                                   new DialogInterface.OnClickListener() {
            public void onClick(DialogInterface dialog, int whichButton) {
            	mCallbacks.onConfirmSave(null, null);
            }
        });

    // Hand out an instance of our popup.    
    return popupBuilder.create();
}

Display Adapter

This is a standard adapter which displays a single textview containing the directory's name for each File instance in the source list.

Java
private class DirectoryDisplay
    extends ArrayAdapter<File> { ...

Selecting A Directory

We need to pick up on the item selection in the listview. We do this by having the popup class implement OnItemClickListener which will allow it to handle the user press/click in the listview.

Java
public class FileSaveFragment extends DialogFragment 
		implements OnItemClickListener {

The item click handler link is established in the onCreateDialog method.

Java
directoryView.setOnItemClickListener(this);

The popup's onItemClick callback handler retrieves the sub-directories for the selected directory, associates the list with a new display adapter and assigns that to the list view.

Java
@Override
public void onItemClick(AdapterView<?> arg0, View list, int pos, long id )
{
  
  File selected = null;
  
  if (pos >= 0 || pos < directoryList.size()) {
    selected = directoryList.get(pos);
    String name = selected.getName();

    // Are we going up or down?
    if (name.equals(PARENT)) {
      currentDirectory = currentDirectory.getParentFile();
    }
    else {
      currentDirectory =   selected;
    }

    // Refresh the listview display for the newly selected directory.
    directoryList = getSubDirectories(currentDirectory);
    DirectoryDisplay displayFormatter = new DirectoryDisplay(getActivity(), directoryList);
    directoryView.setAdapter(displayFormatter);
    
    // Update the path TextView widget.  Tell the user where he or she is.
    String path = currentDirectory.getAbsolutePath();
    if (currentDirectory.getParent() != null) {
      path += "/";
    }
    currentPath.setText(path);  
  }
}

Retaining View During Validation

The setup for the onClickListener for the positive button is moved to the AlertDialog::onStart override. This allows us to call the AlertDialog::dismiss method or not according to the return value of a validation callback.

In onCreate the button is created with a NOP listener because it isn't possible to suppress the automatic call of dismiss from there.

Java
popupBuilder.setPositiveButton(resourceID_OK,
                               new DialogInterface.OnClickListener() {
    public void onClick(DialogInterface dialog, int whichButton) {
      // Empty method. Method defined in onStart();
    }
});

The onStart override replaces the NOP listener with a listener which can suppress the dismiss call.

Java
@Override
public void onStart() {
  super.onStart();    
  AlertDialog d = (AlertDialog)getDialog();
  if(d != null)
  {
    Button positiveButton = (Button) d.getButton(Dialog.BUTTON_POSITIVE);
    positiveButton.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
      String absolutePath = currentDirectory.getAbsolutePath();
      String filename = fileName.getText().toString();
        if (mCallbacks.onCanSave(absolutePath, filename)) {
          dismiss();
          mCallbacks.onConfirmSave(absolutePath, filename);
        }
      }
    });
  }
}

File Select Differences

The main difference is that the directory open and content population is moved to an onItemLongClick handler and the onItemClick handler selects a file within a directory. This makes it easy to distinguish between file selection and a request to change directory.

The file save popup doesn't bother with a file directory icon as there is no need to distinguish between a directory and a file. If you want to display a directory icon in the file save you will need to modify the DirectoryDisplay::getView method as outlined below. The TextView widget makes it very easy to combine and image with text.

Java
String name = fileList.get(position).getName();
textview.setText(name);

int iconID = resourceID_File;

if (fileList.get(position).isDirectory()) {
  iconID = resourceID_Dir;
}

// We don't show an icon for the parent.
if (name.equals(PARENT)) {
  iconID = -1;
}

// Icon to the left of the text.
if (iconID > 0 ){
  Drawable icon = getActivity().getResources().getDrawable( iconID );
  textview.setCompoundDrawablesWithIntrinsicBounds(icon,null, null, null );
}

Notes

The user has to click the [positive] button to confirm the selection for both save and select although it ought to be possible to select and dismiss automatically in an itemclick handler. This prevents incorrect selection problems where users have fat fingers and/or devices with less sensitive screens.

A parent directory is marked by a left point triangle, \u25C0. This was done to minimise the amount of set up required when using the popup. Recent reading suggests that this codepoint may not be supported in all Android implementations. An alternative would be to provide a drawable resource in the host application and hand the resource ID in at popup creation as is done for the popup's title icon.

I've used the 4_collections_view_as_grid and 4_collections_view_as_list icons from the holo light download as the icons for directory and file respectively in the file select. I think they work quite well and they are available in all required sizes without extra work. Lazy? Me?

The listview height is set at 150px because MATCH_PARENT gives a popup that varies in height from directory to directory. This value seems to give good results for a 7" tablet and 3" 'phone formats in emulation, but I'd be interested to hear of any better way of fixing the height.

Although both classes have a lot of common code I felt it would simplify re-use if they were kept completely separate. If you disagree please feel free to turn them into a single class.

Although intended for use with Android V4 onwards it should be possible to back port these classes to any version that has the fragment support library. This is left as an exercise for the interested reader.

History

DateVersionComment
Jan. 20143Rewrite on addition of File Select.
Jan. 20142Popup not dismissed if validation fails in File Save.
File Save Validation helper methods added.
Spellcheck disabled in File Save for filename edit widget.
Dec. 20131Version 1 for publication

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)