Data persistence is a wide subject area. In this chapter we focus on selected topics, including:
Filesystem topics relating to the app-accessible parts of the filesystems (/sdcard and friends)—but we assume you know the basics of reading/writing text files in Java.
Persisting data in a database, commonly but not exclusively SQLite.
Reading data from the database, and doing various conversions on it.
Reading and writing the Preferences data, used to store small per-application customization values.
Some data format conversions (e.g., JSON and XML conversions) that don’t fit naturally into any of the other chapters.
The Android ContentProvider
, which allows unrelated applications to share data in the form of a SQLite cursor. We’ll focus specifically on the Android Contacts provider.
Drag and drop, which might seem to be a GUI topic but typically involves using a ContentProvider
.
FileProvider
, a simplification of the ContentProvider
that allows unrelated applications to share individual files.
The SyncAdapter
mechanism, which allows data to be synchronized to/from a backend database;
we discuss one example, that of synchronizing “todo list” items.
Finally, we cover a new cloud database called Firebase from Google.
Ian Darwin
Every Android device features a fairly complete Unix/Linux filesystem hierarchy. Parts of it are “off-limits” for normal applications, to ensure that the device’s integrity and functionality are not compromised. Storage areas available for reading/writing within the application are divided into “internal” storage, which is private per application, and “public” storage, which may be accessed by other applications.
Internal storage is always located in the device’s “flash memory” area—part of the 8 GB or 32 GB of “storage” that your device was advertised with—under /data/data/PKG_NAME/. External storage may be on the SD card (which should technically be called “removable storage,” as it might be a MicroSD card or even some other media type). However, there are several complications.
First, some devices don’t have removable storage. On these, the external storage directory always exists—it is just in a different partition of the same flash memory storage as internal storage.
Second, on devices that do have removable storage, the storage might be removed at the time your application checks it. There’s no point trying to write it if it’s not there.
On these devices, as well, the storage might be “read-only” as most removable media memory devices have a “write-protect” switch that disables power to the write circuitry.
Internal storage can be accessed using the Context
methods openFileInput(String filename)
, which returns a FileInputStream
object, or openFileOutput(String filename, int mode)
, which returns a FileOutputStream
. The mode
value should either be 0
for a new file, or Context.MODE_APPEND
to add to the end of an existing file. Other mode
values are deprecated and should not be used.
Once obtained, these streams can be read using standard java.io
classes and methods (e.g., wrap the FileInputStream
in an InputStreamReader
and a BufferedReader
to get line-at-a-time access to character data). You should close these when finished with them.
Alternatively, the Context
method getFilesDir()
returns the root of this directory,
and you can then access it using normal java.io
methods and classes.
Example 10-1 shows a simple example of writing a file into this area and then reading it back.
try
(
FileOutputStream
os
=
openFileOutput
(
DATA_FILE_NAME
,
Context
.
MODE_PRIVATE
))
{
os
.
write
(
message
.
getBytes
());
println
(
"Wrote the string "
+
message
+
" to file "
+
DATA_FILE_NAME
);
}
catch
(
IOException
e
)
{
println
(
"Failed to write "
+
DATA_FILE_NAME
+
" due to "
+
e
);
}
// Get the absolute path to the directory for our app's internal storage
File
where
=
getFilesDir
();
println
(
"Our private dir is "
+
where
.
getAbsolutePath
());
try
(
BufferedReader
is
=
new
BufferedReader
(
new
InputStreamReader
(
openFileInput
(
DATA_FILE_NAME
))))
{
String
line
=
is
.
readLine
();
println
(
"Read the string "
+
line
);
}
catch
(
IOException
e
)
{
println
(
"Failed to read back "
+
DATA_FILE_NAME
+
" due to "
+
e
);
}
Accessing the external storage is, predictably, more complicated. The mental model of external storage is a removable flash-memory card such as an SD card. Initially most devices had an SD card or MicroSD card slot. Today most do not, but some still do, so Android always treats the SD card as though it might be removable, or might be present but the write-protect switch might be enabled. You should not access it by the directory name /sdcard, but rather through API calls. On some devices the path may change when an SD card is inserted or removed. And, Murphy’s Law being what it is, a removable card can be inserted or removed at any time; the users may or may not know that they’re supposed to unmount it in software before removing it.
Be prepared for IOException
s!
For external storage you will usually require the READ_EXTERNAL_STORAGE
or WRITE_EXTERNAL_STORAGE
permission (note that WRITE
implies READ
, so you don’t need both):
<uses-permission
android:name=
"android.permission.WRITE_EXTERNAL_STORAGE"
/>
The first thing your application must do is check if the external storage is even available. The method static String Environment.getExternalStorageState()
returns one of almost a dozen String
values, but unless you are writing a File Manager–type application, you only care about two: MEDIA_MOUNTED
and MEDIA_MOUNTED_READ_ONLY
. Any other values imply that the external storage directory is not currently usable. You can verify this as follows:
String
state
=
Environment
.
getExternalStorageState
();
println
(
"External storage state = "
+
state
);
if
(
state
.
equals
(
Environment
.
MEDIA_MOUNTED_READ_ONLY
))
{
mounted
=
true
;
readOnly
=
true
;
println
(
"External storage is read-only!!"
);
}
else
if
(
state
.
equals
(
Environment
.
MEDIA_MOUNTED
))
{
mounted
=
true
;
readOnly
=
false
;
println
(
"External storage is usable"
);
}
else
{
println
(
"External storage NOT USABLE"
);
}
Once you’ve ascertained that the external storage is usable, you can create files and/or directories in it.
The Environment
class exposes a bunch of public directory types, such as DIRECTORY_MUSIC
for playable
music, DIRECTORY_RINGTONES
for music files that should only be used as telephone ringtones, DIRECTORY_MOVIES
for videos, and so on. If you use one of these your files will be placed in the correct directory for the
stated purpose. You can create subdirectories here using File.mkdirs()
:
// Get the external storage folder for Music
final
File
externalStoragePublicDirectory
=
Environment
.
getExternalStoragePublicDirectory
(
Environment
.
DIRECTORY_MUSIC
);
// Get the directory for the user's public pictures directory.
// We want to use it, for example, to create a new music album.
File
albumDir
=
new
File
(
externalStoragePublicDirectory
,
"Jam Session 2017"
);
albumDir
.
mkdirs
();
if
(!
albumDir
.
isDirectory
())
{
println
(
"Unable to create music album"
);
}
else
{
println
(
"Music album exists as "
+
albumDir
);
}
You could then create files in the subdirectory that would show up as the “album” “Jam Session 2017.”
The final category of files is “private external storage.” This is just a directory with your
package name in it, for convenience, and offers zero security—any application with the
appropriate EXTERNAL_STORAGE
permission can read or write to it.
However, it has the advantage that it will be removed if the user uninstalls your app.
This category is thus intended for use for configuration and data files that are specific to your
application, and should not be used for files that logically “belong” to the user.
Directories in this category are accessed by passing null
to the getExternalStorageDirectory()
method,
as in Example 10-2.
// Finally, we'll create an "application private" file on /sdcard,
// Note that these are accessible to all other applications!
final
File
privateDir
=
getExternalFilesDir
(
null
);
File
semiPrivateFile
=
new
File
(
privateDir
,
"fred.jpg"
);
try
(
OutputStream
is
=
new
FileOutputStream
(
semiPrivateFile
))
{
println
(
"Writing to "
+
semiPrivateFile
);
// Do some writing here...
}
catch
(
IOException
e
)
{
println
(
"Failed to create "
+
semiPrivateFile
+
" due to "
+
e
);
}
The sample project FilesystemDemos includes all this code plus a bit more. Running it will produce the result shown in Figure 10-1.
For getting information about files, and directory listings, see Recipe 10.2. To read static files that are shipped (read-only) as part of your application, see Recipe 10.3. For more information on data storage options in Android, refer to the official documentation.
The source code for this example is in the Android Cookbook repository, in the subdirectory FilesystemDemos (see “Getting and Using the Code Examples”).
Ian Darwin
The File
class has a number of “informational” methods. To use any of these, you must construct a File
object containing the name of the file on which it is to operate. It should be noted up front that creating a File
object has no effect on the permanent filesystem; it is only an object in Java’s memory. You must call methods on the File
object in order to change the filesystem; there are numerous “change” methods, such as one for creating a new (but empty) file, one for renaming a file, and so on, as well as many informational methods. Table 10-1 lists some of the informational methods.
Return type | Method name | Meaning |
---|---|---|
|
|
|
|
|
Full name |
|
|
Relative filename |
|
|
Parent directory |
|
|
|
|
|
|
|
|
File modification time |
|
|
File size |
|
|
|
|
|
|
|
|
List contents if it’s a directory |
|
|
List contents if it’s a directory |
You cannot change the name stored in a File
object; you simply create a new File
object each time you need to refer to a different file.
Standard Java as of JDK 1.7 includes java.nio.Files
, which is a newer replacement for the File
class,
but Android does not yet ship with this class.
Example 10-3 is drawn from desktop Java, but the File
object operates the same in Android as in Java SE.
import
java.io.*
;
import
java.util.*
;
/**
* Report on a file's status in Java
*/
public
class
FileStatus
{
public
static
void
main
(
String
[]
argv
)
throws
IOException
{
// Ensure that a filename (or something) was given in argv[0]
if
(
argv
.
length
==
0
)
{
System
.
err
.
println
(
"Usage: FileStatus filename"
);
System
.
exit
(
1
);
}
for
(
int
i
=
0
;
i
<
argv
.
length
;
i
++)
{
status
(
argv
[
i
]);
}
}
public
static
void
status
(
String
fileName
)
throws
IOException
{
System
.
out
.
println
(
"---"
+
fileName
+
"---"
);
// Construct a File object for the given file
File
f
=
new
File
(
fileName
);
// See if it actually exists
if
(!
f
.
exists
())
{
System
.
out
.
println
(
"file not found"
);
System
.
out
.
println
();
// Blank line
return
;
}
// Print full name
System
.
out
.
println
(
"Canonical name "
+
f
.
getCanonicalPath
());
// Print parent directory if possible
String
p
=
f
.
getParent
();
if
(
p
!=
null
)
{
System
.
out
.
println
(
"Parent directory: "
+
p
);
}
// Check our permissions on this file
if
(
f
.
canRead
())
{
System
.
out
.
println
(
"File is readable by us."
);
}
// Check if the file is writable
if
(
f
.
canWrite
())
{
System
.
out
.
println
(
"File is writable by us."
);
}
// Report on the modification time
Date
d
=
new
Date
();
d
.
setTime
(
f
.
lastModified
());
System
.
out
.
println
(
"Last modified "
+
d
);
// See if file, directory, or other. If file, print size.
if
(
f
.
isFile
())
{
// Report on the file's size
System
.
out
.
println
(
"File size is "
+
f
.
length
()
+
" bytes."
);
}
else
if
(
f
.
isDirectory
())
{
System
.
out
.
println
(
"It's a directory"
);
}
else
{
System
.
out
.
println
(
"So weird, man! Neither a file nor a directory!"
);
}
System
.
out
.
println
();
// Blank line between entries
}
}
Take a look at the output produced when the program is run (on MS Windows) with the three command-line arguments shown, it produces the output shown here:
C:javasrcdir_file> java FileStatus / /tmp/id /autoexec.bat ---/--- Canonical name C: File is readable. File is writable. Last modified Thu Jan 01 00:00:00 GMT 1970 It's a directory ---/tmp/id--- file not found ---/autoexec.bat--- Canonical name C:AUTOEXEC.BAT Parent directory: File is readable. File is writable. Last modified Fri Sep 10 15:40:32 GMT 1999 File size is 308 bytes.
As you can see, the so-called canonical name not only includes a leading directory root of C:, but also has had the name converted to uppercase. You can tell I ran that on an older version of Windows. On Unix, it behaves differently, as you can see here:
$ java FileStatus / /tmp/id /autoexec.bat ---/--- Canonical name / File is readable. Last modified October 4, 1999 6:29:14 AM PDT It's a directory ---/tmp/id--- Canonical name /tmp/id Parent directory: /tmp File is readable. File is writable. Last modified October 8, 1999 1:01:54 PM PDT File size is 0 bytes. ---/autoexec.bat--- file not found $
This is because a typical Unix system has no autoexec.bat file. And Unix filenames (like those on the filesystem inside your Android device, and those on a Mac) can consist of upper- and lowercase characters: what you type is what you get.
The java.io.File
class also contains methods for working with directories. For example, to list the filesystem entities named in the current directory, just write:
String
[]
list
=
new
File
(
"."
).
list
()
To get an array of already constructed File
objects rather than strings, use:
File
[]
list
=
new
File
(
"."
).
listFiles
();
You can display the result in a ListView
(see Recipe 8.2).
Of course, there’s lots of room for elaboration. You could print the names in multiple columns across or down the screen in a TextView
in a monospace font, since you know the number of items in the list before you print. You could omit filenames with leading periods, as does the Unix ls
program, or you could print the directory names first, as some “file manager"–type programs do. By using listFiles()
, which constructs a new File
object for each name, you could print the size of each, as per the MS-DOS dir
command or the Unix ls -l
command. Or you could figure out whether each object is a file, a directory, or neither. Having done that, you could pass each directory to your top-level function, and you would have directory recursion (the equivalent of using the Unix find
command, or ls -R
, or the DOS DIR /S
command). Quite the makings of a file manager application of your own!
A more flexible way to list filesystem entries is with list(FilenameFilter ff)
. FilenameFilter
is a tiny interface with only one method: boolean accept(File inDir, String fileName)
. Suppose you want a listing of only Java-related files (*.java, *.class, *.jar, etc.). Just write the accept()
method so that it returns true
for these files and false
for any others. Example 10-4 shows the Ls
class warmed over to use a FilenameFilter
instance.
import
java.io.*
;
/**
* FNFilter - directory lister modified to use FilenameFilter
*/
public
class
FNFilter
{
public
static
String
[]
getListing
(
String
startingDir
)
{
// Generate the selective list, with a one-use File object
String
[]
dir
=
new
java
.
io
.
File
(
startingDir
).
list
(
new
OnlyJava
());
java
.
util
.
Arrays
.
sort
(
dir
);
// Sorts by name
return
dir
;
}
/** FilenameFilter implementation:
* The accept() method only returns true for .java , .jar, and .class files.
*/
class
OnlyJava
implements
FilenameFilter
{
public
boolean
accept
(
File
dir
,
String
s
)
{
if
(
s
.
endsWith
(
".java"
)
||
s
.
endsWith
(
".jar"
)
||
s
.
endsWith
(
".dex"
))
return
true
;
// Others: projects, ... ?
return
false
;
}
}
We could make the FilenameFilter
a bit more flexible; in a full-scale application, the list of files returned by the FilenameFilter
would be chosen dynamically, possibly automatically, based on what you were working on. File chooser dialogs implement this as well, allowing the user to select interactively from one of several sets of files to be listed. This is a great convenience in finding files, just as it is here in reducing the number of files that must be examined.
For the listFiles()
method, there is an additional overload that accepts a FileFilter
. The only difference is that FileFilter
’s accept()
method is called with a File
object, whereas FileNameFilter
’s is called with a filename string.
See Recipe 8.2 to display the results in your GUI. Chapter 11 of Java Cookbook, written by me and published by O’Reilly, has more information on file and directory operations.
Rachee Singh
The standard file-oriented Java I/O classes can only open files stored on “disk,” as described in Recipe 10.1. If you want to read a file that is a static part of your application (installed as part of the APK rather than downloaded), you can access it in one of two special places.
If you’d like to read a static file, you can place it either in the assets directory or in res/raw.
For res/raw, open it with the getResources()
and openRawResource()
methods, and then read it normally.
For assets, you access the file as a filesystem entry (see Recipe 10.1); Android maps this directory to file:///android_asset/ (note the triple slash and singular spelling of “asset”).
We wish to read information from a file packaged with the Android application, so we will need to put the relevant file in the res/raw directory or the assets directory (and probably create the directory, since it is often not created by default).
If the file is stored in res/raw, the generated R
class will have
an ID for it, which we pass into openRawResource()
. Then we will read
the file using the returned InputStreamReader
wrapped in a
BufferedReader
. Finally, we extract the string from the
BufferedReader
using the readLine()
method.
If the file is stored in assets, it will appear to be in the file:///android_asset/ directory, which we can just open and read normally.
In both cases the IDE will ask us to enclose the readLine()
function within a try-catch
block since there is a possibility of it throwing an IOException
.
For res/$$raw the file is named samplefile and is shown in Example 10-5.
InputStreamReader
is
=
new
InputStreamReader
(
this
.
getResources
().
openRawResource
(
R
.
raw
.
samplefile
));
BufferedReader
reader
=
new
BufferedReader
(
is
);
StringBuilder
finalText
=
new
StringBuilder
();
String
line
;
try
{
while
((
line
=
reader
.
readLine
())
!=
null
)
{
finalText
.
append
(
line
);
}
}
catch
(
IOException
e
)
{
e
.
printStackTrace
();
}
fileTextView
=
(
TextView
)
findViewById
(
R
.
id
.
fileText
);
fileTextView
.
setText
(
finalText
.
toString
());
After reading the entire string, we set it to the TextView
in the Activity.
When using the assets folder, the most common use is for loading a web resource into a WebView
.
Suppose we have samplefile.html stored in the assets folder;
the code in Example 10-6 will load it into a web display.
webView
=
(
WebView
)
findViewById
(
R
.
id
.
about_html
);
webview
.
loadUrl
(
"file:///android_asset/samplefile.html"
);
Figure 10-2 shows the result of both the text and HTML files.
The source code for this example is in the Android Cookbook repository, in the subdirectory StaticFileRead (see “Getting and Using the Code Examples”).
Amir Alagic
Here is some code that obtains the information:
StatFs
statFs
=
new
StatFs
(
Environment
.
getExternalStorageDirectory
().
getPath
());
double
bytesTotal
=
(
long
)
statFs
.
getBlockSize
()
*
(
long
)
statFs
.
getBlockCount
();
double
megTotal
=
bytesTotal
/
1048576
;
To get the total space on the SD card, use StatFs
in the android.os
package. Use Environment.getExternalStorageDirectory().getPath()
as a constructor parameter.
Then, multiply the block size by the number of blocks on the SD card:
(
long
)
statFs
.
getBlockSize
()
*
(
long
)
statFs
.
getBlockCount
();
To get size in megabytes, divide the result by 1048576. To get the amount of available space on the SD card, replace statFs.getBlockCount()
with statFs.getAvailableBlocks()
:
(
long
)
statFs
.
getBlockSize
()
*
(
long
)
statFs
.
getAvailableBlocks
();
If you want to display the value with two decimal places you can use a DecimalFormat
object from java.text
:
DecimalFormat
twoDecimalForm
=
new
DecimalFormat
(
"#.##"
);
Ian Darwin
Android will happily maintain a SharedPreferences
object for you in semipermanent storage. To retrieve settings from it, use:
sharedPreferences
=
PreferenceManager
.
getDefaultSharedPreferences
(
this
);
This should be called in your main Activity’s onCreate()
method, or in the onCreate()
of any Activity that needs to view the user’s chosen preferences.
You do need to tell Android what values you want the user to be able to specify, such as name, Twitter account, favorite color, or whatever. You don’t use the traditional view items such as ListView
or Spinner
, but instead use the special Preference
items. A reasonable set of choices are available, such as List
s, TextEdit
s, CheckBox
es, and so on, but remember, these are not the standard View
subclasses. Example 10-7 uses a List
, a TextEdit
, and a CheckBox
.
<PreferenceScreen
xmlns:android=
"http://schemas.android.com/apk/res/android"
>
<ListPreference
android:key=
"listChoice"
android:title=
"List Choice"
android:entries=
"@array/choices"
android:entryValues=
"@array/choices"
/>
<PreferenceCategory
android:title=
"Personal"
>
<EditTextPreference
android:key=
"nameChoice"
android:title=
"Name"
android:hint=
"Name"
/>
<CheckBoxPreference
android:key=
"booleanChoice"
android:title=
"Binary Choice"
/>
</PreferenceCategory>
</PreferenceScreen>
The PreferenceCategory
in the XML allows you to subdivide your panel into labeled sections. It is also possible to have more than one PreferenceScreen
if you have a large number of settings and want to divide it into “pages.” Several additional kinds of UI elements can be used in the XML PreferenceScreen
; see the official documentation for details.
The PreferenceActivity
subclass can consist of as little as this onCreate()
method:
@Override
protected
void
onCreate
(
Bundle
savedInstanceState
)
{
super
.
onCreate
(
savedInstanceState
);
addPreferencesFromResource
(
R
.
layout
.
prefs
);
}
When activated, the PreferenceActivity
looks like Figure 10-3.
When the user clicks, say, Name, an Edit dialog opens, as in Figure 10-4.
In the XML layout for the preferences screen, each preference setting is assigned a name or “key,” as in a Java Map
or Properties
object. The supported value types are the obvious String
, int
, float
, and boolean
. You use this to retrieve the user’s values, and you provide a default value in case the settings screen hasn’t been put up yet or in case the user didn’t bother to specify a particular setting:
String
preferredName
=
sharedPreferences
.
getString
(
"nameChoice"
,
"No name"
);
Since the preferences screen does the editing for you, there is little need to set preferences from within your application. There are a few uses, though, such as remembering that the user has accepted an end-user license agreement, or EULA. The code for this would be something like the following:
sharedPreferences.edit().putBoolean("accepted EULA", true).commit();
When writing, don’t forget the commit()
! And, for this particular use case, the EULA option should obviously not appear in the GUI, or the user could just set it there without having a chance to read and ignore the text of your license agreement.
Like many Android apps, this demo has no Back button from its preferences screen; the user simply presses the system’s Back button. When the user returns to the main Activity, a real app would operate based on the user’s choices. My demo app simply displays the values. This is shown in Figure 10-5.
There is no “official” way to add a Done button to an XML PreferenceScreen
,
but some developers use a generic Preference
item:
<Preference
android:title=
"@string/done"
android:key=
"settingsDoneButton"
/>
You can then make this work like a Button
just by giving it an OnClickListener
(from Preference
,
not the normal one from View
):
// Set up our Done preference to function as a Button
Preference
button
=
findPreference
(
"settingsDoneButton"
);
// NOI18N
button
.
setOnPreferenceClickListener
(
new
Preference
.
OnPreferenceClickListener
()
{
@Override
public
boolean
onPreferenceClick
(
Preference
arg0
)
{
finish
();
return
true
;
}
});
When building a full-size application, I like to define the keys used as strings, for stylistic reasons and to prevent misspellings. I create a separate XML resource file for these strings called, say, keys.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string
translatable=
"false"
name=
"key_enable_sync"
>
KEY_ENABLE_SYNC</string>
<string
translatable=
"false"
name=
"key_sync_interval"
>
KEY_SYNC_INTERVAL</string>
<string
translatable=
"false"
name=
"key_username"
>
KEY_USERNAME</string>
<string
translatable=
"false"
name=
"key_password"
>
KEY_PASSWORD</string>
...</resources>
Note the use of translatable="false"
to prevent translation accidents.
I can use these strings directly in the prefs.xml file, and in code using the Activity
method getString()
:
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"> <PreferenceCategory android:title="Synchronization"> <CheckBoxPreference android:key="@string/key_enable_synch" android:title="Enable Sync" android:hint="Sync" /> <EditTextPreference android:key="@string/key_sync_interval" android:title="Sync Interval (minutes)" android:inputType="number" android:defaultValue="60" /> ... </PreferenceScreen>
Basically, that’s all you need: an XML PreferenceScreen
to define the properties and how the user sets them; a call to getDefaultSharedPrefences()
; and calls to getString()
, getBoolean()
, and so on on the returned SharedPreferences
object. It’s easy to handle preferences this way, and it gives the Android system a feel of uniformity, consistency, and predictability that is important to the overall user experience.
Federico Paolinelli
Android provides a very easy way to set up default preferences by defining a Preference
Activity
and providing it a resource file, as discussed in Recipe 10.5. What is not clear is how to perform checks on preferences given by the user.
You can implement the PreferenceActivity
method onSharedPreferenceChanged()
:
public
void
onSharedPreferenceChanged
(
SharedPreferences
prefs
,
String
key
)
You perform your checks in this method’s body. If the check fails you can restore a default value for the preference. Be aware that even though the SharedPreference
s will contain the right value, you won’t see it displayed correctly; for this reason, you need to reload the PreferenceActivity
.
If you have a default PreferenceActivity
that implements On SharedPreferenceChangeListener
, your PreferenceActivity
can implement the on SharedPreferenceChanged()
method, as shown in Example 10-8.
public
class
MyPreferenceActivity
extends
PreferenceActivity
implements
OnSharedPreferenceChangeListener
{
public
void
onCreate
(
Bundle
savedInstanceState
)
{
super
.
onCreate
(
savedInstanceState
);
Context
context
=
getApplicationContext
();
prefs
=
PreferenceManager
.
getDefaultSharedPreferences
(
context
);
addPreferencesFromResource
(
R
.
xml
.
userprefs
);
}
The onSharedPreferenceChanged()
method will be called after the change is committed, so every other change you perform will be permanent.
The idea is to check whether the value is appropriate, and if not replace it with a default value or disable it.
To arrange to have this method called at the appropriate time, you have to register your Activity as a valid listener. A good way to do so is to register in onResume()
and unregister in onPause()
:
@Override
protected
void
onResume
()
{
super
.
onResume
();
prefs
.
registerOnSharedPreferenceChangeListener
(
this
);
}
@Override
protected
void
onPause
()
{
super
.
onPause
();
prefs
.
unregisterOnSharedPreferenceChangeListener
(
this
);
}
Now it’s time to perform the consistency check. For example, if you have an option whose key is MY_OPTION_KEY
, you can use the code in Example 10-9 to check and allow/disallow the value.
public
void
onSharedPreferenceChanged
(
SharedPreferences
prefs
,
String
key
)
{
SharedPreferences
.
Editor
prefEditor
=
prefs
.
edit
();
if
(
key
.
equals
(
MY_OPTION_KEY
))
{
String
optionValue
=
prefs
.
getString
(
MY_OPTION_KEY
,
""
);
if
(
dontLikeTheValue
(
optionValue
))
{
prefEditor
.
putString
(
MY_OPTION_KEY
,
"Default value"
);
prefEditor
.
commit
();
reload
();
}
}
return
;
}
Of course, if the check fails the user will be surprised and will not know why you refused his option. You can then show an error dialog and perform the reload action after the user confirms the dialog (see Example 10-10).
private
void
showErrorDialog
(
String
errorString
)
{
String
okButtonString
=
context
.
getString
(
R
.
string
.
ok_name
);
AlertDialog
.
Builder
ad
=
new
AlertDialog
.
Builder
(
context
);
ad
.
setTitle
(
context
.
getString
(
R
.
string
.
error_name
));
ad
.
setMessage
(
errorString
);
ad
.
setPositiveButton
(
okButtonString
,
new
OnClickListener
()
{
public
void
onClick
(
DialogInterface
dialog
,
int
arg1
)
{
reload
();
}
}
);
ad
.
show
();
return
;
}
In this way, the dontLikeTheValue()
“if” becomes:
if
(
dontLikeTheValue
(
optionValue
))
{
if
(!
GeneralUtils
.
isPhoneNumber
(
smsNumber
))
{
showErrorDialog
(
"I dont like the option"
);
prefEditor
.
putString
(
MY_OPTION_KEY
,
"Default value"
);
prefEditor
.
commit
();
}
}
What’s still missing is the reload()
function, but it’s pretty obvious. It relaunches the Activity using the same Intent that fired it:
private
void
reload
()
{
startActivity
(
getIntent
());
finish
();
}
Rachee Singh
SQLite is used in many platforms, not just Android. While SQLite provides an API, many systems (including Android) develop their own APIs for their particular needs.
To use a SQLite database in an Android application, it is necessary to create a class that inherits from the SQLiteOpenHelper
class, a standard Android class that arranges to open the database file:
public
class
SqlOpenHelper
extends
SQLiteOpenHelper
{
It checks for the existence of the database file and, if it exists, it opens it; otherwise, it creates one.
The constructor for the parent SQLiteOpenHelper
class takes in a few arguments—the context, the database name, the CursorFactory
object (which is most often null
), and the version number of your database schema:
public
static
final
String
DBNAME
=
"tasksdb.sqlite"
;
public
static
final
int
VERSION
=
1
;
public
static
final
String
TABLE_NAME
=
"tasks"
;
public
static
final
String
ID
=
"id"
;
public
static
final
String
NAME
=
"name"
;
public
SqlOpenHelper
(
Context
context
)
{
super
(
context
,
DBNAME
,
null
,
VERSION
);
}
To create a table in SQL, you use the CREATE TABLE
statement. Note that Android’s API for SQLite assumes that your primary key will be a long integer (long
in Java). The primary key column can have any name for now, but when we wrap the data in a ContentProvider
in a later recipe (Recipe 10.15) this column is required to be named _id
, so we’ll start on the right foot by using that name now:
CREATE
TABLE
some_table_name
(
_id
INTEGER
PRIMARY
KEY
AUTOINCREMENT
NOT
NULL
,
name
TEXT
);
The SQLiteOpenHelper
method onCreate()
is called to allow you to create (and possibly populate) the database.
At this point the database file exists, so you can use the passed-in SQLiteDatabase
object to invoke SQL
commands, using its execSql(String)
method:
public
void
onCreate
(
SQLiteDatabase
db
)
{
createDatabase
(
db
);
}
private
void
createDatabase
(
SQLiteDatabase
db
)
{
db
.
execSQL
(
"create table "
+
TABLE_NAME
+
"("
+
ID
+
" integer primary key autoincrement not null, "
+
NAME
+
" text "
+
");"
);
}
The table name is needed to insert or retrieve data, so it is customary to identify it in a final String
field named TABLE
or TABLE_NAME
.
To get a “handle” to write to or read from the SQL database you created, instantiate the SQLiteOpenHelper
subclass, passing the Android Context
(e.g., Activity) into the constructor, then call its getReadableDatabase()
method for read-only access or getWritableDatabase()
for read/write access:
SqlOpenHelper
helper
=
new
SqlOpenHelper
(
this
);
SQLiteDatabase
database
=
helper
.
getWritableDatabase
();
Now, the SQLiteDatabase
database methods can be used to insert and retrieve data. To insert data, we’ll use the SQLiteDatabase
insert()
method and pass an object of type ContentValues
.
ContentValues
is similar to a Map<String,Object>
, a set of key/value pairs. Android does not provide an object-oriented API to the database; you must decompose your object into a ContentValues
. The keys must map to the names of columns in the database. For example, NAME
could be a final string containing the key (the name of the “Name” column), and Mangoes
could be the value. We could pass this to the insert()
method to insert a row in the database with the value Mangoes
in it. SQLite returns the ID for the newly created row in the database (id
):
ContentValues
values
=
new
ContentValues
();
values
.
put
(
NAME
,
"Mangoes"
);
long
id
=
(
database
.
insert
(
TABLE_NAME
,
null
,
values
));
Now we want to retrieve data from the existing database. To query the database, we use the query()
method along with appropriate arguments, most importantly the table name and the column names for which we are extracting values (see Example 10-11). We’ll use the returned Cursor
object to iterate over the database and process the data.
ArrayList<Food> foods = new ArrayList(); Cursor listCursor = database.query(TABLE_NAME, new String [] {ID, NAME}, null, null, null, null, NAME); while (listCursor.moveToNext()) { Long id = listCursor.getLong(0); String name= listCursor.getString(1); Food t = new Food(name); foods.add(t); } listCursor.close();
The moveToNext()
method moves the Cursor
to the next item and returns true
if there is such an item,
rather like the JDBC ResultSet.next()
in standard Java (there is also moveToFirst()
to move back to
the first item in the database, a moveToLast()
method, and so on).
We keep checking until we have reached the end of the database Cursor
. Each item of the database is added to an ArrayList
.
We close the Cursor
to free up resources.
There is considerably more functionality available in the query()
method, whose most common signature is:1
Cursor
query
(
String
tableName
,
String
[]
columns
,
String
selection
,
String
[]
selectionArgs
,
String
groupBy
,
String
having
,
String
orderBy
)
The most important of these are the selection
and orderBy
arguments.
Unlike in standard JDBC, the selection
argument is just the selection part of the SELECT
statement.
However, it does use the ?
syntax for parameter markers, whose values are taken from the following selectionArgs
parameter,
which must be an array of String
s regardless of the underlying column types.
The orderBy
argument is the ORDER BY
part of a standard SQL query, which causes the database to return the results in sorted order
instead of making the application do the sorting.
In both cases, the keywords (SELECT
and ORDER BY
) must be omitted as they will be added by the database code.
Likewise for the SQL keywords GROUP BY
and HAVING
, if you are familiar with those; their values without
the keywords appear as the third-to-last and second-to-last arguments, which are usually null
.
For example, to obtain a list of customers aged 18 or over living in the US state of New York,
sorted by last name, you might use something like this:
Cursor
custCursor
=
db
.
query
(
"customer"
,
"age > ? AND state = ? and COUNTRY = ?"
,
new
String
[]
{
Integer
.
toString
(
minAge
),
"NY"
,
"US"
},
null
,
null
,
"lastname ASC"
);
I’ve written the SQL keywords in uppercase in the query to identify them; this is not required as SQL keywords are not case-sensitive.
Claudio Esperanca
By following these steps, you will be able to create an example Android project with a data layer where you will be able to store and retrieve some data using a SQLite database:
Create a new Android project (AdvancedSearchProject
) targeting a current API level.
Specify AdvancedSearch
as the application name.
Use com.androidcookbook.example.advancedsearch
as the package name.
Create an Activity with the name AdvancedSearchActivity
.
Create a new Java class called DbAdapter
within the package com.androidcookbook.example.advancedsearch
in the src folder.
To create the data layer for the example application, enter the Example 10-12 source code in the created file.
public
class
DbAdapter
{
public
static
final
String
APP_NAME
=
"AdvancedSearch"
;
private
static
final
String
DATABASE_NAME
=
"AdvancedSearch_db"
;
private
static
final
int
DATABASE_VERSION
=
1
;
// Our internal database version (e.g., to control upgrades)
private
static
final
String
TABLE_NAME
=
"example_tbl"
;
public
static
final
String
KEY_USERNAME
=
"username"
;
public
static
final
String
KEY_FULLNAME
=
"fullname"
;
public
static
final
String
KEY_EMAIL
=
"email"
;
public
static
long
GENERIC_ERROR
=
-
1
;
public
static
long
GENERIC_NO_RESULTS
=
-
2
;
public
static
long
ROW_INSERT_FAILED
=
-
3
;
private
final
Context
context
;
private
DbHelper
dbHelper
;
private
SQLiteDatabase
sqlDatabase
;
public
DbAdapter
(
Context
context
)
{
this
.
context
=
context
;
}
private
static
class
DbHelper
extends
SQLiteOpenHelper
{
private
boolean
databaseCreated
=
false
;
DbHelper
(
Context
context
)
{
super
(
context
,
DATABASE_NAME
,
null
,
DATABASE_VERSION
);
}
@Override
public
void
onCreate
(
SQLiteDatabase
db
)
{
Log
.
d
(
APP_NAME
,
"Creating the application database"
);
try
{
// Create the FTS3 virtual table
db
.
execSQL
(
"CREATE VIRTUAL TABLE ["
+
TABLE_NAME
+
"] USING FTS3 ("
+
"["
+
KEY_USERNAME
+
"] TEXT,"
+
"["
+
KEY_FULLNAME
+
"] TEXT,"
+
"["
+
KEY_EMAIL
+
"] TEXT"
+
");"
);
this
.
databaseCreated
=
true
;
}
catch
(
Exception
e
)
{
Log
.
e
(
APP_NAME
,
"An error occurred while creating the database: "
+
e
.
toString
(),
e
);
this
.
deleteDatabaseStructure
(
db
);
}
}
public
boolean
databaseCreated
()
{
return
this
.
databaseCreated
;
}
private
boolean
deleteDatabaseStructure
(
SQLiteDatabase
db
)
{
try
{
db
.
execSQL
(
"DROP TABLE IF EXISTS ["
+
TABLE_NAME
+
"];"
);
return
true
;
}
catch
(
Exception
e
)
{
Log
.
e
(
APP_NAME
,
"An error occurred while deleting the database: "
+
e
.
toString
(),
e
);
}
return
false
;
}
}
/**
* Open the database; if the database can't be opened, try to create it
*
* @return {@link Boolean} true if database opened/created OK, false otherwise
* @throws {@link SQLException] if an error occurred
*/
public
boolean
open
()
throws
SQLException
{
try
{
this
.
dbHelper
=
new
DbHelper
(
this
.
context
);
this
.
sqlDatabase
=
this
.
dbHelper
.
getWritableDatabase
();
return
this
.
sqlDatabase
.
isOpen
();
}
catch
(
SQLException
e
)
{
throw
e
;
}
}
/**
* Close the database connection
* @return {@link Boolean} true if the connection was terminated, false otherwise
*/
public
boolean
close
()
{
this
.
dbHelper
.
close
();
return
!
this
.
sqlDatabase
.
isOpen
();
}
/**
* Check if the database was opened
*
* @return {@link Boolean} true if it was, false otherwise
*/
public
boolean
isOpen
()
{
return
this
.
sqlDatabase
.
isOpen
();
}
/**
* Check if the database was created
*
* @return {@link Boolean} true if it was, false otherwise
*/
public
boolean
databaseCreated
()
{
return
this
.
dbHelper
.
databaseCreated
();
}
/**
* Insert a new row i3n the table
*
* @param username {@link String} with the username
* @param fullname {@link String} with the fullname
* @param email {@link String} with the email
* @return {@link Long} with the row ID or ROW_INSERT_FAILED (value < 0) on error
*/
public
long
insertRow
(
String
username
,
String
fullname
,
String
)
{
try
{
// Prepare the values
ContentValues
values
=
new
ContentValues
();
values
.
put
(
KEY_USERNAME
,
username
);
values
.
put
(
KEY_FULLNAME
,
fullname
);
values
.
put
(
KEY_EMAIL
,
);
// Try to insert the row
return
this
.
sqlDatabase
.
insert
(
TABLE_NAME
,
null
,
values
);
}
catch
(
Exception
e
)
{
Log
.
e
(
APP_NAME
,
"An error occurred while inserting the row: "
+
e
.
toString
(),
e
);
}
return
ROW_INSERT_FAILED
;
}
/**
* The search() method uses the FTS3 virtual table and
* the MATCH function from SQLite to search for data.
* @see http://www.sqlite.org/fts3.html to know more about the syntax.
* @param search {@link String} with the search expression
* @return {@link LinkedList} with the {@link String} search results
*/
public
LinkedList
<
String
>
search
(
String
search
)
{
LinkedList
<
String
>
results
=
new
LinkedList
<
String
>();
Cursor
cursor
=
null
;
try
{
cursor
=
this
.
sqlDatabase
.
query
(
true
,
TABLE_NAME
,
new
String
[]
{
KEY_USERNAME
,
KEY_FULLNAME
,
KEY_EMAIL
},
TABLE_NAME
+
" MATCH ?"
,
new
String
[]
{
search
},
null
,
null
,
null
,
null
);
if
(
cursor
!=
null
&&
cursor
.
getCount
()>
0
&&
cursor
.
moveToFirst
())
{
int
iUsername
=
cursor
.
getColumnIndex
(
KEY_USERNAME
);
int
iFullname
=
cursor
.
getColumnIndex
(
KEY_FULLNAME
);
int
iEmail
=
cursor
.
getColumnIndex
(
KEY_EMAIL
);
do
{
results
.
add
(
new
String
(
"Username: "
+
cursor
.
getString
(
iUsername
)
+
", Fullname: "
+
cursor
.
getString
(
iFullname
)
+
", Email: "
+
cursor
.
getString
(
iEmail
)
)
);
}
while
(
cursor
.
moveToNext
());
}
}
catch
(
Exception
e
)
{
Log
.
e
(
APP_NAME
,
"An error occurred while searching for "
+
search
+
": "
+
e
.
toString
(),
e
);
}
finally
{
if
(
cursor
!=
null
&&
!
cursor
.
isClosed
())
{
cursor
.
close
();
}
}
return
results
;
}
}
Now that the data layer is usable, the AdvancedSearchActivity
can be used to test it.
To define the application strings, replace the contents of the res/values/strings.xml file with the following:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string
name=
"label_search"
>
Search</string>
<string
name=
"app_name"
>
AdvancedSearch</string>
</resources>
The application layout can be set within the file res/layout/main.xml. This contains the expected EditText
(named etSearch
), a Button
(named btnSearch
), and a TextView
(named tvResults
) to display the results, all in a LinearLayout
.
Finally, Example 10-13 shows the AdvancedSearchActivity.java code.
public
class
AdvancedSearchActivity
extends
Activity
{
private
DbAdapter
dbAdapter
;
@Override
public
void
onCreate
(
Bundle
savedInstanceState
)
{
super
.
onCreate
(
savedInstanceState
);
setContentView
(
R
.
layout
.
main
);
dbAdapter
=
new
DbAdapter
(
this
);
dbAdapter
.
open
();
if
(
dbAdapter
.
databaseCreated
())
{
dbAdapter
.
insertRow
(
"test"
,
"test example"
,
"[email protected]"
);
dbAdapter
.
insertRow
(
"lorem"
,
"lorem ipsum"
,
"[email protected]"
);
dbAdapter
.
insertRow
(
"jdoe"
,
"Jonh Doe"
,
"[email protected]"
);
}
Button
button
=
(
Button
)
findViewById
(
R
.
id
.
btnSearch
);
final
EditText
etSearch
=
(
EditText
)
findViewById
(
R
.
id
.
etSearch
);
final
TextView
tvResults
=
(
TextView
)
findViewById
(
R
.
id
.
tvResults
);
button
.
setOnClickListener
(
new
View
.
OnClickListener
()
{
public
void
onClick
(
View
v
)
{
LinkedList
<
String
>
results
=
dbAdapter
.
search
(
etSearch
.
getText
().
toString
());
if
(
results
.
isEmpty
())
{
tvResults
.
setText
(
"No results found"
);
}
else
{
Iterator
<
String
>
i
=
results
.
iterator
();
tvResults
.
setText
(
""
);
while
(
i
.
hasNext
())
{
tvResults
.
setText
(
tvResults
.
getText
()+
i
.
next
()+
" "
);
}
}
}
});
}
@Override
protected
void
onDestroy
()
{
dbAdapter
.
close
();
super
.
onDestroy
();
}
}
See the SQLite website to learn more about the Full-Text Search 3 extension module’s capabilities, including the search syntax, and localizeandroid to learn about a project with an implementation of this search mechanism.
Jonathan Fuerth
This recipe demonstrates the advantages of using SQLite timestamps over storing raw millisecond values in your database, and shows how to retrieve those timestamps from your database as java.util.Date
objects.
The usual representation for an absolute timestamp in Unix is time_t
, which historically was just an alias for a 32-bit integer. This integer represented the date as the number of seconds elapsed since UTC 00:00 on January 1, 1970 (the Unix epoch). On systems where time_t
is still a 32-bit integer, the clock will roll over partway through the year 2038.
Java adopted a similar convention, but with a few twists. The epoch remains the same, but the count is always stored in a 64-bit signed integer (the native Java long
type) and the units are milliseconds rather than seconds. This method of timekeeping will not roll over for another 292 million years.
Android example code that deals with persisting dates and times tends to simply store and retrieve the raw milliseconds since the epoch values in the database. However, by doing this, it misses out on some useful features built into SQLite.
There are several advantages to storing proper SQLite timestamps in your data: you can default timestamp columns to the current time using no Java code at all; you can perform calendar-sensitive arithmetic such as selecting the first day of a week or month, or adding a week to the value stored in the database; and you can extract just the date or time components and return those from your data provider.
All of these code-saving advantages come with two added bonuses: first, your data provider’s API can stick to the Android convention of passing timestamps around as long
values; second, all of this date manipulation is done in the natively compiled SQLite code, so the manipulations don’t incur the garbage collection overhead of creating multiple java.util.Date
or java.util.Calendar
objects.
Without further ado, here’s how to do it.
First, create a table that defines a column of type timestamp
:
CREATE
TABLE
current_list
(
item_id
INTEGER
NOT
NULL
,
added_on
TIMESTAMP
NOT
NULL
DEFAULT
current_timestamp
,
added_by
VARCHAR
(
50
)
NOT
NULL
,
quantity
INTEGER
NOT
NULL
,
units
VARCHAR
(
50
)
NOT
NULL
,
CONSTRAINT
current_list_pk
PRIMARY
KEY
(
item_id
)
);
Note the default value for the added_on
column. Whenever you insert a row into this table, SQLite will automatically fill in the current time, accurate to the second, for the new record (this is shown here using the command-line SQLite program running on a desktop; we’ll see later in this recipe how to get these into a database under Android):
sqlite> insert into current_list (item_id, added_by, quantity, units) ...> values (1, 'fuerth', 1, 'EA'); sqlite> select * from current_list where item_id = 1; 1|2020-05-14 23:10:26|fuerth|1|EA sqlite>
See how the current date was inserted automatically? This is one of the advantages you get from working with SQLite timestamps.
How about the other advantages?
Select just the date part, forcing the time back to midnight:
sqlite> select item_id, date(added_on,'start of day') ...> from current_list where item_id = 1; 1|2020-05-14 sqlite>
Or adjust the date to the Monday of the following week:
sqlite> select item_id, date(added_on,'weekday 1') ...> from current_list where item_id = 1; 1|2020-05-17 sqlite>
Or the Monday before:
sqlite> select item_id, date(added_on,'weekday 1','-7 days') ...> from current_list where item_id = 1; 1|2020-05-10 sqlite>
These examples are just the tip of the iceberg. You can do a lot of useful things with your timestamps once SQLite recognizes them as such.
Last, but not least, you must be wondering how to get these dates back into your Java code. The trick is to press another of SQLite’s date functions into service—this time strftime()
. Here is a Java method that fetches a row from the current_list
table we’ve been working with:
Cursor
cursor
=
database
.
rawQuery
(
"SELECT item_id AS _id,"
+
" (strftime('%s', added_on) * 1000) AS added_on,"
+
" added_by, quantity, units"
+
" FROM current_list"
,
new
String
[
0
]);
long
millis
=
cursor
.
getLong
(
cursor
.
getColumnIndexOrThrow
(
"added_on"
));
Date
addedOn
=
new
Date
(
millis
);
That’s it: using strftime()
’s %s
format, you can select timestamps directly into your Cursor
as Java milliseconds since the epoch values. Client code will be none the wiser, except that your content provider will be able to do date manipulations for free that would otherwise take significant amounts of Java code and extra object allocations.
Ian Darwin
It is common to have data in a form other than a Cursor
, but to want to present it as a Cursor
for use in a ListView
with an Adapter
or a CursorLoader
.
The AbstractCursor
class facilitates this. While Cursor
is an interface that you could implement directly, there are a number of routines therein that are pretty much the same in every implementation of Cursor
, so they have been abstracted out and made into the AbstractCursor
class.
In this short example we expose a list of filenames with the following structure:
_id
is the sequence number.
filename
is the full path.
type
is the filename extension.
This list of files is hardcoded to simplify the demo.
We will expose this as a Cursor
and consume it in a SimpleCursorAdapter
.
First, the start of the DataToCursor
class:
/**
* Provide a Cursor from a fixed list of data
* column 1 - _id
* column 2 - filename
* column 3 - file type
*/
public
class
DataToCursor
extends
AbstractCursor
{
private
static
final
String
[]
COLUMN_NAMES
=
{
"_id"
,
"filename"
,
"type"
};
private
static
final
String
[]
DATA_ROWS
=
{
"one.mpg"
,
"two.jpg"
,
"tre.dat"
,
"fou.git"
,
};
As you can see, there are two arrays: one for the column names going across,
and one for the rows going down. In this simple example we don’t have to track
the ID values (since they are the same as the index into the DATA_ROWS
array) or the file types (since they are the same as the filename extension).
There are a few structural methods that are needed:
@Override
public
int
getCount
()
{
return
DATA
.
length
;
}
@Override
public
int
getColumnCount
()
{
return
COLUMN_NAMES
.
length
;
}
@Override
public
String
[]
getColumnNames
()
{
return
COLUMN_NAMES
;
}
The getColumnCount()
method’s value is obviously derivable from the array, but since it’s constant,
we override the method for efficiency reasons—probably not
necessary in most applications.
Then there are some necessary get
methods, notably getType()
for getting the type
of a given column (whether it’s numeric, string, etc.):
@Override
public
int
getType
(
int
column
)
{
switch
(
column
)
{
case
0
:
return
Cursor
.
FIELD_TYPE_INTEGER
;
case
1
:
case
2
:
return
Cursor
.
FIELD_TYPE_STRING
;
default
:
throw
new
IllegalArgumentException
(
Integer
.
toString
(
column
));
}
}
The next methods have to do with getting the value of a given column in the current row.
Nicely, the AbstractCursor
handles all the moveToRow()
and related methods,
so we just have to call the inherited (and protected) method getPosition()
:
/**
* Return the _id value (the only integer-valued column).
* Conveniently, rows and array indices are both 0-based.
*/
@Override
public
int
getInt
(
int
column
)
{
int
row
=
getPosition
();
switch
(
column
)
{
case
0
:
return
row
;
default
:
throw
new
IllegalArgumentException
(
Integer
.
toString
(
column
));
}
}
/** SQLite _ids are actually long, so make this work as well.
* This direct equivalence is usually not applicable; do not blindly copy.
*/
@Override
public
long
getLong
(
int
column
)
{
return
getInt
(
column
);
}
@Override
public
String
getString
(
int
column
)
{
int
row
=
getPosition
();
switch
(
column
)
{
case
1
:
return
DATA_ROWS
[
row
];
case
2
:
return
extension
(
DATA_ROWS
[
row
]);
default
:
throw
new
IllegalArgumentException
(
Integer
.
toString
(
column
));
}
}
The remaining methods aren’t interesting; methods like getFloat()
, getBlob()
, and
so on merely throw exceptions as, in this example, there are no columns of those types.
The main Activity shows nothing different than the other ListView
examples in Chapter 8: the data from the Cursor
is loaded into a ListView
using
a SimpleCursorAdapter
(this overload of which is deprecated, but works fine for this example).
The result is shown in Figure 10-6.
We have successfully shown this proof-of-concept of generating a Cursor
without using SQLite.
It would obviously be straightforward to turn this into a more dynamic file-listing utility,
even a file manager. You’d want to use a CursorLoader
instead of a SimpleCursorAdapter
to make it complete, though;
CursorLoader
is covered in the next recipe.
Ian Darwin
Use a CursorLoader
.
Loader
is a top-level class that provides a basic capability for loading almost any type of data.
AsyncTaskLoader<T>
provides a specialization of Loader
to handle threading.
Its important subclass is CursorLoader
, which is used to load data
from a database Cursor
and, typically in conjunction with a Fragment or Activity, to display it.
The Android documentation for AsyncTaskLoader<T>
shows a comprehensive example of loading the list of all
applications installed on a device, and keeping that information up-to-date as it changes.
We will start with something a bit simpler, which reads (as CursorLoader
classes are expected to)
a list of data from a ContentProvider
implementation, and displays it in a list.
To keep the example simple, we’ll use the preinstalled Browser Bookmarks content provider. (Note that this content provider is not available in Android 7 or later).
The application will provide a list of installed bookmarks, similar to that shown in Figure 10-7.
To use the Loader
, you have to provide an implementation of the LoaderManager.LoaderCallbacks<T>
interface,
where T
is the type you want to load from; in our case, Cursor
.
While most examples have the Activity or Fragment directly implement this, we’ll make it an inner class
just to isolate it, and make more of the argument types explicit.
We start off in onCreate()
or onResume()
by creating a SimpleCursorAdapter
.
The constructor for this adapter—the mainstay of many a list-based application in prior releases of Android—is now deprecated, but calling it with an additional flags
argument (the final 0
argument here)
for use with the Loader
family is not deprecated.
Other than the final flags
arg, and passing null
for the Cursor
, the code is largely the same as
our non-Loader
-based example, ContentProviderBookmarks, which has been left in the Android Cookbook repository to show “the old way.”
With the extra flags
argument the Cursor
may be null
, as we will provide it later,
in the LoaderCallbacks
code (Example 10-14):
@Override
protected
void
onCreate
(
Bundle
savedInstanceState
)
{
super
.
onCreate
(
savedInstanceState
);
setContentView
(
R
.
layout
.
activity_main
);
String
[]
fromFields
=
new
String
[]
{
Browser
.
BookmarkColumns
.
TITLE
,
Browser
.
BookmarkColumns
.
URL
};
int
[]
toViews
=
new
int
[]
{
android
.
R
.
id
.
text1
,
android
.
R
.
id
.
text2
};
mAdapter
=
new
SimpleCursorAdapter
(
this
,
android
.
R
.
layout
.
simple_list_item_2
,
null
,
fromFields
,
toViews
,
0
);
setListAdapter
(
mAdapter
);
// Prepare the loader: reconnects an existing one or reuses one
getLoaderManager
().
initLoader
(
0
,
null
,
new
MyCallbacks
(
this
));
}
Note also the last line, which will find a Loader
instance and associate our callbacks with it.
The callbacks are where the actual work gets done. There are three required methods in the LoaderCallbacks
object:
onCreateLoader()
Called to create an instance of the actual loader; the framework will start it, which will perform a ContentProvider
query
onLoadFinished()
Called when the loader has finished loading its data, to set its cursor to the one from the query
onLoaderReset()
Called when the loader is done, to disassociate it from the view (by setting its cursor back to null
)
Our implementation is fairly simple. As we want all the columns and don’t care about the order, we
only need to provide the bookmarks Uri
, which is predefined for us (see Example 10-14).
class
MyCallbacks
implements
LoaderCallbacks
<
Cursor
>
{
Context
context
;
public
MyCallbacks
(
Activity
context
)
{
this
.
context
=
context
;
}
@Override
public
Loader
<
Cursor
>
onCreateLoader
(
int
id
,
Bundle
stuff
)
{
Log
.
d
(
TAG
,
"MainActivity.onCreateLoader()"
);
return
new
CursorLoader
(
context
,
// Normal CP query: url, proj, select, where, having
Browser
.
BOOKMARKS_URI
,
null
,
null
,
null
,
null
);
}
@Override
public
void
onLoadFinished
(
Loader
<
Cursor
>
loader
,
Cursor
data
)
{
// Load has finished, swap the loaded cursor into the view
mAdapter
.
swapCursor
(
data
);
}
@Override
public
void
onLoaderReset
(
Loader
<
Cursor
>
loader
)
{
// The end of time: set cursor to null to prevent bad ending
mAdapter
.
swapCursor
(
null
);
}
}
The Loader
family is quite sophisticated; it lets you load a Cursor
in the background then
adapt it to the GUI. The
AsyncTaskLoader<T>
, as the name implies, uses an AsyncTask
(see Recipe 4.10) to do the data loading
on a background thread and runs the updating on the UI thread.
Its subclass, the CursorLoader
, is now the tool of choice for loading lists of data.
The source code for this example is in the Android Cookbook repository, in the subdirectory CursorLoaderDemo (see “Getting and Using the Code Examples”).
Rachee Singh
While there are a couple of dozen Java APIs for JSON listed on the JSON website, we’ll
use the built-in JSONObject
class to parse JSON and retrieve the data values contained in it.
For this recipe, we will use a method to generate JSON code. In a real application you would likely obtain the JSON data from some web service. In this method we make use of a JSONObject
class object to put in values and then to return the corresponding string (using the toString()
method). Creating an object of type JSONObject
can throw a JSONException
, so we enclose the code in a try-catch
block (see Example 10-15).
private
String
getJsonString
()
{
JSONObject
string
=
new
JSONObject
();
try
{
string
.
put
(
"name"
,
"John Doe"
);
string
.
put
(
"age"
,
new
Integer
(
25
));
string
.
put
(
"address"
,
"75 Ninth Avenue, New York, NY 10011"
);
string
.
put
(
"phone"
,
"8367667829"
);
}
catch
(
JSONException
e
)
{
e
.
printStackTrace
();
}
return
string
.
toString
();
}
We need to instantiate an object of class JSONObject
that takes the JSON string as an argument. In this case, the JSON string is being obtained from the getJsonString()
method. We extract the information from the JSONObject
and print it in a TextView
(see Example 10-16).
try
{
String
jsonString
=
getJsonString
();
JSONObject
jsonObject
=
new
JSONObject
(
jsonString
);
String
name
=
jsonObject
.
getString
(
"name"
);
String
age
=
jsonObject
.
getString
(
"age"
);
String
address
=
jsonObject
.
getString
(
"address"
);
String
phone
=
jsonObject
.
getString
(
"phone"
);
String
jsonText
=
name
+
" "
+
age
+
" "
+
address
+
" "
+
phone
;
json
=
(
TextView
)
findViewById
(
R
.
id
.
json
);
json
.
setText
(
jsonText
);
}
catch
(
JSONException
e
)
{
// Display the exception...
}
For more information on JavaScript Object Notation, see the JSON website.
There are about two dozen JSON APIs for Java alone. One of the more powerful ones—which includes a data-binding package that will automatically convert between Java objects and JSON—is JackSON.
The source code for this project is in the Android Cookbook repository, in the subdirectory JSONParsing (see “Getting and Using the Code Examples”).
Ian Darwin
Example 10-17
is the code that parses the XML document containing the list of recipes in this book, as discussed in Recipe 12.1. The input file has a single recipes
root element, followed by a sequence of recipe
elements, each with an id
and a title
with textual content.
The code creates a DOM DocumentBuilderFactory
, which can be tailored, for example, to make schema-aware parsers. In real code you could create this in a static initializer instead of re-creating it each time. The DocumentBuilderFactory
is used to create a document builder, a.k.a. parser. The parser expects to be reading from an InputStream
, so we convert the data that we have in string form into an array of bytes and construct a ByteArrayInputStream
. Again, in real life you would probably want to combine this code with the web service consumer so that you could simply get the input stream from the network connection and read the XML directly into the parser, instead of saving it as a string and then wrapping that in a converter as we do here.
Once the elements are parsed, we convert the document into an array of data (the singular of data is datum, so the class is called Datum
) by calling tDOM API methods such as getDocumentElement()
, getChildNodes()
, and getNodeValue()
. Since the DOM API was not invented by Java people, it doesn’t use the standard Collections API but has its own collections, like NodeList
. In DOM’s defense, the same or similar APIs are used in a really wide variety of programming languages, so it can be said to be as much a standard as Java’s Collections.
Example 10-17 shows the code.
/** Convert the list of Recipes in the String result from the
* web service into an ArrayList of Datum objects.
* @throws ParserConfigurationException
* @throws IOException
* @throws SAXException
*/
public
static
ArrayList
<
Datum
>
parse
(
String
input
)
throws
Exception
{
final
ArrayList
<
Datum
>
results
=
new
ArrayList
<
Datum
>(
1000
);
final
DocumentBuilderFactory
dbFactory
=
DocumentBuilderFactory
.
newInstance
();
final
DocumentBuilder
parser
=
dbFactory
.
newDocumentBuilder
();
final
Document
document
=
parser
.
parse
(
new
ByteArrayInputStream
(
input
.
getBytes
()));
Element
root
=
document
.
getDocumentElement
();
NodeList
recipesList
=
root
.
getChildNodes
();
for
(
int
i
=
0
;
i
<
recipesList
.
getLength
();
i
++)
{
Node
recipe
=
recipesList
.
item
(
i
);
NodeList
fields
=
recipe
.
getChildNodes
();
String
id
=
((
Element
)
fields
.
item
(
0
)).
getNodeValue
();
String
title
=
((
Element
)
fields
.
item
(
1
)).
getNodeValue
();
Datum
d
=
new
Datum
(
Integer
.
parseInt
(
id
),
title
);
results
.
add
(
d
);
}
return
results
;
}
In converting this code from Java SE to Android, the only change we had to make was to use getNodeValue()
in the retrieval of id
and title
instead of Java SE’s getTextContent()
, so the API really is very close.
The web service is discussed in Recipe 12.1. There is much more in the XML chapter of my Java Cookbook (O’Reilly).
Should you wish to process XML in a streaming mode, you can use the XMLPullParser, documented in the online version of this Cookbook.
Ian Darwin
One way is to create a Content
Uri
using constants provided by the ContentProvider
and use Activity.getContentResolver().query()
, which returns a SQLite Cursor
object.
Another way, useful if you want to select one record such as a single contact, is to create a PICK
Uri
, open it in an Intent using startActivityForResult()
, extract the URI from the returned Intent, then perform the query as just described.
This is part of the contact selection code from TabbyText, my SMS-over-WiFi text message sender (the rest of its code is in Recipe 10.17).
First, the main program sets up an OnClickListener
to use the Contacts app as a chooser, from a Find Contact button:
b
.
setOnClickListener
(
new
View
.
OnClickListener
()
{
@Override
public
void
onClick
(
View
arg0
)
{
Uri
uri
=
ContactsContract
.
Contacts
.
CONTENT_URI
;
System
.
out
.
println
(
uri
);
Intent
intent
=
new
Intent
(
Intent
.
ACTION_PICK
,
uri
);
startActivityForResult
(
intent
,
REQ_GET_CONTACT
);
}
});
The URI is predefined for us; it actually has the value content://com.android.contacts/contacts
. The constant REQ_GET_CONTACT
is arbitrary; it’s just there to associate this Intent start-up with the handler code, since more complex apps will often start more than one Intent and they need to handle the results differently. Once this button is pressed, control passes from our app out to the Contacts app. The user can then select a contact she wishes to send an SMS message to. The Contacts app then is backgrounded and control returns to our app at the onActivityResult()
method, to indicate that the Activity we started has completed and delivered a result.
The next bit of code shows how the onActivityResult()
method converts the response from the Activity into a SQLite cursor (see Example 10-18).
@Override
protected
void
onActivityResult
(
int
requestCode
,
int
resultCode
,
Intent
data
)
{
if
(
requestCode
==
REQ_GET_CONTACT
)
{
switch
(
resultCode
)
{
case
Activity
.
RESULT_OK
:
// The Contacts API is about the most complex to use.
// First retrieve the Contact, as we only get its URI from the Intent.
Uri
resultUri
=
data
.
getData
();
// E.g., content://contacts/people/123
Cursor
cont
=
getContentResolver
().
query
(
resultUri
,
null
,
null
,
null
,
null
);
if
(!
cont
.
moveToNext
())
{
// Expect exactly one row
Toast
.
makeText
(
this
,
"Cursor has no data"
,
Toast
.
LENGTH_LONG
).
show
();
return
;
}
...
There are a few key things to note here. First, make sure the request Code
is the one you started, and the resultCode
is RESULT_OK
or RESULT_CANCELED
(if not, pop up a warning dialog). Then, extract the URL for the response you picked—the Intent data from the returned Intent—and use that to create a query, using the inherited Activity
method getContentResolver()
to get the ContentResolver
and its query()
method to make up a SQLite cursor.
We expect the user to have selected one contact, so if that’s not the case we error out. Otherwise, we’d go ahead and use the SQLite cursor to read the data. The exact formatting of the Contacts database is a bit out of scope for this recipe, so it’s been deferred to Recipe 10.17.
To insert data, we need to create a ContentValues
object and populate it with the fields.
Once that’s done, we use the ContentContracts
-provided base Uri
value, along with the
ContentValues
, to insert the values, somewhat like the following:
mNewUri
=
getContentResolver
().
insert
(
ContactsContract
.
Contacts
.
CONTENT_URI
,
contentValues
);
You could then put the mNewUri
into an Intent and display it.
This is shown in more detail in Recipe 10.16.
Ashwini Shahapurkar, Ian Darwin
Write a ContentProvider
that will allow other applications to access data contained in your app.
ContentProvider
s allow other applications to access the data generated by your app. A custom ContentProvider
is effectively a wrapper around your existing
application data (typically but not necessarily contained in a SQLite database; see Recipe 10.7).
Remember that just because you can do so doesn’t mean that you should; in particular,
you generally do not need to write a ContentProvider
just to access your data from within your application.
To make other apps aware that a ContentProvider
is available, we need to declare it in AndroidManifest.xml as follows:
<application
...
>
<activity
...
/>
<provider
android:authorities=
"com.example.contentprovidersample"
android:name=
"MyContentProvider"
/>
</application>
The android:authorities
attribute is a string used throughout the system to identify your ContentProvider
; it should also be
declared in a public static final String
variable in your provider class.
The android:name
attribute refers to the class MyContentProvider
, which extends the ContentProvider
class. We need to override the following methods in this class:
onCreate
();
getType
(
Uri
);
insert
(
Uri
,
ContentValues
);
query
(
Uri
,
String
[],
String
,
String
[],
String
);
update
(
Uri
,
ContentValues
,
String
,
String
[]);
delete
(
Uri
,
String
,
String
[]);
The onCreate()
method is for setup, as in any other Android component. Our example just creates a SQLite database in a field:
mDatabase
=
new
MyDatabaseHelper
(
this
);
The getType()
method assigns MIME types to incoming Uri
values.
This method will typically use a statically allocated UriMatcher
to determine whether the incoming Uri
refers to a list of values (does not end with a numeric ID) or a single value (ends with a /
and a numeric ID,
indicated by “/#” in the pattern argument).
The method must return either ContentResolver.CURSOR_ITEM_BASE_TYPE + "/" + MIME_VND_TYPE
for a single item or
ContentResolver.CURSOR_DIR_BASE_TYPE + "/" + MIME_VND_TYPE
for a multiplicity, where MIME_VND_TYPE
is an application-specific MIME type string; in our example that’s “vnd.example.item”.
It must begin with “vnd,” which stands, throughout this paragraph, for “Vendor,” as these values
are not provided by the official MIME type committee but by Android.
The UriMatcher
is also used in the four data methods shown next to sort out singular from plural requests.
Example 10-19 contains the declarations and code for the matcher and the getType()
method.
public
class
MyContentProvider
extends
ContentProvider
{
/** The authority name. MUST be as listed in
* <provider android:authorities=...> in AndroidManifest.xml
*/
public
static
final
String
AUTHORITY
=
"com.example.contentprovidersample"
;
public
static
final
String
MIME_VND_TYPE
=
"vnd.example.item"
;
private
static
final
UriMatcher
matcher
=
new
UriMatcher
(
UriMatcher
.
NO_MATCH
);
private
static
final
int
ITEM
=
1
;
private
static
final
int
ITEMS
=
2
;
static
{
matcher
.
addURI
(
AUTHORITY
,
"items/#"
,
ITEM
);
matcher
.
addURI
(
AUTHORITY
,
"items"
,
ITEMS
);
}
@Override
public
String
getType
(
Uri
uri
)
{
int
matchType
=
matcher
.
match
(
uri
);
Log
.
d
(
"ReadingsContentProvider.getType()"
,
uri
+
" --> "
+
matchType
);
switch
(
matchType
)
{
case
ITEM:
return
ContentResolver
.
CURSOR_ITEM_BASE_TYPE
+
"/"
+
MIME_VND_TYPE
;
case
ITEMS:
return
ContentResolver
.
CURSOR_DIR_BASE_TYPE
+
"/"
+
MIME_VND_TYPE
;
default
:
throw
new
IllegalArgumentException
(
"Unknown or Invalid URI "
+
uri
);
}
}
The last four methods are usually wrapper functions for SQL queries on the SQLite database;
note that they have the same parameter lists as the like-named SQLite methods, with the
insertion of a Uri
at the front of the parameter list.
These methods typically parse the input parameters, do some error checking,
and forward the operation on to the SQLite database, as shown in Example 10-20.
/** The C of CRUD: insert() */
@Override
public
Uri
insert
(
Uri
uri
,
ContentValues
values
)
{
Log
.
d
(
Constants
.
TAG
,
"MyContentProvider.insert()"
);
switch
(
matcher
.
match
(
uri
))
{
case
ITEM:
// Fail
throw
new
RuntimeException
(
"Cannot specify ID when inserting"
);
case
ITEMS:
// OK
break
;
default
:
throw
new
IllegalArgumentException
(
"Did not recognize URI "
+
uri
);
}
long
id
=
mDatabase
.
getWritableDatabase
().
insert
(
TABLE_NAME
,
null
,
values
);
uri
=
Uri
.
withAppendedPath
(
uri
,
"/"
+
id
);
getContext
().
getContentResolver
().
notifyChange
(
uri
,
null
);
return
uri
;
}
/** The R of CRUD: query() */
@Override
public
Cursor
query
(
Uri
uri
,
String
[]
projection
,
String
selection
,
String
[]
selectionArgs
,
String
sortOrder
)
{
Log
.
d
(
Constants
.
TAG
,
"MyContentProvider.query()"
);
switch
(
matcher
.
match
(
uri
))
{
case
ITEM:
// OK
selection
=
"_id = ?"
;
selectionArgs
=
new
String
[]{
Long
.
toString
(
ContentUris
.
parseId
(
uri
))
};
break
;
case
ITEMS:
// OK
break
;
default
:
throw
new
IllegalArgumentException
(
"Did not recognize URI "
+
uri
);
}
// Build the query with SQLiteQueryBuilder
SQLiteQueryBuilder
qBuilder
=
new
SQLiteQueryBuilder
();
qBuilder
.
setTables
(
TABLE_NAME
);
// Query the database and get result in cursor
final
SQLiteDatabase
db
=
mDatabase
.
getWritableDatabase
();
Cursor
resultCursor
=
qBuilder
.
query
(
db
,
projection
,
selection
,
selectionArgs
,
null
,
null
,
sortOrder
,
null
);
resultCursor
.
setNotificationUri
(
getContext
().
getContentResolver
(),
uri
);
return
resultCursor
;
}
}
The remaining methods, update()
and delete()
, are parallel in structure and so have been omitted from this book to save space,
but the online example is fully fleshed out.
Providing a ContentProvider
lets you expose your data without giving other developers direct access to your database and also reduces the chances of database inconsistency.
The source code for this recipe is in the Android Cookbook repository, in the subdirectory ContentProviderSample (see “Getting and Using the Code Examples”).
Ian Darwin
The Contacts
database is, to be sure, “flexible.” It has to adapt to many different kinds of accounts and contact management uses, with different types of data. And it is, as a result, somewhat complicated.
The classes named Contacts
(and, by extension, all their inner classes and interfaces) are deprecated, meaning “don’t use them in new development.” The classes and interfaces that take their place have names beginning with the somewhat cumbersome and tongue-twisting ContactsContract
.
We’ll start with the simplest case of adding a person’s contact information. We want to insert the following information, which we either got from the user or found on the network someplace:
Name |
Jon Smith |
Home Phone |
416-555-5555 |
Work Phone |
416-555-6666 |
First we have to determine which Android account to associate the data with. For now we will use a fake account name (darwinian is both an adjective and my name, so we’ll use that).
For each of the four fields, we’ll need to create an account operation.
We add all five operations to a List
, and pass that into getContentResolver().applyBatch()
.
Example 10-21 shows the code for the addContact()
method.
private
void
addContact
()
{
final
String
ACCOUNT_NAME
=
"darwinian"
String
name
=
"Jon Smith"
;
String
homePhone
=
"416-555-5555"
;
String
workPhone
=
"416-555-6666"
;
String
=
"[email protected]"
;
// Use new-style Contacts batch operations.
// Build List of ops, then call applyBatch().
try
{
ArrayList
<
ContentProviderOperation
>
ops
=
new
ArrayList
<
ContentProviderOperation
>();
AuthenticatorDescription
[]
types
=
accountManager
.
getAuthenticatorTypes
();
ops
.
add
(
ContentProviderOperation
.
newInsert
(
ContactsContract
.
RawContacts
.
CONTENT_URI
).
withValue
(
ContactsContract
.
RawContacts
.
ACCOUNT_TYPE
,
types
[
0
].
type
)
.
withValue
(
ContactsContract
.
RawContacts
.
ACCOUNT_NAME
,
ACCOUNT_NAME
)
.
build
());
ops
.
add
(
ContentProviderOperation
.
newInsert
(
ContactsContract
.
Data
.
CONTENT_URI
)
.
withValueBackReference
(
ContactsContract
.
Data
.
RAW_CONTACT_ID
,
0
)
.
withValue
(
ContactsContract
.
Data
.
MIMETYPE
,
ContactsContract
.
CommonDataKinds
.
StructuredName
.
CONTENT_ITEM_TYPE
)
.
withValue
(
ContactsContract
.
CommonDataKinds
.
StructuredName
.
DISPLAY_NAME
,
name
)
.
build
());
ops
.
add
(
ContentProviderOperation
.
newInsert
(
ContactsContract
.
Data
.
CONTENT_URI
).
withValueBackReference
(
ContactsContract
.
Data
.
RAW_CONTACT_ID
,
0
).
withValue
(
ContactsContract
.
Data
.
MIMETYPE
,
ContactsContract
.
CommonDataKinds
.
Phone
.
CONTENT_ITEM_TYPE
)
.
withValue
(
ContactsContract
.
CommonDataKinds
.
Phone
.
NUMBER
,
homePhone
).
withValue
(
ContactsContract
.
CommonDataKinds
.
Phone
.
TYPE
,
ContactsContract
.
CommonDataKinds
.
Phone
.
TYPE_HOME
)
.
build
());
ops
.
add
(
ContentProviderOperation
.
newInsert
(
ContactsContract
.
Data
.
CONTENT_URI
).
withValueBackReference
(
ContactsContract
.
Data
.
RAW_CONTACT_ID
,
0
).
withValue
(
ContactsContract
.
Data
.
MIMETYPE
,
ContactsContract
.
CommonDataKinds
.
Phone
.
CONTENT_ITEM_TYPE
)
.
withValue
(
ContactsContract
.
CommonDataKinds
.
Phone
.
NUMBER
,
workPhone
).
withValue
(
ContactsContract
.
CommonDataKinds
.
Phone
.
TYPE
,
ContactsContract
.
CommonDataKinds
.
Phone
.
TYPE_WORK
)
.
build
());
ops
.
add
(
ContentProviderOperation
.
newInsert
(
ContactsContract
.
Data
.
CONTENT_URI
).
withValueBackReference
(
ContactsContract
.
Data
.
RAW_CONTACT_ID
,
0
).
withValue
(
ContactsContract
.
Data
.
MIMETYPE
,
ContactsContract
.
CommonDataKinds
.
.
CONTENT_ITEM_TYPE
)
.
withValue
(
ContactsContract
.
CommonDataKinds
.
.
DATA
,
)
.
withValue
(
ContactsContract
.
CommonDataKinds
.
.
TYPE
,
ContactsContract
.
CommonDataKinds
.
.
TYPE_HOME
)
.
build
());
getContentResolver
().
applyBatch
(
ContactsContract
.
AUTHORITY
,
ops
);
Toast
.
makeText
(
this
,
getString
(
R
.
string
.
addContactSuccess
),
Toast
.
LENGTH_LONG
).
show
();
}
catch
(
Exception
e
)
{
Toast
.
makeText
(
this
,
getString
(
R
.
string
.
addContactFailure
),
Toast
.
LENGTH_LONG
).
show
();
Log
.
e
(
LOG_TAG
,
getString
(
R
.
string
.
addContactFailure
),
e
);
}
}
The resultant contact shows up in the Contacts app, as shown in Figure 10-8. If the new contact is not initially visible, go to the main Contacts list page, press Menu, select Display Options, and select Groups until it does appear. Alternatively, you can search in All Contacts and it will show up.
The source code for this project is in the Android Cookbook repository, in the subdirectory AddContact (see “Getting and Using the Code Examples”).
Ian Darwin
Use an Intent to let the user pick one contact. Use a ContentResolver
to create a SQLite query for the chosen contact, and use SQLite and predefined constants in the confusingly named ContactsContract
class to retrieve the parts you want. Be aware that the Contacts database was designed for generality, not for simplicity.
The code in Example 10-22 is from TabbyText, my SMS/text message sender for tablets. The user has already picked the given contact (using the Contactz app; see Recipe 10.14). Here we want to extract the mobile number and save it in a text field in the current Activity, so the user can post-edit it if need be, or even reject it, before actually sending the message, so we just set the text in an EditText
once we find it.
Finding it turns out to be the hard part. We start with a query that we get from the ContentProvider
, to extract the ID field for the given contact. Information such as phone numbers and email addresses are in their own tables, so we need a second query to feed in the ID as part of the “select” part of the query. This query gives a list of the contact’s phone numbers. We iterate through this, taking each valid phone number and setting it on the EditText
.
A further elaboration would restrict this to only selecting the mobile number (some versions of Contacts allow both home and work numbers, but only one mobile number).
@Override
protected
void
onActivityResult
(
int
requestCode
,
int
resultCode
,
Intent
data
)
{
if
(
requestCode
==
REQ_GET_CONTACT
)
{
switch
(
resultCode
)
{
case
Activity
.
RESULT_OK
:
// The Contacts API is about the most complex to use.
// First we have to retrieve the Contact, since
// we only get its URI from the Intent.
Uri
resultUri
=
data
.
getData
();
// E.g., content://contacts/people/123
Cursor
cont
=
getContentResolver
().
query
(
resultUri
,
null
,
null
,
null
,
null
);
if
(!
cont
.
moveToNext
())
{
// Expect 001 row(s)
Toast
.
makeText
(
this
,
"Cursor contains no data"
,
Toast
.
LENGTH_LONG
).
show
();
return
;
}
int
columnIndexForId
=
cont
.
getColumnIndex
(
ContactsContract
.
Contacts
.
_ID
);
String
contactId
=
cont
.
getString
(
columnIndexForId
);
int
columnIndexForHasPhone
=
cont
.
getColumnIndex
(
ContactsContract
.
Contacts
.
HAS_PHONE_NUMBER
);
boolean
hasAnyPhone
=
Boolean
.
parseBoolean
(
cont
.
getString
(
columnIndexForHasPhone
));
if
(!
hasAnyPhone
)
{
Toast
.
makeText
(
this
,
"Selected contact seems to have no phone numbers "
,
Toast
.
LENGTH_LONG
).
show
();
}
// Now we have to do another query to actually get the numbers!
Cursor
numbers
=
getContentResolver
().
query
(
ContactsContract
.
CommonDataKinds
.
Phone
.
CONTENT_URI
,
null
,
ContactsContract
.
CommonDataKinds
.
Phone
.
CONTACT_ID
+
"="
+
contactId
,
// "selection",
null
,
null
);
// Could further restrict to mobile number...
while
(
numbers
.
moveToNext
())
{
String
aNumber
=
numbers
.
getString
(
numbers
.
getColumnIndex
(
ContactsContract
.
CommonDataKinds
.
Phone
.
NUMBER
));
System
.
out
.
println
(
aNumber
);
number
.
setText
(
aNumber
);
}
if
(
cont
.
moveToNext
())
{
System
.
out
.
println
(
"WARNING: More than 1 contact returned by picker!"
);
}
numbers
.
close
();
cont
.
close
();
break
;
case
Activity
.
RESULT_CANCELED
:
// Nothing to do here
break
;
default
:
Toast
.
makeText
(
this
,
"Unexpected resultCode: "
+
resultCode
,
Toast
.
LENGTH_LONG
).
show
();
break
;
}
}
super
.
onActivityResult
(
requestCode
,
resultCode
,
data
);
}
You can download the source code for this example from GitHub.
Ian Darwin
Use the drag and drop API, supported since Android 3.0 and even in the appcompat library. Register a drag listener on the drop target. Start the drag in response to a UI event in the drag source.
The normal use of drag-and-drop is to request a change, such as uninstalling an application, removing an item from a list, and so on.
The normal operation of a drag is to communicate some user-chosen data from one View
to another;
i.e, both the source of the drag and the target of the drop must be View
objects.
To pass information from the source View
(e.g., the list item from where you start the drag) to the drop target, there is a special wrapper object called a ClipData
. The ClipData
can either hold an Android URI representing the object to be dropped, or some arbitrary data. The URI will usually be passed to a ContentProvider
for processing.
The basic steps in a drag and drop are:
Implement an OnDragListener
; its only method is onDrag()
, in which
you should be prepared for the various action events such as ACTION_DRAG_STARTED
,
ACTION_DRAG_ENTERED
, ACTION_DRAG_EXITED
, ACTION_DROP
, and ACTION_DRAG_ENDED
.
Register this listener on the drop target, using the View
’s setOnDragListener()
method.
In a listener attached to the source View
, usually in an onItemLongClick()
or similar method, start the drag.
For ACTION_DRAG_STARTED
and/or ACTION_DRAG_ENTERED
on the drop target, highlight the View to direct the user’s attention to it as a target, by changing the background color, the image, or similar.
For ACTION_DROP
, in the drop target, perform the action.
For ACTION_DRAG_EXITED
and/or ACTION_DRAG_ENDED
, do any cleanup required (e.g., undo changes made in the ACTION_DRAG_STARTED
and/or ACTION_DRAG_ENTERED
case).
In this example (Figure 10-9) we implement a very simple drag-and-drop scenario: a URL is passed from a button to a text field.
You start the drag by long-pressing on the button at the top, and drag it down to the text view at the bottom.
As soon as you start the drag, the drop target’s background changes to yellow, and the “drag shadow”
(by default, an image of the source View
object) appears
to indicate the drag position.
At this point, if you release the drag shadow outside the drop target, the drag shadow will find its way back
to the drag source, and the drag will end (the target turns white again).
On the other hand, if you drag into the drop target, its color changes to red.
If you release the drag shadow here, it is considered a successful drop, and the listener is called
with an action code of ACTION_DROP
; you should perform the corresponding action (our example just displays the Uri
in a toast to prove
that it arrived).
Example 10-23 shows the code in onCreate()
to start the drag in response to a long-press on the top button.
// Register the long-click listener to START the drag
Button
b
=
(
Button
)
findViewById
(
R
.
id
.
button
);
b
.
setOnLongClickListener
(
new
View
.
OnLongClickListener
()
{
@Override
public
boolean
onLongClick
(
View
v
)
{
Uri
contentUri
=
Uri
.
parse
(
"http://oracle.com/java/"
);
ClipData
cd
=
ClipData
.
newUri
(
getContentResolver
(),
"Dragging"
,
contentUri
);
v
.
startDrag
(
cd
,
new
DragShadowBuilder
(
v
),
null
,
0
);
return
true
;
}
});
This version passes a Uri
through the ClipData
. To pass arbitrary information, you can use another factory method
such as:
ClipData
.
newPlainText
(
String
label
,
String
data
)
Then you need to register your OnDragListener
with the target View
:
target
=
findViewById
(
R
.
id
.
drop_target
);
target
.
setOnDragListener
(
new
MyDrag
());
The code for our OnDragListener
is shown in Example 10-24.
public
class
MyDrag
implements
View
.
OnDragListener
{
@Override
public
boolean
onDrag
(
View
v
,
DragEvent
e
)
{
switch
(
e
.
getAction
())
{
case
DragEvent
.
ACTION_DRAG_STARTED
:
target
.
setBackgroundColor
(
COLOR_TARGET_DRAGGING
);
return
true
;
case
DragEvent
.
ACTION_DRAG_ENTERED
:
Log
.
d
(
TAG
,
"onDrag: ENTERED e="
+
e
);
target
.
setBackgroundColor
(
COLOR_TARGET_ALERT
);
return
true
;
case
DragEvent
.
ACTION_DRAG_LOCATION
:
// Nothing to do but MUST consume the event
return
true
;
case
DragEvent
.
ACTION_DROP
:
Log
.
d
(
TAG
,
"onDrag: DROP e="
+
e
);
final
ClipData
clipItem
=
e
.
getClipData
();
Toast
.
makeText
(
DragDropActivity
.
this
,
"DROPPED: "
+
clipItem
.
getItemAt
(
0
).
getUri
(),
Toast
.
LENGTH_LONG
).
show
();
return
true
;
case
DragEvent
.
ACTION_DRAG_EXITED
:
target
.
setBackgroundColor
(
COLOR_TARGET_NORMAL
);
return
true
;
case
DragEvent
.
ACTION_DRAG_ENDED
:
target
.
setBackgroundColor
(
COLOR_TARGET_NORMAL
);
return
true
;
default
:
// Unhandled event type
return
false
;
}
}
}
In the ACTION_DROP
case, you would usually pass the Uri
to a ContentResolver
; for example:
getContentResolver
().
delete
(
event
.
getClipData
().
getItemAt
(
0
).
getUri
());
In some versions of Android, you must consume the DragEvent.ACTION_DRAG_LOCATION
event as shown,
or you will get a strange ClassCastException
with the stack trace down in the View
class.
If you are maintaining compatibility with ancient legacy versions of Android (anything pre-Honeycomb), you must protect the calls to this API with a code guard; it will compile for those older releases with the compatibility library, but calls will cause an application failure.
Ian Darwin
You want to share internal-storage files (see Recipe 10.1) with another app,
without the bother of putting the data into a Cursor
and creating a ContentProvider
.
The FileProvider
class allows you to make files available to another application,
usually in response to an Intent.
It is simpler to set up than a ContentProvider
(Recipe 10.15), but is actually a subclass of ContentProvider
.
This example exposes a secrets.txt file from one application to another.
For this example I have created an Android Studio project called FileProviderDemo, which contains two
different applications in two different packages, providerapp
and requestingapp
.
We’ll start by discussing the Provider app since it contains the actual FileProvider
.
However, you have to run the Requester application first, as it will start the Provider app.
Figure 10-10 shows the sequence of the Requester app, then the Provider app,
and finally the Requester app with its request satisfied.
Unlike the ContentProvider
case, you rarely have to write code for the provider itself;
instead, use the FileProvider
class directly as a provider
in your AndroidManifest.xml file,
as shown in Example 10-25.
<provider
android:name=
"android.support.v4.content.FileProvider"
android:authorities=
"com.darwinsys.fileprovider"
android:grantUriPermissions=
"true"
android:exported=
"false"
>
<meta-data
android:name=
"android.support.FILE_PROVIDER_PATHS"
android:resource=
"@xml/filepaths"
/>
</provider>
The provider does not have to be exported for this usage, but must have the ability to grant Uri
permissions as shown.
The meta-data
element gives the name of a simple mapping file, which is required
to map “virtual paths” to actual paths, as shown in Example 10-26.
<paths>
<files-path
path=
"secrets/"
name=
"shared_secrets"
/>
</paths>
Finally, there has to be an Activity to provide the Uri
to the requested file.
In our example this is the ProvidingActivity
, shown in Example 10-27.
/**
* The backend app, part of FileProviderDemo.
* There is only one file provided; in a real app there would
* probably be a file chooser UI or other means of selecting a file.
*/
public
class
ProvidingActivity
extends
AppCompatActivity
{
private
File
mRequestFile
;
private
Intent
mResultIntent
;
@Override
protected
void
onCreate
(
Bundle
savedInstanceState
)
{
super
.
onCreate
(
savedInstanceState
);
mResultIntent
=
new
Intent
(
"com.darwinsys.fileprovider.ACTION_RETURN_FILE"
);
setContentView
(
R
.
layout
.
activity_providing
);
Toolbar
toolbar
=
(
Toolbar
)
findViewById
(
R
.
id
.
toolbar
);
setSupportActionBar
(
toolbar
);
// The Layout provides a text field with text like
// "If you agree to provide the file, press the Agree button"
Button
button
=
(
Button
)
findViewById
(
R
.
id
.
button
);
button
.
setOnClickListener
(
new
View
.
OnClickListener
()
{
@Override
public
void
onClick
(
View
view
)
{
provideFile
();
}
});
mRequestFile
=
new
File
(
getFilesDir
(),
"secrets/demo.txt"
);
// On first run of application, create the "hidden" file in internal storage
if
(!
mRequestFile
.
exists
())
{
mRequestFile
.
getParentFile
().
mkdirs
();
try
(
PrintWriter
pout
=
new
PrintWriter
(
mRequestFile
))
{
pout
.
println
(
"This is the revealed text"
);
pout
.
println
(
"And then some."
);
}
catch
(
IOException
e
)
{
e
.
printStackTrace
();
}
}
}
/**
* The provider application has to return a Uri wrapped in an Intent,
* along with permission to read that file.
*/
private
void
provideFile
()
{
// The approved target is one hardcoded file in our directory
mRequestFile
=
new
File
(
getFilesDir
(),
"secrets/demo.txt"
);
Uri
fileUri
=
FileProvider
.
getUriForFile
(
this
,
"com.darwinsys.fileprovider"
,
mRequestFile
);
// The requester is in a different app so can't normally read our files!
mResultIntent
.
addFlags
(
Intent
.
FLAG_GRANT_READ_URI_PERMISSION
);
mResultIntent
.
setDataAndType
(
fileUri
,
getContentResolver
().
getType
(
fileUri
));
// Attach that to the result Intent
mResultIntent
.
setData
(
fileUri
);
// Set the result to be "success" + the result
setResult
(
Activity
.
RESULT_OK
,
mResultIntent
);
finish
();
}
}
The important part of this code is in the provideFile()
method, which:
Creates a Uri
for the actual file (in this trivial example there is only one file, with a hardcoded filename)
Adds flags to the result Intent to let the receiving app read this one file (only) with our permissions
Sets the MIME type of the result
Adds the file Uri
as data to the result Intent
Sets the result Intent, and the “success” flags Activity.RESULT_OK
, as the result of this Activity
Calls finish()
to end the Activity
Note that this Activity is not meant to be invoked by the user directly, so it does not have
LAUNCHER
in its AndroidManifest entry.
Thus when you “run” it, you will get an error message, “Could not identify launch activity: Default Activity not found.”
This error is normal and expected.
If it bothers you, build the application and install it, but don’t try to run it.
Or, you could add a dummy Activity with a trivial message onscreen.
Remember that the point of the FileProvider
is to share files from one application to another,
running with different user permissions.
Our second application also has only one Activity, the “requesting” Activity.
Most of this is pretty standard boilerplate code. In onCreate()
, we create the requesting Intent:
mRequestFileIntent = new Intent(Intent.ACTION_PICK); mRequestFileIntent.setType("text/plain");
The main part of the UI is a text area, which initially suggests that you request a file by pressing the button. That button’s action listener is only one line:
startActivityForResult(mRequestFileIntent, ACTION_GET_FILE);
This will, as discussed in Recipe 4.5, result in a subsequent call to onActivityComplete()
, which is
shown in Example 10-28.
public class RequestingActivity extends AppCompatActivity { private static final int ACTION_GET_FILE = 1; private Intent mRequestFileIntent; ... @Override protected void onActivityResult(int requestCode, int resultCode, Intent resultIntent) { if (requestCode == ACTION_GET_FILE) { if (resultCode == Activity.RESULT_OK) { try { // get the file Uri returnUri = resultIntent.getData(); final InputStream is = getContentResolver().openInputStream(returnUri); final BufferedReader br = new BufferedReader(new InputStreamReader(is)); String line; TextView fileViewTextArea = (TextView) findViewById(R.id.fileView); fileViewTextArea.setText(""); // reset each time while ((line = br.readLine()) != null) { fileViewTextArea.append(line); fileViewTextArea.append(" "); } } catch (IOException e) { Toast.makeText(this, "IO Error: " + e, Toast.LENGTH_LONG).show(); } } else { Toast.makeText(this, "Request denied or canceled", Toast.LENGTH_LONG).show(); } return; } // For any other Activity, we can do nothing... super.onActivityResult(requestCode, resultCode, resultIntent); } }
Assuming that the request succeeds, you will get called here with requestCode
set to the only valid action,
RESULT_OK
, and the resultIntent
being the one that the providing Activity set as the Activity result—that is, the Intent wrapping the Uri
that we need in order to read the file!
So we just get the Uri
from the Intent and open that as an input stream, and we can read the “secret”
file from the providing application’s otherwise-private internal storage.
Just to show that we got it, we display the “secret” file in a text area, shown in
the righthand screenshot in Figure 10-10.
The official documentation on sharing files.
Use a SyncAdapter
, which lets you synchronize in both directions, providing the infrastructure to run a “merge” algorithm of your own devising.
Assuming that you have decided to go the SyncAdapter
route, you will first need to make some design decisions:
How will you access the remote service? (A REST API, as in Recipe 12.1? Volley, as in Recipe 12.2? Custom socket code?)
How will you package the remote service code? (ContentProvider
, as in Recipe 10.15? DAO? Other?)
What algorithm will you use for ensuring that all the objects are kept up-to-date on the server and on one (or more!) mobile devices? (Don’t forget to handle inserts, updates, and deletions originating from either end!)
Then you need to prepare (design and code) the following:
A ContentProvider
(see Recipe 10.15. You’ll need this even if you aren’t using one to access your on-device data, but if you already have one, you can certainly use it.
An AuthenticatorService
, which is boilerplate code, to start the Authenticator
.
An Authenticator
class, since you must have an on-device “account” to allow the system to control syncing.
A SyncAdapterService
, which is boilerplate code, to start the SyncAdapter
.
And finally, the actual SyncAdapter
, which can probably subclass AbstractThreadedSyncAdapter
.
The following code snippets are taken from my working todo list manager, TodoMore,
which exists as an Android app, a JavaServer Faces (JSF) web application, a JAX-RS REST service used by the
Android app, and possibly others. Each is its own GitHub module, so you can git clone
just the parts you need.
In your main Activity’s onCreate()
method you need to tell the system that you want to be synchronized.
This consists mainly of creating an Account
(the first time; after that, finding it) and requesting synchronization using
this Account
. You can start this in your main Activity’s onCreate()
method by calling two helper methods shown
in Example 10-29. Note that accountType
is just a unique string, such as "MyTodoAccount"
.
public
void
onCreate
(
Bundle
savedInstanceState
)
{
// ...
mAccount
=
createSyncAccount
(
this
);
enableSynching
(
mPrefs
.
getBoolean
(
KEY_ENABLE_SYNC
,
true
));
}
Account
createSyncAccount
(
Context
context
)
{
AccountManager
accountManager
=
(
AccountManager
)
context
.
getSystemService
(
ACCOUNT_SERVICE
);
Account
[]
accounts
=
accountManager
.
getAccountsByType
(
getString
(
R
.
string
.
accountType
));
// Our account type
if
(
accounts
.
length
==
0
)
{
// Haven't created one?
// Create the account type and default account
Account
newAccount
=
new
Account
(
ACCOUNT
,
getString
(
R
.
string
.
accountType
));
/*
* Add the account and account type; no password or user data yet.
* If successful, return the Account object; else report an error.
*/
if
(
accountManager
.
addAccountExplicitly
(
newAccount
,
"top secret"
,
null
))
{
Log
.
d
(
TAG
,
"Add Account Explicitly: Success!"
);
return
newAccount
;
}
else
{
throw
new
IllegalStateException
(
"Add Account failed..."
);
}
}
else
{
// Or we already created one, so use it
return
accounts
[
0
];
}
}
void
enableSynching
(
boolean
enable
)
{
String
authority
=
getString
(
R
.
string
.
datasync_provider_authority
);
if
(
enable
)
{
ContentResolver
.
setSyncAutomatically
(
mAccount
,
authority
,
true
);
// Force immediate syncing at startup - optional feature
Bundle
immedExtras
=
new
Bundle
();
immedExtras
.
putBoolean
(
"SYNC_EXTRAS_MANUAL"
,
true
);
ContentResolver
.
requestSync
(
mAccount
,
authority
,
immedExtras
);
Bundle
extras
=
new
Bundle
();
long
pollFrequency
=
SYNC_INTERVAL_IN_MINUTES
;
ContentResolver
.
addPeriodicSync
(
mAccount
,
authority
,
extras
,
pollFrequency
);
}
else
{
// Disabling, so cancel all outstanding syncs until further notice
ContentResolver
.
cancelSync
(
mAccount
,
authority
);
}
}
The Authority
is that of your ContentProvider
, which is why you need one of those even if you are
accessing your data through a DAO (Data Access Object) that, for example, calls SQLite directly.
The Extras
that you put in your call to requestSync()
control some of the optional behavior
of the SyncAdapter
. You can just copy my code for now, but you’ll want to read the full documentation
at some point.
Let’s turn now to the most interesting (and complex) part, the SyncAdapter
itself,
which subclasses AbstractThreadedSyncAdapter
and
whose central mover and shaker is the onPerformSync()
method:
public
class
MySyncAdapter
extends
AbstractThreadedSyncAdapter
{
public
void
onPerformSync
(
Account
account
,
Bundle
extras
,
String
authority
,
ContentProviderClient
provider
,
SyncResult
syncResult
)
{
// Do some awesome work here
syncResult
.
clear
();
// Indicate success
}
}
This one method is called automatically by the synchronization subsystem when the specified interval has elapsed,
or when you call requestSync()
for an immediate sync (which is optional, but you probably want to do it at application
startup, as we did in our onCreate()
).
As the method signature states, it is called with an Account
(which you created earlier), a Bundle
for arguments,
your Authority
string again,
a ContentProvider
wrapper called a ContentProviderClient
, and a SyncResult
(which you have to use to
inform the framework whether your operation succeeded or failed).
When it comes to writing the body of this method, you’re completely on your own. I did something like the following in my todo list application:
Fetch the previous update timestamp.
Delete remotely any Task
s that were deleted locally.
Get a list of all the Remote
tasks.
Get a list of all the Local
tasks.
Step through each list, determining which Task
s need to be sent to the other side (remote Task
s to local database, local Task
s to remote database), by such criteria as whether they have a Server ID yet, whether their modification time
is later than the previous update timestamp, etc.
Save any new/modified remote Task
s into the local database.
Save any new/modified local Task
s into the remote database.
Update the timestamp.
The core of this operation is #5, “step through each list.”
Because this does all the decision making, I extracted it to a separate method,
unimaginatively called algorithm()
, which does no I/O or networking, but just works on lists that are passed in.
This was done for testability: I can easily unit test this part without invoking any Android or networking functionality.
It gets passed the remote list and the local list from steps 3 and 4, and populates two more lists
(which are passed in empty) to contain the to-be-copied tasks.
The algorithm()
method is called in the middle of the onPerformSync()
method—after steps 1–4 have been completed—to prepare the lists for use in steps 6 and 7, in which
the code in onPerformSync()
has to do the actual work of updating the local and remote databases.
Here you can use almost any database access method for the local database (such as a ContentProvider
, a DAO, direct use of SQLite, etc.),
and almost any network method to add, update, and remove remote objects (URLConnection
to the REST service,
the deprecated HttpClient
, perhaps Volley, etc.).
I don’t even show the code for onPerformSync()
or my inner algorithm()
method, as this is something you’ll have to work out on your own; it is in the sample GitHub download if you want to look at mine.
I will say that I used a DAO locally and a URLConnection
to the REST service for remote access.
You must have a ContentProvider
, even if you’re not using it but are instead using SQLite directly, as mentioned.
A dummy ContentProvider
just has to exist, it doesn’t have to do anything if you don’t use it.
All the methods can in fact be dummied out, as long as your dummy content provider in fact subclasses ContentProvider
:
public
class
TodoContentProvider
extends
ContentProvider
{
/*
* Always return true, indicating success
*/
@Override
public
boolean
onCreate
()
{
return
true
;
}
/*
* Return no type for MIME type
*/
@Override
public
String
getType
(
Uri
uri
)
{
return
null
;
}
/*
* query() always returns no results
*/
@Override
public
Cursor
query
(
Uri
uri
,
String
[]
projection
,
String
selection
,
String
[]
selectionArgs
,
String
sortOrder
)
{
return
null
;
}
// Other methods similarly return null
// or could throw UnsupportedOperationException(?)
}
You will also need a Service to start your SyncAdapter
.
This is pretty basic; my version just gets the SharedPreferences
because they are needed in the adapter,
and passes that to the constructor. The onBind()
method is called by the system
to connect things up:
public
class
TodoSyncService
extends
Service
{
private
TodoSyncAdapter
mSyncAdapter
;
private
static
final
Object
sLock
=
new
Object
();
@Override
public
void
onCreate
()
{
super
.
onCreate
();
SharedPreferences
prefs
=
PreferenceManager
.
getDefaultSharedPreferences
(
getApplication
());
synchronized
(
sLock
)
{
if
(
mSyncAdapter
==
null
)
{
mSyncAdapter
=
new
TodoSyncAdapter
(
getApplicationContext
(),
prefs
,
true
);
}
}
}
@Override
public
IBinder
onBind
(
Intent
intent
)
{
return
mSyncAdapter
.
getSyncAdapterBinder
();
}
}
We also need an AccountAuthenticator
, which can be dummied out:
public
class
TodoDummyAuthenticator
extends
AbstractAccountAuthenticator
{
private
final
static
String
TAG
=
"TodoDummyAuthenticator"
;
public
TodoDummyAuthenticator
(
Context
context
)
{
super
(
context
);
}
@Override
public
Bundle
editProperties
(
AccountAuthenticatorResponse
response
,
String
accountType
)
{
throw
new
UnsupportedOperationException
();
}
@Override
public
Bundle
addAccount
(
AccountAuthenticatorResponse
response
,
String
accountType
,
String
authTokenType
,
String
[]
requiredFeatures
,
Bundle
options
)
{
Log
.
d
(
TAG
,
"TodoDummyAuthenticator.addAccount()"
);
return
null
;
}
@Override
public
Bundle
confirmCredentials
(
AccountAuthenticatorResponse
response
,
Account
account
,
Bundle
options
)
{
return
null
;
}
@Override
public
Bundle
getAuthToken
(
AccountAuthenticatorResponse
response
,
Account
account
,
String
authTokenType
,
Bundle
options
)
{
throw
new
UnsupportedOperationException
();
}
@Override
public
String
getAuthTokenLabel
(
String
authTokenType
)
{
throw
new
UnsupportedOperationException
();
}
@Override
public
Bundle
updateCredentials
(
AccountAuthenticatorResponse
response
,
Account
account
,
String
authTokenType
,
Bundle
options
)
{
throw
new
UnsupportedOperationException
();
}
@Override
public
Bundle
hasFeatures
(
AccountAuthenticatorResponse
response
,
Account
account
,
String
[]
features
)
{
throw
new
UnsupportedOperationException
();
}
}
And as with the SyncAdapter
itself, you need a Service
class to start this, which is pretty simple:
public
class
TodoDummyAuthenticatorService
extends
Service
{
// Instance field that stores the authenticator object,
// so we only create it once for multiple uses
private
TodoDummyAuthenticator
mAuthenticator
;
@Override
public
void
onCreate
()
{
// Create the Authenticator object
mAuthenticator
=
new
TodoDummyAuthenticator
(
this
);
}
/*
* Called when the system binds to this Service to make the IPC call;
* just return the authenticator's IBinder
*/
@Override
public
IBinder
onBind
(
Intent
intent
)
{
return
mAuthenticator
.
getIBinder
();
}
}
The Services that expose the SyncAdapter
and the Authenticator
, as well as the ContentProvider
,
are Android components, so they must all be listed in the Android manifest:
<!-- Sync Adapter-->
<service
android:name=
".sync.TodoSyncService"
android:exported=
"true"
android:process=
":sync"
>
<intent-filter>
<action
android:name=
"android.content.SyncAdapter"
/>
</intent-filter>
<meta-data
android:name=
"android.content.SyncAdapter"
android:resource=
"@xml/synchadapter"
/>
</service>
<!-- Dummy authenticator - needed by SyncAdapter -->
<service
android:name=
".sync.TodoDummyAuthenticatorService"
>
<!-- Required filter used by the system to launch our account service. -->
<intent-filter>
<action
android:name=
"android.accounts.AccountAuthenticator"
/>
</intent-filter>
<!-- This points to an XML file which describes our account service. -->
<meta-data
android:name=
"android.accounts.AccountAuthenticator"
android:resource=
"@xml/authenticator"
/>
</service>
<!-- Dummy ContentProvider also "needed" -->
<provider
android:name=
"TodoContentProvider"
android:authorities=
"@string/datasync_provider_authority"
android:exported=
"false"
android:syncable=
"true"
/>
With all the pieces in place, you should see your account type showing up in Settings → Accounts, and selecting it should allow you to trigger a sync, enable/disable syncing, see the time of the last sync, and so on (see Figure 10-11).
The official documentation on transferring data using sync adapters.
The sample code is available on GitHub.
To use it as a distributed system you would have to set up your own server
(possibly using the TodoREST
repository), then configure
the server, credentials, etc. in the Settings Activity and test it out.
Ian Darwin
You need to store data from your app users’ devices into the cloud, and don’t have time to write a SyncAdapter
.
You want to have access to your cloud data from Apple iOS devices and/or a web application.
While the SyncAdapter
is fine in its own way, it is a complex beast to use.
Firebase, a Google commercial product, makes it easier to develop your application.
Firebase is far from the first solution in this area, but it is the one that Android recommends—unsurprisingly, since Google offers it as a commercial service. We discuss it here not to say that it’s the best or only way to do things, but because it really is the path of least resistance to getting an Android cloud-based database up and running, and it’s a good example of how such things work.
A brief summary of the steps are:
Create an account on the Firebase website, which will give you a unique URL to use, of the form https://nnn
.firebase.com/ (the nnn
will be provided when you register).
Decide how to structure the data.
Add the Firebase library coordinates to your pom.xml or build.gradle file (or download the JAR and add it to your project the hard way, or use Android Studio to add the Firebase library to your project).
Write code in the onCreate()
method of your Activity or application (see Recipe 2.3) to create a Firebase
object using your unique URL.
To receive the data, add a listener to the Firebase
object.
To insert, query, update, or delete, invoke methods on the Firebase
object, some of which have additional listeners to notify you of completion and/or results.
In this example we will explore a simple “todo list” application, with data stored only in Firebase.
This is an alternate implementation of the example used in the SyncAdapter
recipe (Recipe 10.20);
both are part of my “TodoMore” application family, but the two use different backends at this point.
The Firebase version shown here can be downloaded from GitHub.
Creating an account is just a matter of using the website signup. It’s free to sign up and develop your app; see the Pricing page for the various plans available.
My data is quite simple; it consists of a list of Todo Task items for each user.
The Firebase data is basically a hierarchy of data, effectively in JSON format.
The Task
class in Java maps directly to the fields of the database. As you can see in Figure 10-12,
there are fields like name
(a one-liner describing the item),
description
(a longer discussion if needed, may be null
),
creationDate
(when you entered the task, stored as a lightweight custom Data
class, not a java.util.Date
),
modified
(simple timestamp format),
priority
and status
(which are enum
s in the Java code but represented as Strings
in the JSON),
and id
(a long integer used as a primary key in the relational database, but not used here).
Initializing the database is done in the onCreate()
method of the Application
class,
so the database will be available to any Activity
classes that need it:
private
String
mBaseUrl
;
// Loaded from a config file
private
Firebase
mDatabase
;
// The database connection, has a get method
@Override
public
void
onCreate
()
{
super
.
onCreate
();
Firebase
.
setAndroidContext
(
this
);
String
baseUrl
=
getBaseUrl
()
+
TaskListActivity
.
mCurrentUser
+
"/tasks/"
;
mDatabase
=
new
Firebase
(
baseUrl
);
}
With that out of the way, we can add a Listener
to receive the data. In the Todo application we want to download
this user’s complete list of tasks at the start of the application. If you had a larger database and didn’t want it all
on the device, you’d use a Query
, discussed in Recipe 10.7.
This is done in the onCreate()
method of the main Activity
:
((
ApplicationClass
)
getApplication
()).
getDatabase
().
addValueEventListener
(
new
ValueEventListener
()
{
@Override
public
void
onDataChange
(
DataSnapshot
snapshot
)
{
System
.
out
.
println
(
"
There are "
+
snapshot
.
getChildrenCount
()
+
" Todo Tasks(s)"
);
ApplicationClass
.
sTasks
.
clear
();
for
(
DataSnapshot
dnlSnapshot:
snapshot
.
getChildren
())
{
Task
task
=
dnlSnapshot
.
getValue
(
Task
.
class
);
System
.
out
.
println
(
task
.
getName
()
+
" - "
+
task
.
getDescription
());
String
jsonKey
=
dnlSnapshot
.
getKey
();
ApplicationClass
.
sTasks
.
add
(
new
KeyValueHolder
<>(
jsonKey
,
task
));
}
Collections
.
sort
(
ApplicationClass
.
sTasks
,
tasksComparator
);
mAdapter
.
notifyDataSetChanged
();
}
@Override
public
void
onCancelled
(
FirebaseError
error
)
{
Toast
.
makeText
(
getBaseContext
(),
"Task read cancelled!! "
+
error
,
Toast
.
LENGTH_LONG
).
show
();
}
});
The DataSnapshot
object is vaguely analogous to a SQLite Cursor
or a JDBC ResultSet
.
We iterate over its children, which are magically turned into Task
objects when we call getValue(Task.class)
.
Normally that’s all you’d have to do—it really is that simple!
Except, we will later (to update or delete) need the objects’ Firebase key values—i.e., the -KFq
… strings at the top level of each Task
.
We don’t want to store these inside the Task
class, because otherwise they’d be persisted as fields inside the object
as well as the keys.
So we introduce a wrapper class, the KeyValueHolder
(part of our application, not the Firebase API),
to map the Firebase key and the Task
object.
We want the data in List
format, both for speed and to preserve order; otherwise, the keys and values could be put in a Map
.
Speaking of order, once we’ve added them to the list (a field in the Application
class, again to share with other Activities),
we sort the list using Collections.sort()
, and notify our list adapter (see Chapter 8) that its data has changed, so the list will now show the latest data.
When I was starting I didn’t have a save()
method yet and wasn’t quite sure how the data would look, so I created
the first few entries using the “+” button in the developer’s console, to add hierarchical objects.
The “+” and “×” buttons allow you to insert and remove data at any node in the tree.
If you do make changes this way after your app has been set up—even with just the code we’ve shown so far—the list view on the device will reflect the changes almost instantly. Nice!
That’s also why the first entry has a “1” for its key instead of the longer strings that Firebase likes to use for its keys.
Those longer keys are generated client-side, by the way, to allow you to generate them even if the device is offline, but they have
more randomness than the standard UUID format so pretty much guarantee unique key values on the server.
What about updates and deletes? Well, yes. They work, and they’re pretty simple. Let’s take a look at the update and delete code.
This is in the EditActivity
; the startup code gets the task to edit by getting its key passed in the Intent.
There is a modelToView()
method that puts it into text fields and so on in the UI, and of course viewToModel()
does the inverse.
The doSave()
method is called from a Button
, and doDelete()
—hopefully less common—is called from a menu:
private
String
mKey
;
private
Task
mTask
;
private
EditText
nameTF
,
descrTF
;
@Override
protected
void
onCreate
(
Bundle
savedInstanceState
)
{
super
.
onCreate
(
savedInstanceState
);
int
index
=
getIntent
().
getIntExtra
(
TaskDetailFragment
.
ARG_ITEM_INDEX
,
0
);
KeyValueHolder
<
String
,
Task
>
taskWrapper
=
ApplicationClass
.
sTasks
.
get
(
index
);
mKey
=
taskWrapper
.
key
;
mTask
=
taskWrapper
.
value
;
setContentView
(
R
.
layout
.
activity_task_edit
);
nameTF
=
(
EditText
)
findViewById
(
R
.
id
.
nameEditText
);
descrTF
=
(
EditText
)
findViewById
(
R
.
id
.
descrEditText
);
FloatingActionButton
fab
=
(
FloatingActionButton
)
findViewById
(
R
.
id
.
fab
);
fab
.
setOnClickListener
(
new
View
.
OnClickListener
()
{
@Override
public
void
onClick
(
View
view
)
{
viewToModel
();
doSave
();
}
});
modelToView
();
// Copies fields from mTask to the UI components
}
void
doSave
()
{
viewToModel
();
((
ApplicationClass
)
getApplication
()).
getDatabase
().
child
(
mKey
).
setValue
(
mTask
);
finish
();
}
void
doDelete
()
{
((
ApplicationClass
)
getApplication
()).
getDatabase
().
child
(
mKey
).
removeValue
();
finish
();
}
Figure 10-13 shows how the application looks in action.
There are more capabilities in Firebase. The most important one that we didn’t explore is authentication; Google provides a complete and powerful authentication API along with an Access Control–based permissions scheme that you’ll certainly want to enable before you make your app-specific data available to the world. This and other features are documented on the Firebase website.
1 There are other signatures; see the official documentation for details.
98.82.120.188