Since 3.1

External Storage Devices

The purpose of the components of this package is to ease accessing external storage units, mainly transient USB storage devices. Transient SdCard readers are also considered.

Android 4.4 (API level 19) introduces the Storage Access Framework (SAF), a unified way to access individual documents through standardized storage providers.
Android 5.0 (API level 21) extends SAF with the capability to have full access to an entire subtree of documents by letting the user pick a directory instead of a single file.
Android 6.0 (API level 23) enforces the usage of SAF and prohibits direct access to external portable storage devices, for privacy and security reasons.

The library tries to accommodate these differences and selects the best mean to manage storage devices according to the version of the Operating System.

Code samples

The following code snippets come from a fragment dedicated to the management of settings related to Volumes. The full source code and the layout files are available into the sample app at the Download section.

Setup

In this demo, Volumes are managed with a ListView and a ListAdapter. If applicable, a "Add Volume" button handler allows to request the picker UI.
public class StorageSettingsFragment extends ListFragment implements OnClickListener {
  private static final int STORAGE_ACCESS_REQUEST_CODE = 99;  // any value of your choice

  private ItemAdapter mAdapter;
  private Intent mRequestStorageAccessIntent;
  private VolumeManager mVolumeManager;

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    mVolumeManager = new VolumeManager();
    // A null means that Storage Access Framework is not activated.
    mRequestStorageAccessIntent = VolumeManager.getStorageAccessIntent();
  }

  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    final View v = inflater.inflate(R.layout.fragment_storage, container, false);
    final Button btn = (Button) v.findViewById(R.id.btnAddVolume);
    if (mRequestStorageAccessIntent != null) {
      btn.setOnClickListener(this);
    } else {
      // The button is not applicable in this running context, hide it.
      btn.setVisibility(View.GONE);
    }
    final List volumes = mVolumeManager.getVolumes();
    mAdapter = new ItemAdapter(getActivity(), R.layout.list_volume, volumes);
    setListAdapter(mAdapter);
    return v;
  }

  @Override
  public void onClick(View v) {
    switch (v.getId()) {
    case R.id.btnAddVolume:
      requestStorageAccess();
      break;
    }
  }
  // ...
}

The adapter

Each item view shows a name and between one and three buttons:
  1. An "Edit Label" button, to give a custom name to the Volume.
  2. A "Remove Label" button, if a label has been given.
  3. A "Forget" button, if applicable, to revoke the authorization once granted by the "Add Volume" button.
private class ItemAdapter extends ArrayAdapter implements OnClickListener {
  // ItemAdapter( ...
  // public View getView( ...
  // private Volume getTargetVolume(View v) ...

  @Override
  public void onClick(View v) {
    final Volume volume = getTargetVolume(v);
    if (volume != null) {
      switch (v.getId()) {
      case R.id.btnEditLabel:
        final DialogFragment f = EditLabelDialogFragment.newInstance(mAdapter.getPosition(volume), volume.getLabel());
        f.setTargetFragment(StorageSettingsFragment.this, 0);
        f.show(getFragmentManager(), "EditLabelDialogFragment");
        break;
      case R.id.btnRemoveLabel:
        if (volume.deleteLabelFile()) refreshVolumes();
        break;
      case R.id.btnForget:
        if (volume.forget()) renewVolumes();
        break;
      }
    }
  }
}

The communication with Storage Access Framework

private void requestStorageAccess() {
  startActivityForResult(mRequestStorageAccessIntent, STORAGE_ACCESS_REQUEST_CODE);
}

@Override
public void onActivityResult(int requestCode, int resultCode, Intent resultData) {
  if (requestCode == STORAGE_ACCESS_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
    // Forward the result, to persist the selection.
    VolumeManager.onStorageAccessResult(resultData);
    // The list needs to be fully refreshed.
    renewVolumes();
  }
}

Refreshing the data and the display

// Called on the exit from the dialog, if the label has changed.
private void onDialogLabelChange(int position, String label) {
  final Volume volume = mAdapter.getItem(position);
  if (volume != null) {
    volume.writeLabelFile(label);
    refreshVolumes();
  }
}

// There has been an addition or a subtraction of a Volume, request an updated list.
private void renewVolumes() {
  mAdapter.clear();
  mAdapter.addAll(mVolumeManager.getVolumes());
  refreshVolumes();
}

// There has been a change in the properties of at least one of the items, request an updated display.
private void refreshVolumes() {
  mAdapter.notifyDataSetChanged();
}

Support of the Genymotion emulator

The accessibility to a USB device on the host varies with the virtual device. The criterias are unknown. A bug report has been filed at Genymobile.

Hereunder the results of some experiments - made in this environment: OS Windows 10, VirtualBox version 5.1.6, Genymotion version 2.8.0.

USB Accessible:

  • Samsung Galaxy Note 2 - 4.1.1 - API 16
  • Samsung Galaxy Note 2 - 4.2.2 - API 17

USB Not Accessible:

  • Samsung Galaxy Note 2 - 4.3 - API 18
  • Samsung Galaxy Note 3 - 4.3 - API 18
  • Samsung Galaxy S6 - 5.0.0 - API 21
  • Google Nexus 7 - 5.1.0 - API 22

Here is a sequence of actions that works:

Configuration

- In VirtualBox Manager, select the virtual device, being in Powered Off state.
- Go to its Settings and choose the USB tab.
- Check Enable USB Controller.
- Choose USB 1.1 (OHCI) Controller (trials with 2.0 or 3.0 didn't success, for unknown reason).
- Add a new USB filter, the simplest is to have all fields set to empty strings.

Running

- Don't have yet the USB device plugged.
- In Genymotion, start the virtual device and wait for the end of the boot.
- Plug the USB device. Because the USB device is captured for the guest OS, it should not be visible under the host OS, otherwise something went wrong.
- For a clean eject of the USB device, wait for the close of the emulator. At that moment, the USB device will be given back to the host OS.

A Volume instance is built for the storage unit, with getName() returning "USB".

Only one USB device is visible at the same time. If you plug a second one, it has no effect on the first.

Alternative

In particular for the virtual devices that do not give native access to the USB device, there is a workaround applicable during the development phase.

- In VirtualBox Manager, select the virtual device, being in Powered Off state.
- Go to its Settings
- In the USB tab, uncheck any USB filters that would catch the USB devices.
- In the Shared folders tab, add a folder for each USB device. For example:

  Name     Path  Auto-mount  Access
  E_DRIVE  E:\   Yes         Read-only
- In your code, add: mVolumeManager.addEmulatedStorageContainer("/mnt/shared");

For the example above, a Volume instance is built for the storage unit, with getName() returning "E_DRIVE".


Deprecated since 3.1

USB Devices

Examples

USB devices exploring

Here is an example to explore the USB devices connected to a Google TV box:

    StringBuilder sb = new StringBuilder();
    VolumeManager volumeManager = new VolumeManager();
    volumeManager.addMediaStorageDirectory("/mnt/sdcard/Movies");
    List<Volume> volumes = volumeManager.getVolumes();
    for (Volume volume: volumes) {
        sb.append("volume: label=").append(volume.getLabel())
                .append(", type=").append(volume.getType())
                .append(NEW_LINE);
        File rootFolder = volume.getRoot();
        File[] files = rootFolder.listFiles();
        for (File file: files) {
            sb.append(TAB).append("file: ").append(file.getName()).append(NEW_LINE);
        }
    }
    TextView tv = (TextView) findViewById(R.id.content);
    tv.setText(sb.toString());