Android offers a few structured ways to store data, notably
SharedPreferences
and local SQLite databases. And,
of course, you are welcome to store your data “in the cloud” by using an
Internet-based service. We will get to all of those topics
shortly.
Beyond that, though, Android allows you to work with plain old ordinary files, either ones baked into your app (“assets”) or ones on so-called internal or external storage.
To make those files work — and to consume data off of the Internet — you will likely need to employ a parser. Android ships with several choices for XML and JSON parsing, in addition to third-party libraries you can attempt to use.
This chapter focuses on assets, files, and parsers.
Let’s suppose you have some static data you want to ship with the application,
such as a list of words for a spell-checker. Somehow, you need to bundle that
data with the application, in a way you can get at it from Java code later on,
or possibly in a way you can pass to another component (e.g., WebView
for
bundled HTML files).
There are three main options here: raw resources, XML resources, and assets.
One way to deploy a file like a spell-check catalog is
to put the file in the res/raw
directory, so it gets put in the Android
application .apk
file as part of the packaging process as a raw resource.
To access this file, you need to get yourself a Resources
object. From an
activity, that is as simple as calling getResources()
. A Resources
object
offers openRawResource()
to get an InputStream
on the file you specify.
Rather than a path, openRawResource()
expects an integer identifier for the
file as packaged. This works just like accessing widgets via findViewById()
– if you put a file named words.xml
in res/raw
, the identifier is
accessible in Java as R.raw.words
.
Since you can only get an InputStream
, you have no means of modifying this
file. Hence, it is really only useful for static reference data. Moreover,
since it is unchanging until the user installs an updated version of your
application package, either the reference data has to be valid for the
foreseeable future, or you will need to provide some means of updating the
data. The simplest way to handle that is to use the reference data to bootstrap
some other modifiable form of storage (e.g., a database), but this makes for
two copies of the data in storage. An alternative is to keep the reference data
as-is but keep modifications in a file or database, and merge them together
when you need a complete picture of the information. For example, if your
application ships a file of URLs, you could have a second file that tracks URLs
added by the user or reference URLs that were deleted by the user.
If, however, your file is in an XML format, you are better served not putting
it in res/raw/
, but rather in res/xml/
. This is a directory for XML resources
– resources known to be in XML format, but without any assumptions about what
that XML represents.
To access that XML, you once again get a Resources
object by calling
getResources()
on your Activity
or other Context
. Then, call getXml()
on
the Resources
object, supplying the ID value of your XML resource (e.g.,
R.xml.words
). This will return an XmlResourceParser
, which implements the
XmlPullParser
interface. We will discuss how to use this parser, and the
performance advantage of using XML resources,
later in this chapter.
As with raw resources, XML resources are read-only at runtime.
Your third option is to package the data in the form of an asset. You can create
an assets/
directory in your sourceset (e.g., src/main/assets
), then place whatever
files you want in there. Those are accessible at runtime by calling getAssets()
on your Activity
or other Context
, then calling open()
with the path
to the file (e.g., assets/foo/index.html
would be retrieved via
open("foo/index.html")
). As with raw resources, this returns an InputStream
on the file’s contents. And, as with all types of resources, assets are read-only
at runtime.
One benefit of using assets over raw resources is the file:///android_asset/
Uri
prefix. You can use this to load an asset into a WebView
. For example,
for an asset located in assets/foo/index.html
within your project,
calling loadUrl("file:///android_asset/foo/index.html")
will load that
HTML into the WebView
.
Note that assets are compressed when the APK is packaged. Unfortunately,
on Android 1.x/2.x,
this compression mechanism has a 1MB file size limit. If you wish to package
an asset that is bigger than 1MB, you either need to give it a file extension
that will not be compressed (e.g., .mp3
) or actually store a ZIP file of
the asset (to avoid the automatic compression) and decompress it yourself
at runtime, using the standard java.util.zip
classes. This restriction
was lifted with Android 3.0, and so if your minSdkVersion
is 11 or higher,
this will not be an issue for you.
On the whole, Android just uses normal Java file I/O for local files. You
will use the same File
and InputStream
and OutputWriter
and other
classes that you have used time and again in your prior Java development work.
What is distinctive in Android is where you read and write. Akin to writing a Java Web app, you do not have read and write access to arbitrary locations. Instead, there are only a handful of directories to which you have any access, particularly when running on production hardware.
Internal storage refers to your application’s portion of the on-board, always-available flash storage. External storage refers to storage space that can be mounted by the user as a drive in Windows (or, possibly with some difficulty, as a volume in OS X or Linux).
Historically (i.e., Android 1.x/2.x), internal storage was very limited in space. That is far less of a problem on 3.0 and higher.
Similarly, external storage is not always available on Android 1.x and 2.x – if it is mounted as a drive or volume on a host desktop or notebook, your app will not have access to external storage. We will examine this limitation in a bit more detail later in this chapter. This is not usually a problem on Android 3.0+.
On both internal and external storage, you have the option of saving files as a cache, or on a more permanent basis. Files located in a cache directory may be deleted by the OS or third-party apps to free up storage space for the user. Files located outside of cache will remain unless manually deleted.
Internal storage is on a per-application basis. Files you write to in your own internal storage cannot be read or written to by other applications… normally. Users who “root” their phones can run apps with superuser privileges and be able to access your internal storage. Most users do not root their phones, and so only your app will be able to access your internal storage files.
Files on external storage, though, are visible to all applications and the user. Anyone can read anything stored there, and any application that requests to can write or delete anything it wants.
You have a few options for manipulating the contents of your app’s portion of internal storage.
One possibility is to use
openFileInput()
and openFileOutput()
on your Activity
or other Context
to get an InputStream
and OutputStream
, respectively. However, these methods
do not accept file paths (e.g., path/to/file.txt
), just simple filenames.
If you want to have a bit more flexibility, getFilesDir()
and getCacheDir()
return a File
object pointing to the roots of your files and cache locations
on internal storage, respectively. Given the File
, you can create files
and subdirectories as you see fit.
To see how this works, take a peek at
the Files/FilesEditor
sample project.
This application implements a tabbed editor, using a ViewPager
and
a third-party tab library. Each tab
is an EditorFragment
, implementing a large EditText
widget, akin to
what we saw as examples back in the chapter on ViewPager
.
However, those ViewPager
samples had no persistence. Whatever you typed
stayed in the fragments but was lost when the process was terminated.
FileEditor
instead will save what you enter into files, one file
per tab.
The layout for the activity is reminiscent of the ViewPager
samples,
except that we are using an io.karim.MaterialTabs
widget for the tabs,
instead of something like a PagerTabStrip
:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<io.karim.MaterialTabs
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="48dp"
app:mtIndicatorColor="@color/accent"
app:mtSameWeightTabs="true"/>
<android.support.v4.view.ViewPager
android:id="@+id/pager"
android:layout_width="match_parent"
android:layout_height="match_parent">
</android.support.v4.view.ViewPager>
</LinearLayout>
That library, io.karim:materialtabs
, is one of our dependencies, along
with the support-v13
library for ViewPager
itself:
apply plugin: 'com.android.application'
dependencies {
compile 'io.karim:materialtabs:2.0.2'
compile 'com.android.support:support-v13:25.1.0'
}
android {
compileSdkVersion 25
buildToolsVersion "25.0.2"
defaultConfig {
minSdkVersion 15
targetSdkVersion 25
}
}
Other than some slight tweaks for using a MaterialTabs
for the tabs,
the MainActivity
is not significantly different than the original
ViewPager
examples. It loads up the layout and populates the ViewPager
and tabs:
@Override
protected void onReady(Bundle savedInstanceState) {
setContentView(R.layout.main);
ViewPager pager=(ViewPager)findViewById(R.id.pager);
pager.setAdapter(new SampleAdapter(this, getFragmentManager()));
MaterialTabs tabs=(MaterialTabs)findViewById(R.id.tabs);
tabs.setViewPager(pager);
}
Where things start to depart more significantly from the original samples
comes in SampleAdapter
. Rather than 10 pages, we limit the number
of tabs to 2 or 3 in getCount()
. Whether we support 2 or 3 pages
depends on what version of Android we are running on — we will explore this
issue more later in this chapter.
Rather than delegate the page titles to
the EditorFragment
, getPageTitle()
looks up a string resource value
from an array, based on the position, and uses that for the title.
And getItem()
… becomes more complicated:
package com.commonsware.android.fileseditor;
import android.app.Fragment;
import android.app.FragmentManager;
import android.content.Context;
import android.os.Build;
import android.os.Environment;
import android.support.v13.app.FragmentPagerAdapter;
import java.io.File;
public class SampleAdapter extends FragmentPagerAdapter {
private static final int[] TITLES={R.string.internal,
R.string.external, R.string.pub};
private static final int TAB_INTERNAL=0;
private static final int TAB_EXTERNAL=1;
private static final String FILENAME="test.txt";
private final Context ctxt;
public SampleAdapter(Context ctxt, FragmentManager mgr) {
super(mgr);
this.ctxt=ctxt;
}
@Override
public int getCount() {
return(3);
}
@Override
public Fragment getItem(int position) {
File fileToEdit;
switch(position) {
case TAB_INTERNAL:
fileToEdit=new File(ctxt.getFilesDir(), FILENAME);
break;
case TAB_EXTERNAL:
fileToEdit=new File(ctxt.getExternalFilesDir(null), FILENAME);
break;
default:
fileToEdit=
new File(Environment.
getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS),
FILENAME);
break;
}
return(EditorFragment.newInstance(fileToEdit));
}
@Override
public String getPageTitle(int position) {
return(ctxt.getString(TITLES[position]));
}
}
Based on the supplied position
, we create a File
object representing
where the data resides for our EditorFragment
. Right now, let’s focus
on the TAB_INTERNAL
case, where we use getFilesDir()
to create a
File
object pointing to a test.txt
file on our internal storage.
The newInstance()
factory method on EditorFragment
now takes the File
object
as input, instead of the position
. A File
is Serializable
, and
so we can put a File
into the arguments Bundle
:
static EditorFragment newInstance(File fileToEdit) {
EditorFragment frag=new EditorFragment();
Bundle args=new Bundle();
args.putSerializable(KEY_FILE, fileToEdit);
frag.setArguments(args);
return(frag);
}
In onCreateView()
of EditorFragment
, we inflate a layout that contains
our large EditText
widget and retrieve that EditText
widget:
@Override
public View onCreateView(LayoutInflater inflater,
ViewGroup container,
Bundle savedInstanceState) {
View result=inflater.inflate(R.layout.editor, container, false);
editor=(EditText)result.findViewById(R.id.editor);
return(result);
}
In addition to an editor
field for our EditText
, EditorFragment
has
two other fields. One is a LoadTextTask
, an AsyncTask
subclass that we
will use to load text from our file into our EditText
. The other is
loaded
, a simple boolean
to see if we have loaded our text yet:
private EditText editor;
private LoadTextTask loadTask=null;
private boolean loaded=false;
In onViewCreated()
, if we have not yet loaded the text, we kick off
a LoadTextTask
to do just that, passing in the File
that we put
into the arguments Bundle
:
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
if (!loaded) {
loadTask=new LoadTextTask();
loadTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,
(File)getArguments().getSerializable(KEY_FILE));
}
}
LoadTextTask
, in doInBackground()
, goes through a typical Java file I/O
read-all-the-lines process to read in a text file, if it exists. The resulting
string is poured into the EditText
. In onPostExecute()
, it updates
the EditText
with the read-in text, plus clears the loadTask
field
and sets loaded
to true
:
private class LoadTextTask extends AsyncTask<File, Void, String> {
@Override
protected String doInBackground(File... files) {
String result=null;
if (files[0].exists()) {
BufferedReader br;
try {
br=new BufferedReader(new FileReader(files[0]));
try {
StringBuilder sb=new StringBuilder();
String line=br.readLine();
while (line!=null) {
sb.append(line);
sb.append("\n");
line=br.readLine();
}
result=sb.toString();
}
finally {
br.close();
}
}
catch (IOException e) {
Log.e(getClass().getSimpleName(), "Exception reading file", e);
}
}
return(result);
}
@Override
protected void onPostExecute(String s) {
editor.setText(s);
loadTask=null;
loaded=true;
}
}
However, since we are using an AsyncTask
, we should retain this
fragment:
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
}
…and in onDestroy()
, we should cancel()
this task if it is still
running, as we no longer need the results:
@Override
public void onDestroy() {
if (loadTask!=null) {
loadTask.cancel(false);
}
super.onDestroy();
}
Rather than have some dedicated “save” action bar item or similar UI
element, we can just arrange to save the data when our fragment gets
paused. This is a typical approach in Android apps, as users do not
necessarily get an opportunity to click some “save” UI element, if they
get interrupted by a phone call or something. So, in onPause()
, we
kick off a SaveThread
to write our EditText
contents to the same
File
, once again pulled from the arguments Bundle
:
@Override
public void onPause() {
if (loaded) {
new SaveThread(editor.getText().toString(),
(File)getArguments().getSerializable(KEY_FILE)).start();
}
super.onPause();
}
However, note that we do not fork the SaveThread
if loaded
is
still false
. In that case, we know that we are still loading in the
text, which means the text cannot possibly have been modified by the user,
so there is nothing to save.
SaveThread
ensures that the directory we want to write to exists
(as it may or may not exist, particularly on emulators), then
uses Java Writer
objects to write out our text. Since there is nothing
that we want to do with the UI here, a plain Thread
, rather than
an AsyncTask
, is a better solution:
private static class SaveThread extends Thread {
private final String text;
private final File fileToEdit;
SaveThread(String text, File fileToEdit) {
this.text=text;
this.fileToEdit=fileToEdit;
}
@Override
public void run() {
try {
fileToEdit.getParentFile().mkdirs();
FileOutputStream fos=new FileOutputStream(fileToEdit);
Writer w=new BufferedWriter(new OutputStreamWriter(fos));
try {
w.write(text);
w.flush();
fos.getFD().sync();
}
finally {
w.close();
}
}
catch (IOException e) {
Log.e(getClass().getSimpleName(), "Exception writing file", e);
}
}
}
The reason for using a FileOutputStream
, and that mysterious
getFD().sync()
part, will be covered
later in this chapter.
The result is a set of tabbed editors, where the first one is our one for internal storage:
Figure 287: FilesEditor Sample, As Initially Launched
If you type something into the “Internal” tab, press BACK to exit the activity, and go back into the app again, whatever you typed in will be re-loaded from disk and will show up in the editor.
The files stored in internal storage are accessible only to your application, by default. Other applications on the device have no rights to read, let alone write, to this space. However, bear in mind that some users “root” their Android phones, gaining superuser access. These users will be able to read and write whatever files they wish. As a result, please consider application-local files to be secure against malware but not necessarily secure against interested users.
On most Android 1.x devices and some early Android 2.x devices, external storage came in the form of a micro SD card or the equivalent. On the remaining Android 2.x devices, external storage was part of the on-board flash, but housed in a separate partition from the internal storage. On most Android 3.0+ devices, external storage is now simply a special directory in the partition that holds internal storage.
Devices will have at least 1GB of external storage free when they ship to the user. That being said, many devices have much more than that, but the available size at any point could be smaller than 1GB, depending on how much data the user has stored.
If you have files that are tied to your application that are simply too big to
risk putting in internal storage, or if the user should be able to download
the files off their device at will, you can use getExternalFilesDir()
,
available on any activity or other Context
. This
will give you a File
object pointing to an automatically-created directory on
external storage, unique for your application. While not secure against other
applications, it does have one big advantage: when your application is
uninstalled, these files are automatically deleted, just like the ones in the
application-local file area. This method was added in API Level 8. This
method takes one parameter — typically null
— that indicates a particular
type of file you are trying to save (or, later, load).
In SampleAdapter
of the sample app, if the user chooses the “External” tab, we use
getExternalFilesDir()
to create the File
to be used by the
EditorFragment
:
case TAB_EXTERNAL:
fileToEdit=new File(ctxt.getExternalFilesDir(null), FILENAME);
break;
There is also getExternalCacheDir()
, which returns a File
pointing at a
directory that contains files that you would like to have, but if Android or
a third-party app clears the cache, your app will continue to function normally.
Android 4.4 (API Level 19) added two new methods, getExternalCacheDirs()
and getExternalFilesDirs()
, the plural versions of the classic methods.
These return an array of File
objects, representing one or more places
where your app can work with external storage. The first element in the
array will be the same File
object returned by the singular versions
of the methods (e.g., getExternalFilesDir()
). The other elements in the
array, if any, will represent app-specific directories
on alternative external storage locations, like
removable cards. The Android Support package has a ContextCompat
class
containing static versions of getExternalCacheDirs()
and
getExternalFilesDirs()
, so you can use the same code on API Level 4 and above,
though the backport will only ever return one directory in the array.
If you have files that belong more to the user than to your app — pictures
taken by the camera, downloaded MP3 files, etc. — a better solution is to
use getExternalStoragePublicDirectory()
, available on the Environment
class. This will give you a File
object pointing to a directory set aside for
a certain type of file, based on the type you pass into
getExternalStoragePublicDirectory()
. For example, you can ask for
DIRECTORY_MOVIES
, DIRECTORY_MUSIC
, or DIRECTORY_PICTURES
for storing MP4,
MP3, or JPEG files, respectively. These files will be left behind when your
application is uninstalled. This method was also added in API Level 8.
In SampleAdapter
of the sample app, if the user chooses the “Public” tab, we use
getExternalStoragePublicDirectory()
to create the File
to be used by the
EditorFragment
, putting our file in the DIRECTORY_DOCUMENTS
location:
default:
fileToEdit=
new File(Environment.
getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS),
FILENAME);
break;
You will also find a getExternalStorageDirectory()
method on Environment
,
pointing to the root of the external storage. This is no longer the preferred
approach — the methods described above help keep the user’s files better
organized. However, if you are supporting older Android devices, you may need
to use getExternalStorageDirectory()
, simply because the newer options may
not be available to you.
On all relevant Android versions prior to Android 4.4 (API Level 19), if
you want to write to external storage, you need to hold the WRITE_EXTERNAL_STORAGE
permission. And, on those versions, you do not need a permission to read
from external storage.
On Android 4.4 and up, the rules are a bit different:
getExternalFilesDir()
and getExternalCacheDir()
, you do not need a permissionWRITE_EXTERNAL_STORAGE
WRITE_EXTERNAL_STORAGE
(if you already have that) or READ_EXTERNAL_STORAGE
(if not)Hence, so long as your android:minSdkVersion
is less than 19, you
need to take the most conservative approach:
WRITE_EXTERNAL_STORAGE
permissionREAD_EXTERNAL_STORAGE
permissionNote that you might get paths to external storage locations from third-party
apps, typically in the form of a Uri
. If you are handling Uri
values
from third-party apps, you should request READ_EXTERNAL_STORAGE
or
WRITE_EXTERNAL_STORAGE
, in case the third-party app hands you a Uri
pointing to external storage.
For example, here is the sample
app’s manifest, complete with the <uses-permission>
element for
WRITE_EXTERNAL_STORAGE
:
<?xml version="1.0"?>
<manifest package="com.commonsware.android.fileseditor"
xmlns:android="http://schemas.android.com/apk/res/android"
android:versionCode="1"
android:versionName="1.0">
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<supports-screens
android:anyDensity="true"
android:largeScreens="true"
android:normalScreens="true"
android:smallScreens="true" />
<application
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/Theme.Apptheme">
<activity
android:name=".MainActivity"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
However, on Android 6.0+, WRITE_EXTERNAL_STORAGE
is one of those dangerous
permissions that we have to request at runtime. That is why this sample app
uses the AbstractPermissionActivity
profiled in
the material on runtime permissions. Overall, our
MainActivity
looks like this:
package com.commonsware.android.fileseditor;
import android.os.Bundle;
import android.support.v4.view.ViewPager;
import android.widget.Toast;
import io.karim.MaterialTabs;
import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
public class MainActivity extends AbstractPermissionActivity {
@Override
protected String[] getDesiredPermissions() {
return(new String[]{WRITE_EXTERNAL_STORAGE});
}
@Override
protected void onPermissionDenied() {
Toast
.makeText(this, R.string.msg_sorry, Toast.LENGTH_LONG)
.show();
finish();
}
@Override
protected void onReady(Bundle savedInstanceState) {
setContentView(R.layout.main);
ViewPager pager=(ViewPager)findViewById(R.id.pager);
pager.setAdapter(new SampleAdapter(this, getFragmentManager()));
MaterialTabs tabs=(MaterialTabs)findViewById(R.id.tabs);
tabs.setViewPager(pager);
}
}
getDesiredPermissions()
indicates that we want WRITE_EXTERNAL_STORAGE
,
and onPermissionDenied()
exits the app afters showing a Toast
. onReady()
is where we set up the tabs, as we now have all the permissions that we
need to be able to work with external storage.
Note that we do not need WRITE_EXTERNAL_STORAGE
for getExternalFilesDir()
on
API Level 19+ devices. This leads to another possible permission strategy
for this app:
android:maxSdkVersion="18"
to the <uses-permission>
element for WRITE_EXTERNAL_STORAGE
. This would indicate that we only want
this permission on devices that are running API Level 18 or lower.SampleAdapter
see what version of Android we are running
on. If we are running on API Level 19 or higher, we know that we did not
request WRITE_EXTERNAL_STORAGE
, but that we do not need that permission
for getExternalFilesDir()
. In that case, we could suppress the “Public” tab
(since we do not have permission to write there) and only show two tabs. But,
on older devices where we did ask for that permission, we could show all
three tabs (since we have rights for all of external storage).Also, external storage may be tied up by the user having mounted it as a USB
storage device. You can use getExternalStorageState()
(a static method on
Environment
) to determine if external storage is presently available or
not. On Android 3.0 and higher, this should be much less of an issue, as they
changed how the external storage is used by the host PC — originally, this
used USB Mass Storage Mode (think thumb drives) and now uses the USB Media
Transfer Protocol (think MP3 players). With MTP, both the Android device and
the PC it is connected to can have access to the files simultaneously; Mass
Storage Mode would only allow the host PC to have access to the files if external
storage is mounted.
Nowadays, you can use getStorageState()
on the EnvironmentCompat
class from the support-v4
library
to find out the state of external storage, for the
particular File
passed as a parameter.
The switch to MTP has one side-effect for Android developers: files you write
to external storage may not be automatically visible to the user. At the
time of this writing, the only files that will show up on the user’s PC will
be ones that have been indexed by the MediaStore
. While the MediaStore
is typically thought of as only indexing “media” (images, audio files, video
files, etc.), it was given the added role in Android 3.0 of maintaining an
index of all files for the purposes of MTP.
Your file that you place on external storage will not be indexed automatically simply by creating it and writing to it. Eventually, it will be indexed, though it may be quite some time for an automatic indexing pass to take place.
To force Android to index your file, you can use scanFile()
on
MediaScannerConnection
:
String[] paths={pathToYourNewFileOnExternalStorage};
MediaScannerConnection.scanFile(this, paths, null, null);
The third parameter to scanFile()
is an array of MIME types, to line
up with the array of paths in the second parameter. If your file is some
form of media, and you know the MIME type, supplying that will ensure that
your media will be visible as appropriate to the right apps (e.g., images
in the Gallery app). Otherwise, Android will try to infer a MIME type from
the file extension.
In the sample app, since the EditorFragment
does not know whether the file is on
external storage and therefore is reachable, it does not know whether
or not this sort of indexing is appropriate. In a more conventional
scenario, where the EditorFragment
would consistently be writing
to external storage, SaveThread
could arrange to invoke MediaScannerConnection
as part of its work. However, scanFile()
needs a Context
, and so
the SaveThread
would need one of those. You would wind up with
something a bit like:
private static class SaveThread extends Thread {
private final String text;
private final File fileToEdit;
private final Context ctxt;
SaveThread(Context ctxt, String text, File fileToEdit) {
this.ctxt=ctxt.getApplicationContext();
this.text=text;
this.fileToEdit=fileToEdit;
}
@Override
public void run() {
try {
fileToEdit.getParentFile().mkdirs();
FileOutputStream fos=new FileOutputStream(fileToEdit);
Writer w=new BufferedWriter(new OutputStreamWriter(fos));
try {
w.write(text);
w.flush();
fos.getFD().sync();
}
finally {
w.close();
String[] paths={fileToEdit};
MediaScannerConnection.scanFile(ctxt, paths, null, null);
}
}
catch (IOException e) {
Log.e(getClass().getSimpleName(), "Exception writing file", e);
}
}
}
Here, we use getApplicationContext()
, which returns to us a Context
that is a process-wide singleton. That way, if our activity is destroyed
while the thread is still running, we still have a valid Context
to use.
Many Android devices will have a per-process limit of 1024 open files, on any sort of storage. This is usually not a problem for developers.
On some devices — including probably all that are running Android 4.2 and higher — there is a global limit of 1024 open files on external storage. In other words, all running apps combined can only open 1024 files simultaneously on external storage.
This means that it is important for you to minimize how many open files on external storage you have at a time. Having a few open files is perfectly reasonable; having a few hundred open files is not.
On Android 4.1 and earlier, each Android device was assumed to be used by just one person.
On Android 4.2+ tablets — and Android 5.0+ phones — it is
possible for a device’s owner to set up
multiple user accounts. Each user gets their own section of internal and
external storage for files, databases, SharedPreferences
, and so forth. From
your standpoint, it is as if the users are really on different devices, even
though in reality it is all the same hardware.
However, this means that paths to internal and external storage now may vary
by user. Hence, it is very important for you to use the appropriate methods,
outlined in this chapter, for finding locations on internal storage
(e.g., getFilesDir()
) and external storage (e.g., getExternalFilesDir()
).
Some blog posts, Stack Overflow answers, and the like will show the use of
hard-coded paths for these locations (e.g., /sdcard
or /mnt/sdcard
for
the root of external storage). Hard-coding such paths was never a good idea.
And, as of Android 4.2, those paths are simply wrong and will not work.
On Android 4.2+, for the original user of the device,
internal storage will wind up in the
same location as before, but external storage will use a different path. For
the second and subsequent users defined on the device, both internal and
external storage will reside in different paths. The various methods, like
getFilesDir()
, will handle this transparently for you.
Note that, at the time of this writing, multiple accounts are not available on the emulators, only on actual tablets. Phones usually will not have multiple-account support, under the premise that tablets are more likely to be shared than are phones.
Android is built atop a Linux kernel and uses Linux filesystems for holding its files. Classically, Android used YAFFS (Yet Another Flash File System), optimized for use on low-power devices for storing data to flash memory.
YAFFS has one big problem: only one process can write to the filesystem at a time. For those of you into filesystems, rather than offering file-level locking, YAFFS has partition-level locking. This can become a bit of a bottleneck, particularly as Android devices grow in power and start wanting to do more things at the same time like their desktop and notebook brethren.
Android 3.0 switched to ext4, another Linux filesystem aimed more at desktops/notebooks. Your applications will not directly perceive the difference. However, ext4 does a fair bit of buffering, and it can cause problems for applications that do not take this buffering into account. Linux application developers ran headlong into this in 2008-2009, when ext4 started to become popular. Android developers will need to think about it now… for your own file storage.
If you are using SQLite or SharedPreferences
, you do not need to worry about
this problem. Android (and SQLite, in the case of SQLite) handle all the
buffering issues for you. If, however, you write your own files, you may wish
to contemplate an extra step as you flush your data to disk. Specifically, you
need to trigger a Linux system call known as fsync()
, which tells the
filesystem to ensure all buffers are written to disk.
If you are using java.io.RandomAccessFile
in a synchronous mode, this step
is handled for you as well, so you will not need to worry about it. However,
Java developers tend to use FileOutputStream
, which does not trigger an
fsync()
, even when you call close()
on the stream. Instead, you call
getFD().sync()
on the FileOutputStream
to trigger the fsync()
. Note that
this may be time-consuming, and so disk writes should be done off the main
application thread wherever practical, such as via an AsyncTask
.
This is why, in EditorFragment
, our SaveThread
implementation looks like this:
private static class SaveThread extends Thread {
private final String text;
private final File fileToEdit;
SaveThread(String text, File fileToEdit) {
this.text=text;
this.fileToEdit=fileToEdit;
}
@Override
public void run() {
try {
fileToEdit.getParentFile().mkdirs();
FileOutputStream fos=new FileOutputStream(fileToEdit);
Writer w=new BufferedWriter(new OutputStreamWriter(fos));
try {
w.write(text);
w.flush();
fos.getFD().sync();
}
finally {
w.close();
}
}
catch (IOException e) {
Log.e(getClass().getSimpleName(), "Exception writing file", e);
}
}
}
While we use a Writer
to do the writing, it is wrapped around
a FileOutputStream
, so we can get access to the FileDescriptor
(via getFD()
) and call sync()
on it.
Users are more likely to like your application if, to them, it feels responsive. Here, by “responsive”, we mean that it reacts swiftly and accurately to user operations, like taps and swipes.
Conversely, users are less likely to be happy with you if they perceive that your UI is “janky” — sluggish to respond to their requests. For example, maybe your lists do not scroll as smoothly as they would like, or tapping a button does not yield the immediate results they seek.
While threads and AsyncTask
and the like can help, it may not always be
obvious where you should be applying them. A full-scale performance analysis,
using Traceview or similar Android tools, is certainly possible. However, there
are a few standard sorts of things that developers do, sometimes quite by
accident, on the main application thread that will tend to cause sluggishness:
However, even here, it may not be obvious that you are performing these operations on the main application thread. This is particularly true when the operations are really being done by Android’s code that you are simply calling.
That is where StrictMode
comes in. Its mission is to help you determine when
you are doing things on the main application thread that might cause a janky
user experience.
StrictMode
works on a set of policies. There are presently two categories of
policies: VM policies and thread policies. The former represent bad coding
practices that pertain to your entire application, notably leaking SQLite
Cursor
objects and kin. The latter represent things that are bad when
performed on the main application thread, notably flash I/O and network I/O.
Each policy dictates what StrictMode
should watch for (e.g., flash reads are
OK but flash writes are not) and how StrictMode
should react when you violate
the rules, such as:
The simplest thing to do is call the static enableDefaults()
method on
StrictMode
from onCreate()
of your first activity. This will set up normal
operation, reporting all violations by simply logging to LogCat. However, you
can set your own custom policies via Builder
objects if you so choose.
However, do not use StrictMode
in production code. It is designed for use
when you are
building, testing, and debugging your application. It is not designed to be
used in the field.
So, for example, you might have something like this in your launcher activity:
StrictMode.ThreadPolicy.Builder b=new StrictMode.ThreadPolicy.Builder();
if (BuildConfig.DEBUG) {
b.detectAll().penaltyDeath();
}
else {
b.detectAll().penaltyLog();
}
StrictMode.setThreadPolicy(b.build());
BuildConfig.DEBUG
will be true
for debuggable builds, false otherwise.
So, in the case of a debug build, we want to detect all mistakes and
crash the app immediately when we encounter them, but in production,
we want to just log information about the mistake to LogCat.
You will note that the sample app does not contain this code.
That is because calling methods like getFilesDir()
and getExternalFilesDir()
really ought to be on background threads, as StrictMode
will complain
about them. Hence, this code would cause SampleAdapter
to crash when
it tries building the File
object to use. This could be rectified by
having SampleAdapter
simply pass in a flag indicating the storage location
and having LoadThreadTask
and SaveThread
deal with the File
objects.
Note that StrictMode
will also report leaked open files. For example, if you
create a FileOutputStream
on a File
and fail to close()
it later, when
the FileOutputStream
(and related objects) are garbage-collected, StrictMode
will report to you the fact that you failed to close the stream. This is very
useful to help you make sure that you are not leaking open files that may
contribute to
exhausting the 1,024 open file limit on external storage.
All this reading and writing of data is nice, but for debugging and diagnostic purposes, it is often useful for you to be able to look at the files, other than through your app.
This is somewhat challenging, due to the lack of tools and due to security restrictions in production devices (as compared to emulators).
That being said, the following sections will outline some options that you have to access your app’s files independently of your app.
If you have an actual Android device, when you plug it in via a USB cable, usually you will get external storage available as a drive letter (Windows) or a mounted volume (OS X and Linux). Depending upon the device, manufacturer, and configuration, you might also have access to removable storage this way as well.
In these cases, you can use your development machine’s OS to poke around these file locations and look at your files (or anyone else’s).
However, there are some wrinkles:
Notification
for the USB connection, to toggle it to share files using MTP.You can get at external storage (and possibly removable storage) of
devices and emulators via
the command-line adb
tool. This program is in platform-tools/
of your Android SDK installation, and it is a good idea to add that
directory to your operating system’s PATH
environment variable, so
you can run adb
from anywhere.
adb push
and adb pull
allow you to upload and download
files, respectively. Both take the local path and the remote (device/emulator)
path as command-line arguments, although in varying order:
adb push localpath remotepath
will upload the file represented
by localpath
to the location represented by remotepath
adb pull remotepath localpath
will download the file represented
by remotepath
to the location represented by localpath
For external storage, the root directory name varies by Android OS version:
/sdcard/
/mnt/shell/emulated/0/
/storage/emulated/0/
.So, for example, the following command would push an index.html
file to the getExternalFilesDir()
location for the primary device
account, for an app whose application is your.package.name.here
:
adb push index.html /storage/emulated/0/Android/data/your.package.name.here/files
If you try to push
a local directory, or pull
a remote directory,
the contents of those directories will be uploaded and downloaded, respectively.
However, the directory itself is not, which can cause some confusion.
Suppose we have a directory on our development PC named foo/
. It
contains four PNG files, named 1.png
, 2.png
, 3.png
,
and parallelism-is-boring.png
. We then execute the following command
on the command line:
adb push foo /storage/emulated/0/Android/data/your.package.name.here/files
You will wind up with:
/storage/emulated/0/Android/data/your.package.name.here/files/1.png
/storage/emulated/0/Android/data/your.package.name.here/files/2.png
/storage/emulated/0/Android/data/your.package.name.here/files/3.png
/storage/emulated/0/Android/data/your.package.name.here/files/parallelism-is-boring.png
Note, though, that the foo
directory name is not included. In other
words, the contents of foo/
are transferred, but not foo/
itself.
adb push
and adb pull
work directly for internal storage as
well… on emulators.
On production hardware, though, you have some additional work to do.
Specifically, you need to use external storage as an intermediary and
use adb run-as
to give yourself the temporary ability to work
with internal storage.
For example, on an emulator, you could push index.html
to
the directory returned by getFilesDir()
, for an app with an application
ID of your.package.name.here
, for the primary device account, via:
adb push index.html /data/data/your.package.name.here/files
If you try that on production hardware, it will fail. While the
piece that adb
communicates with on the emulator runs with
superuser privileges, the equivalent piece on production hardware
does not. The same security that prevents other apps from accessing
your app’s portion of internal storage prevents adb
from doing
so as well.
However, adb
on production hardware can use the run-as
command, to execute a Linux command as if it were being run by the
Linux user associated with your app, the user that owns all your
files and who has read/write access to those files.
So, the equivalent script to copy the file to internal storage on a production Android 4.x/5.x device would be:
adb push index.html /mnt/shell/emulated/0
adb shell run-as your.package.name.here cp /mnt/shell/emulated/0/index.html /data/data/your.package.name.here/files
adb shell rm /mnt/shell/emulated/0/index.html
(note that the second command should appear all on one line, even though it may show up as word-wrapped here due to the length of the line and the available width of the book)
This will only work for debuggable apps, which is the normal state of apps that you run from your IDE. This script:
run-as
to run the Linux cp
command to copy the file from
external storage to the app’s internal storagerm
command to remove the file that we placed
on external storage(if you are wondering why we do not use mv
instead of cp
and
rm
, mv
generates errors related to attempting to change the
ownership of the moved file)
Android supports a fairly standard implementation of the Java DOM and SAX APIs. If you have existing experience with these, or if you have code that already leverages them, feel free to use them.
Android also bakes in the XmlPullParser
from the xmlpull.org site.
Like SAX, the XmlPullParser
is an event-driven interface, compared to the DOM
that builds up a complete data structure and hands you that result. Unlike SAX,
which relies on a listener and callback methods, the XmlPullParser
has you
pull events off a queue, ignoring those you do not need and dispatching the
rest as you see fit to the rest of your code.
The primary reason the XmlPullParser
was put into Android was for XML-encoded
resources. While you write plain-text XML during development, what is packaged
in your APK file is a so-called “binary XML” format, where angle brackets and
quotation marks and such are replaced by bitfields. This helps compression a
bit, but mostly this conversion is done to speed up parsing. Android’s
XML resource parser can parse this “binary XML” approximately ten times faster
than it can parse the equivalent plain-text XML. Hence, anything you put
in an XML resource (res/xml/
) will be parsed similarly quickly.
For plain-text XML content, the XmlPullParser
is roughly equivalent, speed-wise,
to SAX. All else being equal, lean towards SAX, simply because more developers
will be familiar with it from classic Java development. However, if you really
like the XmlPullParser
interface, feel free to use it.
You are welcome to try a third-party XML parser JAR, but bear in mind that there may be issues when trying to get it working in Android.
Android has bundled the org.json
classes into the SDK since the beginning,
for use in parsing JSON. These classes have a DOM-style interface: you hand
JSONObject
a hunk of JSON, and it gives you an in-memory representation
of the completely parsed result. This is handy but, like the DOM, a bit of a
performance hog.
API Level 11 added JSONReader
, based on Google’s GSON parser, as a “streaming”
parser alternative. JSONReader
is much more reminiscent of the XmlPullParser
,
in that you pull events out of the “reader” and process them. This can have
significant performance advantages, particularly in terms of memory consumption,
if you do not need the entire JSON data structure. However, this is only
available on API Level 11 and higher.
Because JSONReader
is a bit “late to the party”, there has been extensive work
on getting other JSON parsers working on Android.
Google’s GSON is popular, as
is Jackson. Jackson offers a few APIs,
and the streaming API reportedly works very nicely on Android with top-notch
performance.
Earlier, we saw how to use an implicit Intent
to, say, view
a Web page, given an https
URL. You can do the same sort of thing with files…
though there are issues.
Technically, you can take any File
, pass it to Uri.fromFile()
, and get a Uri
pointing to that file. You can put that Uri
into an implicit Intent
, such
as one for ACTION_VIEW
, and pass that Intent
to startActivity()
:
startActivity(new Intent(Intent.ACTION_VIEW, Uri.fromFile(somethingCool)));
However, at best, this only works for files on external storage. Other apps — such
as whatever activity handles your ACTION_VIEW
request — do not have rights
to your portion of internal storage or your portion of removable storage.
Plus, you have no guarantee that the other app has either the READ_EXTERNAL_STORAGE
or WRITE_EXTERNAL_STORAGE
permission (though, if it responded to your Intent
,
it should).
Hence, in Android 7.0, the file
scheme on a Uri
is banned, in effect.
If you attempt to pass a file:
Uri
in an Intent
that is going
to another app, you will crash with a FileUriExposedException
exception.
(you will face similar issues with putting file:
Uri
values on the
clipboard in ClipData
— coverage of the clipboard is later in this book)
This is coming from an updated edition of StrictMode
.
StrictMode.VmPolicy.Builder
has a penaltyDeathOnFileUriExposure()
method that triggers the detection of file:
Uri
values and
the resulting FileUriExposedException
exceptions. And, it appears
that this is pre-configured, much as how StrictMode
is pre-configured
to apply penaltyDeathOnNetwork()
(the source of your
NetworkOnMainThreadException
crashes).
However, this only kicks in if your targetSdkVersion
is set to 24 or higher.
At that point, you will need to find other ways of getting your content to
other apps, such as via a class called FileProvider
, which is covered
later in this book.
Or, you can also disable the check by configuring your own
StrictMode.VmPolicy
and skipping directFileUriExposure()
, though this is
not a great solution.
In addition to this chapter, you can learn more about accessing multimedia
files via the MediaStore
and learn more about
the impacts of multiple user accounts on tablets.