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.
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;
}
}
// ...
}
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;
}
}
}
}
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();
}
}
// 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();
}
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:
USB Not Accessible:
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.
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".
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());