Adding backup facilities. TODO: some tests, calling to trigger new automatic backup on Froyo, manual backup to SD option

This commit is contained in:
Rodrigo Damazio
2010-08-16 22:22:14 -03:00
parent df81fba565
commit 3532a6360b
18 changed files with 2223 additions and 25 deletions
+9 -9
View File
@@ -4,14 +4,17 @@
android:versionCode="16"
android:versionName="1.0.15">
<application android:icon="@drawable/arrow_icon"
android:label="@string/app_name">
android:label="@string/app_name"
android:backupAgent="com.google.android.apps.mytracks.io.backup.MyTracksBackupAgent">
<uses-library android:name="com.google.android.maps" />
<meta-data android:name="com.google.android.backup.api_key"
android:value="AEdPqrEAAAAIi-_QiwoRSc9_bAC9cmuNXTQyU8ajJmGtKdhskQ" />
<provider android:name="com.google.android.apps.mytracks.content.MyTracksProvider"
android:multiprocess="true"
android:authorities="com.google.android.maps.mytracks" />
<activity android:name="com.google.android.apps.mytracks.MyTracks"
android:label="My Tracks"
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|orientation|fontScale">
@@ -36,16 +39,13 @@
<activity android:name="com.google.android.apps.mymaps.MyMapsList"
android:label="@string/map_list_title"
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|orientation|fontScale">
</activity>
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|orientation|fontScale" />
<activity android:name="com.google.android.apps.mytracks.MyTracksList"
android:label="Recorded Tracks">
</activity>
android:label="Recorded Tracks" />
<activity android:name="com.google.android.apps.mytracks.MyTracksWaypointsList"
android:label="Recorded Waypoints">
</activity>
android:label="Recorded Waypoints" />
<activity android:name="com.google.android.apps.mytracks.MyTracksDetails"
android:label="@string/trackdetails_title" />
@@ -63,7 +63,7 @@
android:label="@string/elevation_label"/>
<activity android:name="com.google.android.apps.mytracks.WelcomeActivity"
android:label="@string/welcome"/>
android:label="@string/welcome"/>
<service android:name="com.google.android.apps.mytracks.services.TrackRecordingService"
android:process=":trackrecord"
@@ -0,0 +1,32 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.content;
/**
* Utilities for serializing primitive types.
*
* @author Rodrigo Damazio
*/
public class ContentTypeIds {
public static final byte BOOLEAN_TYPE_ID = 0;
public static final byte LONG_TYPE_ID = 1;
public static final byte INT_TYPE_ID = 2;
public static final byte FLOAT_TYPE_ID = 3;
public static final byte DOUBLE_TYPE_ID = 4;
public static final byte STRING_TYPE_ID = 5;
private ContentTypeIds() { /* Not instantiable */ }
}
@@ -15,6 +15,7 @@
*/
package com.google.android.apps.mytracks.content;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.location.Location;
@@ -267,6 +268,24 @@ public interface MyTracksProviderUtils {
*/
void updateTrack(Track track);
/**
* Creates a Track object from a given cursor.
*
* @param cursor a cursor pointing at a db or provider with tracks
* @return a new Track object
*/
Track createTrack(Cursor cursor);
/**
* Creates the ContentValues for a given Track object.
*
* Note: If the track has an id<0 the id column will not be filled.
*
* @param track a given track object
* @return a filled in ContentValues object
*/
ContentValues createContentValues(Track track);
/**
* Creates a location object from a given cursor.
*
@@ -74,15 +74,8 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils {
return values;
}
/**
* Creates the ContentValues for a given Track object.
*
* Note: If the track has an id<0 the id column will not be filled.
*
* @param track a given track object
* @return a filled in ContentValues object
*/
private static ContentValues createContentValues(Track track) {
@Override
public ContentValues createContentValues(Track track) {
ContentValues values = new ContentValues();
TripStatistics stats = track.getStatistics();
@@ -209,13 +202,8 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils {
return location;
}
/**
* Creates a Track object from a given cursor.
*
* @param cursor a cursor pointing at a db or provider with tracks
* @return a new Track object
*/
private static Track createTrack(Cursor cursor) {
@Override
public Track createTrack(Cursor cursor) {
int idxId = cursor.getColumnIndexOrThrow(TracksColumns._ID);
int idxName = cursor.getColumnIndexOrThrow(TracksColumns.NAME);
int idxDescription =
@@ -15,6 +15,8 @@
*/
package com.google.android.apps.mytracks.content;
import static com.google.android.apps.mytracks.content.ContentTypeIds.*;
import android.net.Uri;
import android.provider.BaseColumns;
@@ -42,4 +44,14 @@ public interface TrackPointsColumns extends BaseColumns {
public static final String TIME = "time";
public static final String ACCURACY = "accuracy";
public static final String SPEED = "speed";
/** Columns that go into the backup. */
public static final String[] BACKUP_COLUMNS =
{ _ID, TRACKID, LATITUDE, LONGITUDE, ALTITUDE, BEARING, TIME,
ACCURACY, SPEED };
/** Types of the columns that go into the backup. */
public static final byte[] BACKUP_COLUMN_TYPES =
{ LONG_TYPE_ID, LONG_TYPE_ID, INT_TYPE_ID, INT_TYPE_ID, FLOAT_TYPE_ID,
FLOAT_TYPE_ID, LONG_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID };
}
@@ -15,6 +15,8 @@
*/
package com.google.android.apps.mytracks.content;
import static com.google.android.apps.mytracks.content.ContentTypeIds.*;
import android.net.Uri;
import android.provider.BaseColumns;
@@ -59,4 +61,16 @@ public interface TracksColumns extends BaseColumns {
public static final String MINLON = "minlon";
public static final String MAXLON = "maxlon";
public static final String MAPID = "mapid";
public static final String[] BACKUP_COLUMNS = {
_ID, NAME, DESCRIPTION, CATEGORY, STARTID, STOPID, STARTTIME, STOPTIME,
NUMPOINTS, TOTALDISTANCE, TOTALTIME, MOVINGTIME, AVGSPEED, AVGMOVINGSPEED,
MAXSPEED, MINELEVATION, MAXELEVATION, ELEVATIONGAIN, MINGRADE, MAXGRADE,
MINLAT, MAXLAT, MINLON, MAXLON };
public static final byte[] BACKUP_COLUMN_TYPES = {
LONG_TYPE_ID, STRING_TYPE_ID, STRING_TYPE_ID, LONG_TYPE_ID, LONG_TYPE_ID,
LONG_TYPE_ID, LONG_TYPE_ID, INT_TYPE_ID, FLOAT_TYPE_ID, LONG_TYPE_ID,
LONG_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID,
FLOAT_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID, INT_TYPE_ID,
INT_TYPE_ID, INT_TYPE_ID, INT_TYPE_ID };
}
@@ -15,6 +15,8 @@
*/
package com.google.android.apps.mytracks.content;
import static com.google.android.apps.mytracks.content.ContentTypeIds.*;
import android.net.Uri;
import android.provider.BaseColumns;
@@ -66,4 +68,19 @@ public interface WaypointsColumns extends BaseColumns {
public static final String ELEVATIONGAIN = "elevationgain";
public static final String MINGRADE = "mingrade";
public static final String MAXGRADE = "maxgrade";
public static final String[] BACKUP_COLUMNS = {
_ID, TRACKID, NAME, DESCRIPTION, CATEGORY, ICON, TYPE, LENGTH, DURATION,
STARTTIME, STARTID, STOPID, LATITUDE, LONGITUDE, ALTITUDE, BEARING, TIME,
ACCURACY, SPEED, TOTALDISTANCE, TOTALTIME, MOVINGTIME, AVGSPEED,
AVGMOVINGSPEED, MAXSPEED, MINELEVATION, MAXELEVATION, ELEVATIONGAIN,
MINGRADE, MAXGRADE };
public static final byte[] BACKUP_COLUMN_TYPES = {
LONG_TYPE_ID, LONG_TYPE_ID, STRING_TYPE_ID, STRING_TYPE_ID,
STRING_TYPE_ID, STRING_TYPE_ID, INT_TYPE_ID, FLOAT_TYPE_ID, LONG_TYPE_ID,
LONG_TYPE_ID, LONG_TYPE_ID, LONG_TYPE_ID, INT_TYPE_ID, INT_TYPE_ID,
FLOAT_TYPE_ID, FLOAT_TYPE_ID, LONG_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID,
FLOAT_TYPE_ID, LONG_TYPE_ID, LONG_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID,
FLOAT_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID,
FLOAT_TYPE_ID };
}
@@ -0,0 +1,53 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.backup;
import android.app.backup.BackupDataInput;
import java.io.IOException;
import java.io.InputStream;
/**
* Input stream which reads from backup data.
* This is exactly the same as {@link android.app.backup.BackupDataInputStream},
* but for some odd reason Android doesn't want us instantiating that one.
*
* @author Rodrigo Damazio
*/
class BackupDataInputStream extends InputStream {
private final BackupDataInput data;
private final byte[] oneByte = new byte[1];
public BackupDataInputStream(BackupDataInput data) {
this.data = data;
}
@Override
public int read() throws IOException {
data.readEntityData(oneByte, 0, 1);
return oneByte[0];
}
@Override
public int read(byte[] b) throws IOException {
return data.readEntityData(b, 0, b.length);
}
@Override
public int read(byte[] b, int offset, int length) throws IOException {
return data.readEntityData(b, offset, length);
}
}
@@ -0,0 +1,215 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.backup;
import com.google.android.apps.mytracks.MyTracksConstants;
import com.google.android.apps.mytracks.content.Track;
import android.os.ParcelFileDescriptor;
import android.util.Log;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
/**
* Manager responsible for reading and writing the backup state.
*
* @author Rodrigo Damazio
*/
class BackupStateManager {
private static final byte BACKUP_VERSION = 1;
/**
* The state of a single track.
*/
public static class TrackState {
public final long trackId;
public final String name;
public final String description;
public final String category;
TrackState(long trackId, String name, String description, String category) {
this.trackId = trackId;
this.name = name;
this.description = description;
this.category = category;
}
/**
* Checks and returns whether this state is representative of the given
* track at its current state.
*/
public boolean represents(Track track) {
return trackId == track.getId() &&
name.equals(track.getName()) &&
description.equals(track.getDescription()) &&
category.equals(track.getCategory());
}
}
// File descriptors to read/write state from/to
private final ParcelFileDescriptor oldState;
private final ParcelFileDescriptor newState;
// Actual state data read from/to be written to the descriptors above
private Map<Long, TrackState> oldTracks;
private List<TrackState> newTrackState;
public BackupStateManager(ParcelFileDescriptor oldState, ParcelFileDescriptor newState) {
this.oldState = oldState;
this.newState = newState;
}
/**
* Reads the old track state.
*
* If any errors occur while reading, this will return a partial or empty map.
*
* @return a map of track ID to its state
*/
public Map<Long, TrackState> getOldTrackState() {
if (oldTracks != null) {
return oldTracks;
}
oldTracks = new TreeMap<Long, TrackState>();
if (!hasOldState()) {
return oldTracks;
}
try {
InputStream stateInputStream = openStateInput();
DataInputStream stateInput = new DataInputStream(stateInputStream);
// Read the version from the header
if (stateInput.readByte() != BACKUP_VERSION) {
throw new IllegalStateException("Bad backup version");
}
int previousNumberOfTracks = stateInput.readInt();
for (int i = 0; i < previousNumberOfTracks; i++) {
TrackState oldTrack = readTrackState(stateInput);
oldTracks.put(oldTrack.trackId, oldTrack);
}
} catch (IOException e) {
// If we fail to read, return whatever we got - this only means
// we'll be backing up more than we should.
Log.e(MyTracksConstants.TAG, "Unable to read old state", e);
}
return oldTracks;
}
/**
* Returns whether we have a known old state.
*/
protected boolean hasOldState() {
return oldState != null;
}
/**
* Opens the old state and returns a stream to read it.
*/
protected InputStream openStateInput() {
return new FileInputStream(oldState.getFileDescriptor());
}
/**
* Opens the new state and returns a stream to write to it.
*/
protected OutputStream openStateOutput() {
return new FileOutputStream(newState.getFileDescriptor());
}
/**
* Reads the state of a single track.
*
* @param stateInput the stream to read from
* @return the state read
* @throws IOException if reading fails
*/
private TrackState readTrackState(DataInputStream stateInput) throws IOException {
long trackId = stateInput.readLong();
String name = stateInput.readUTF();
String desc = stateInput.readUTF();
String category = stateInput.readUTF();
return new TrackState(trackId, name, desc, category);
}
/**
* Adds a track to the current/new state.
* Please notice that the state is not actually written until
* {@link #flushNewState} is called.
*/
public void addTrackState(Track track) {
ensureTrackState();
newTrackState.add(new TrackState(track.getId(), track.getName(),
track.getDescription(), track.getCategory()));
}
/**
* Actually writes out the new state to its output.
*
* @throws IOException if there are errors while writing
*/
public void flushNewState() throws IOException {
ensureTrackState();
OutputStream stateOutputStream = openStateOutput();
DataOutputStream stateOutput = new DataOutputStream(stateOutputStream);
stateOutput.writeByte(BACKUP_VERSION);
stateOutput.writeInt(newTrackState.size());
for (TrackState track : newTrackState) {
writeTrackState(stateOutput, track);
}
stateOutput.flush();
}
/**
* Ensures the track state buffer exists.
*/
private void ensureTrackState() {
if (newTrackState == null) {
newTrackState = new ArrayList<TrackState>();
}
}
/**
* Writes the state of a single track.
*
* @param stateOutput the stream to write to
* @param track the track state to write
* @throws IOException if there are errors while writing
*/
private void writeTrackState(DataOutputStream stateOutput, TrackState track) throws IOException {
stateOutput.writeLong(track.trackId);
stateOutput.writeUTF(track.name);
stateOutput.writeUTF(track.description);
stateOutput.writeUTF(track.category);
}
}
@@ -0,0 +1,227 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.backup;
import com.google.android.apps.mytracks.content.ContentTypeIds;
import android.database.Cursor;
import android.database.MergeCursor;
import java.io.DataOutputStream;
import java.io.IOException;
/**
* Database dumper which is able to write only part of the database
* according to some query.
*
* This dumper is symmetrical to {@link DatabaseImporter}.
*
* @author Rodrigo Damazio
*/
class DatabaseDumper {
/** The names of the columns being dumped. */
private final String[] columnNames;
/** The types of the columns being dumped. */
private final byte[] columnTypes;
/** Whether to output null fields. */
private final boolean outputNullFields;
// Temporary state
private int[] columnIndices;
private boolean[] hasFields;
public DatabaseDumper(String[] columnNames, byte[] columnTypes,
boolean outputNullFields) {
if (columnNames.length != columnTypes.length) {
throw new IllegalArgumentException("Names don't match types");
}
this.columnNames = columnNames;
this.columnTypes = columnTypes;
this.outputNullFields = outputNullFields;
}
/**
* Writes the header plus all rows that can be read from the given cursor.
* This assumes the cursor will have the same column and column indices on
* every row (and thus may not work with a {@link MergeCursor}).
*/
public void writeAllRows(Cursor cursor, DataOutputStream writer) throws IOException {
writeHeaders(cursor, cursor.getCount(), writer);
if (!cursor.moveToFirst()) {
return;
}
do {
writeOneRow(cursor, writer);
} while (cursor.moveToNext());
}
/**
* Writes just the headers for the data that will come from the given cursor.
* The headers include column information and the number of rows that will be
* written.
*
* @param cursor the cursor to get columns from
* @param numRows the number of rows that will be later written
* @throws IOException if there are errors while writing
*/
public void writeHeaders(Cursor cursor, int numRows, DataOutputStream writer) throws IOException {
initializeCachedValues(cursor);
writeQueryMetadata(cursor, numRows, writer);
}
/**
* Writes the current row from the cursor. The cursor is not advanced.
* This must be called after {@link #writeHeaders}.
*
* @param cursor the cursor to write data from
* @throws IOException if there are any errors while writing
*/
public void writeOneRow(Cursor cursor, DataOutputStream writer) throws IOException {
if (columnIndices == null) {
throw new IllegalStateException(
"Cannot write rows before writing the header");
}
if (columnIndices.length > Long.SIZE) {
throw new IllegalArgumentException("Too many fields");
}
// Build a bitmap of which fields are present
long fields = 0;
for (int i = 0; i < columnIndices.length; i++) {
hasFields[i] = !cursor.isNull(columnIndices[i]);
fields |= (hasFields[i] ? 1 : 0) << i;
}
writer.writeLong(fields);
// Actually write the present fields
for (int i = 0; i < columnIndices.length; i++) {
if (hasFields[i]) {
writeCell(columnIndices[i], columnTypes[i], cursor, writer);
} else if (outputNullFields) {
writeDummyCell(columnTypes[i], writer);
}
}
}
/**
* Initializes the column indices and other temporary state for reading from
* the given cursor.
*/
private void initializeCachedValues(Cursor cursor) {
// These indices are constant for every row (unless we're fed a MergeCursor)
if (cursor instanceof MergeCursor) {
throw new IllegalArgumentException("Cannot use a MergeCursor");
}
columnIndices = new int[columnNames.length];
for (int i = 0; i < columnNames.length; i++) {
String columnName = columnNames[i];
columnIndices[i] = cursor.getColumnIndexOrThrow(columnName);
}
hasFields = new boolean[columnIndices.length];
}
/**
* Writes metadata about the query to be dumped.
*
* @param cursor the cursor that the data will come from
* @param numRows the number of rows that will be dumped
* @throws IOException if there are any errors while writing
*/
private void writeQueryMetadata(Cursor cursor, int numRows, DataOutputStream writer) throws IOException {
// Write column data
writer.writeInt(columnNames.length);
for (int i = 0; i < columnNames.length; i++) {
String columnName = columnNames[i];
byte columnType = columnTypes[i];
writer.writeUTF(columnName);
writer.writeByte(columnType);
}
// Write the number of rows
writer.writeInt(numRows);
}
/**
* Writes a single cell of the database to the output.
*
* @param columnIdx the column index to read from
* @param columnType the type of the column to be read
* @param cursor the cursor to read from
* @throws IOException if there are any errors while writing
*/
private void writeCell(int columnIdx, byte columnTypeId, Cursor cursor, DataOutputStream writer) throws IOException {
switch (columnTypeId) {
case ContentTypeIds.LONG_TYPE_ID:
writer.writeLong(cursor.getLong(columnIdx));
return;
case ContentTypeIds.DOUBLE_TYPE_ID:
writer.writeDouble(cursor.getDouble(columnIdx));
return;
case ContentTypeIds.FLOAT_TYPE_ID:
writer.writeFloat(cursor.getFloat(columnIdx));
return;
case ContentTypeIds.BOOLEAN_TYPE_ID:
writer.writeBoolean(cursor.getInt(columnIdx) != 0);
return;
case ContentTypeIds.INT_TYPE_ID:
writer.writeInt(cursor.getInt(columnIdx));
return;
case ContentTypeIds.STRING_TYPE_ID:
writer.writeUTF(cursor.getString(columnIdx));
return;
default:
throw new IllegalArgumentException("Type " + columnTypeId + " not supported");
}
}
/**
* Writes a dummy cell value to the output.
*
* @param columnType the type of the value to write
* @throws IOException if there are any errors while writing
*/
private void writeDummyCell(byte columnTypeId, DataOutputStream writer) throws IOException {
switch (columnTypeId) {
case ContentTypeIds.LONG_TYPE_ID:
writer.writeLong(0L);
return;
case ContentTypeIds.DOUBLE_TYPE_ID:
writer.writeDouble(0.0);
return;
case ContentTypeIds.FLOAT_TYPE_ID:
writer.writeFloat(0.0f);
return;
case ContentTypeIds.BOOLEAN_TYPE_ID:
writer.writeBoolean(false);
return;
case ContentTypeIds.INT_TYPE_ID:
writer.writeInt(0);
return;
case ContentTypeIds.STRING_TYPE_ID:
writer.writeUTF("");
return;
default:
throw new IllegalArgumentException("Type " + columnTypeId + " not supported");
}
}
}
@@ -0,0 +1,178 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.backup;
import com.google.android.apps.mytracks.content.ContentTypeIds;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.net.Uri;
import java.io.DataInputStream;
import java.io.IOException;
/**
* Database importer which reads values written by {@link DatabaseDumper}.
*
* @author Rodrigo Damazio
*/
public class DatabaseImporter {
/** Maximum number of entries in a bulk insertion */
private static final int DEFAULT_BULK_SIZE = 1024;
private final Uri destinationUri;
private final ContentResolver resolver;
private final boolean readNullFields;
private final int bulkSize;
// Metadata read from the reader
private String[] columnNames;
private byte[] columnTypes;
public DatabaseImporter(Uri destinationUri, ContentResolver resolver,
boolean readNullFields) {
this(destinationUri, resolver, readNullFields, DEFAULT_BULK_SIZE);
}
protected DatabaseImporter(Uri destinationUri, ContentResolver resolver,
boolean readNullFields, int bulkSize) {
this.destinationUri = destinationUri;
this.resolver = resolver;
this.readNullFields = readNullFields;
this.bulkSize = bulkSize;
}
/**
* Reads the header which includes metadata about the table being imported.
*
* @throws IOException if there are any problems while reading
*/
private void readHeaders(DataInputStream reader) throws IOException {
int numColumns = reader.readInt();
columnNames = new String[numColumns];
columnTypes = new byte[numColumns];
for (int i = 0; i < numColumns; i++) {
columnNames[i] = reader.readUTF();
columnTypes[i] = reader.readByte();
}
}
/**
* Imports all rows from the reader into the database.
* Insertion is done in bulks for efficiency.
*
* @throws IOException if there are any errors while reading
*/
public void importAllRows(DataInputStream reader) throws IOException {
readHeaders(reader);
ContentValues[] valueBulk = new ContentValues[bulkSize];
int numValues = 0;
int numRows = reader.readInt();
int numColumns = columnNames.length;
// For each row
for (int r = 0; r < numRows; r++) {
if (valueBulk[numValues] == null) {
valueBulk[numValues] = new ContentValues(numColumns);
} else {
// Reuse values objects
valueBulk[numValues].clear();
}
// Read the fields bitmap
long fields = reader.readLong();
for (int c = 0; c < numColumns; c++) {
if ((fields & 1) == 1) {
// Field is present, read into values
readOneCell(columnNames[c], columnTypes[c], valueBulk[numValues], reader);
} else if (readNullFields) {
// Field not present but still written, read and discard
readOneCell(columnNames[c], columnTypes[c], null, reader);
}
fields >>= 1;
}
numValues++;
// If we have enough values, flush them as a bulk insertion
if (numValues >= bulkSize) {
doBulkInsert(valueBulk);
numValues = 0;
}
}
// Do a final bulk insert with the leftovers
if (numValues > 0) {
ContentValues[] leftovers = new ContentValues[numValues];
System.arraycopy(valueBulk, 0, leftovers, 0, numValues);
doBulkInsert(leftovers);
}
}
protected void doBulkInsert(ContentValues[] values) {
resolver.bulkInsert(destinationUri, values);
}
/**
* Reads a single cell from the reader.
*
* @param name the name of the column to be read
* @param typeId the type ID of the column to be read
* @param values the {@link ContentValues} object to put the read cell value
* in - if null, the value is just discarded
* @throws IOException if there are any problems while reading
*/
private void readOneCell(String name, byte typeId, ContentValues values, DataInputStream reader) throws IOException {
switch (typeId) {
case ContentTypeIds.BOOLEAN_TYPE_ID: {
boolean value = reader.readBoolean();
if (values != null) { values.put(name, value); }
return;
}
case ContentTypeIds.LONG_TYPE_ID: {
long value = reader.readLong();
if (values != null) { values.put(name, value); }
return;
}
case ContentTypeIds.DOUBLE_TYPE_ID: {
double value = reader.readDouble();
if (values != null) { values.put(name, value); }
return;
}
case ContentTypeIds.FLOAT_TYPE_ID: {
Float value = reader.readFloat();
if (values != null) { values.put(name, value); }
return;
}
case ContentTypeIds.INT_TYPE_ID: {
int value = reader.readInt();
if (values != null) { values.put(name, value); }
return;
}
case ContentTypeIds.STRING_TYPE_ID: {
String value = reader.readUTF();
if (values != null) { values.put(name, value); }
return;
}
default:
throw new IOException("Read unknown type " + typeId);
}
}
}
@@ -0,0 +1,315 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.backup;
import com.google.android.apps.mytracks.MyTracksConstants;
import com.google.android.apps.mytracks.MyTracksSettings;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.TrackPointsColumns;
import com.google.android.apps.mytracks.content.TracksColumns;
import com.google.android.apps.mytracks.content.WaypointsColumns;
import com.google.android.apps.mytracks.io.backup.BackupStateManager.TrackState;
import android.app.backup.BackupAgent;
import android.app.backup.BackupDataInput;
import android.app.backup.BackupDataOutput;
import android.content.ContentResolver;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.os.ParcelFileDescriptor;
import android.util.Log;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* Backup agent used to backup and restore all preferences and tracks.
*
* @author Rodrigo Damazio
*/
public class MyTracksBackupAgent extends BackupAgent {
private static final String PREFERENCES_ENTITY = "prefs";
private static final String TRACK_ENTITY_PREFIX = "track_";
/**
* Initial size of the buffer used to back up a track.
*/
private static final int INITIAL_BUFFER_SIZE = 4096;
/**
* Size of the buffer after which it's flushed into the backup.
*/
private static final int MAX_BUFFER_SIZE = 32768;
/**
* Utilities for accessing the content provider.
*/
private MyTracksProviderUtils providerUtils;
// Database dumpers
protected DatabaseDumper trackDumper;
protected DatabaseDumper waypointDumper;
protected DatabaseDumper pointDumper;
// Database importers
protected DatabaseImporter trackImporter;
protected DatabaseImporter waypointImporter;
protected DatabaseImporter pointImporter;
@Override
public void onCreate() {
providerUtils = MyTracksProviderUtils.Factory.get(this);
}
@Override
public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
ParcelFileDescriptor newState) throws IOException {
// TODO: Trigger backup on: track stop, preference change, track edit
// TODO: How to handle backup while recording? (track may still change)
BackupStateManager backupStateManager = createStateManager(oldState, newState);
BackupStateManager stateManager = backupStateManager;
backupPreferences(data);
backupTracks(data, stateManager);
stateManager.flushNewState();
}
protected BackupStateManager createStateManager(ParcelFileDescriptor oldState,
ParcelFileDescriptor newState) {
return new BackupStateManager(oldState, newState);
}
private void backupPreferences(BackupDataOutput data) throws IOException {
SharedPreferences preferences = this.getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
PreferenceBackupHelper preferenceDumper = createPreferenceBackupHelper();
byte[] dumpedContents = preferenceDumper.exportPreferences(preferences);
data.writeEntityHeader(PREFERENCES_ENTITY, dumpedContents.length);
data.writeEntityData(dumpedContents, dumpedContents.length);
}
protected PreferenceBackupHelper createPreferenceBackupHelper() {
return new PreferenceBackupHelper();
}
/**
* Backs up all the tracks.
*
* @param data the backup data to read from
* @param stateManager the state manager to read from/write to
* @throws IOException if there are any errors while reading or writing
*/
private void backupTracks(BackupDataOutput data, BackupStateManager stateManager) throws IOException {
Cursor tracksCursor = providerUtils.getTracksCursor(null);
if (!tracksCursor.moveToFirst()) {
Log.w(MyTracksConstants.TAG, "Nothing to back up");
return;
}
ensureDumpers();
// For each existing track
Map<Long, TrackState> oldTrackState = stateManager.getOldTrackState();
Set<Long> currentTrackIds = new HashSet<Long>(tracksCursor.getCount());
do {
// Check if not already backed up
Track track = providerUtils.createTrack(tracksCursor);
TrackState oldTrack = oldTrackState.get(track.getId());
if (oldTrack != null && oldTrack.represents(track)) {
// Track is already backed up and hasn't been changed
continue;
}
// Back it up
stateManager.addTrackState(track);
writeTrack(data, track, tracksCursor);
} while (tracksCursor.moveToNext());
tracksCursor.close();
// For each previously-backed-up track, minus those that currently exist
// (=only those that no longer exist)
Set<Long> deletedTrackIds = new HashSet<Long>(oldTrackState.keySet());
deletedTrackIds.removeAll(currentTrackIds);
for (Long trackId : deletedTrackIds) {
// Delete it from the backup
data.writeEntityHeader(TRACK_ENTITY_PREFIX + trackId, -1);
}
}
protected void ensureDumpers() {
if (trackDumper == null) {
trackDumper =
new DatabaseDumper(
TracksColumns.BACKUP_COLUMNS,
TracksColumns.BACKUP_COLUMN_TYPES,
false);
waypointDumper =
new DatabaseDumper(
WaypointsColumns.BACKUP_COLUMNS,
WaypointsColumns.BACKUP_COLUMN_TYPES,
false);
pointDumper =
new DatabaseDumper(
TrackPointsColumns.BACKUP_COLUMNS,
TrackPointsColumns.BACKUP_COLUMN_TYPES,
true); // Keep the size of each entry constant
}
}
/**
* Writes a single track to the backup.
*
* @param data the backup data to write to
* @param track the track to write
* @param tracksCursor the tracks database cursor pointing to the given track
* @throws IOException if there are any problems while writing
*/
private void writeTrack(BackupDataOutput data, Track track, Cursor tracksCursor) throws IOException {
// Write the header
ByteArrayOutputStream bufStream = new ByteArrayOutputStream(INITIAL_BUFFER_SIZE);
DataOutputStream outWriter = new DataOutputStream(bufStream);
// Write track metadata
trackDumper.writeHeaders(tracksCursor, 1, outWriter);
trackDumper.writeOneRow(tracksCursor, outWriter);
// Write waypoints
Cursor waypointsCursor = providerUtils.getWaypointsCursor(track.getId(), -1, -1);
waypointDumper.writeAllRows(waypointsCursor, outWriter);
waypointsCursor.close();
// Read points from the database
Cursor pointsCursor = providerUtils.getLocationsCursor(track.getId(), 0, -1, false);
// Write the header for the points
int numPoints = pointsCursor.getCount();
pointDumper.writeHeaders(pointsCursor, numPoints, outWriter);
// Write the first point to the output and calculate its size
// Since all points have the same size in the output, we can finally predict
// the total size of the track, and write the backup header.
int oldSize = bufStream.size();
pointDumper.writeOneRow(pointsCursor, outWriter);
int newSize = bufStream.size();
int trackPointsSize = numPoints * (newSize - oldSize);
int totalSize = oldSize + trackPointsSize;
// Write out the backup header
data.writeEntityHeader(TRACK_ENTITY_PREFIX + track.getId(), totalSize);
// Write the rest of the points, in chunks
while (pointsCursor.moveToNext()) {
if (bufStream.size() >= MAX_BUFFER_SIZE) {
outWriter.flush();
data.writeEntityData(bufStream.toByteArray(), bufStream.size());
bufStream.reset();
}
pointDumper.writeOneRow(pointsCursor, outWriter);
}
pointsCursor.close();
// Do the last flushing of the buffer
outWriter.flush();
data.writeEntityData(bufStream.toByteArray(), bufStream.size());
}
@Override
public void onRestore(BackupDataInput data, int appVersionCode,
ParcelFileDescriptor newState) throws IOException {
BackupStateManager stateManager = new BackupStateManager(null, newState);
providerUtils.deleteAllTracks();
ensureImporters();
while (data.readNextHeader()) {
String key = data.getKey();
if (key.startsWith(TRACK_ENTITY_PREFIX)) {
Track restoredTrack = restoreTrack(data);
stateManager.addTrackState(restoredTrack);
} else if (key.startsWith(PREFERENCES_ENTITY)) {
restorePreferences(data);
} else {
Log.e(MyTracksConstants.TAG, "Found unknown backup entity: " + key);
data.skipEntityData();
}
}
stateManager.flushNewState();
}
protected void ensureImporters() {
if (trackImporter == null) {
ContentResolver resolver = this.getContentResolver();
trackImporter =
new DatabaseImporter(TracksColumns.CONTENT_URI, resolver, false);
waypointImporter =
new DatabaseImporter(WaypointsColumns.CONTENT_URI, resolver, false);
pointImporter =
new DatabaseImporter(TrackPointsColumns.CONTENT_URI, resolver, true);
}
}
/**
* Restores all preferences from the backup.
*
* @param data the backup data to read from
* @throws IOException if there are any errors while reading
*/
private void restorePreferences(BackupDataInput data) throws IOException {
int dataSize = data.getDataSize();
byte[] dataBuffer = new byte[dataSize];
int read = data.readEntityData(dataBuffer, 0, dataSize);
if (read != dataSize) {
throw new IOException("Failed to read all the preferences data");
}
SharedPreferences preferences = this.getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
PreferenceBackupHelper importer = createPreferenceBackupHelper();
importer.importPreferences(dataBuffer, preferences);
}
/**
* Restores a single track from the backup.
*
* @param data the data to restore from
* @return the restored track
* @throws IOException
*/
private Track restoreTrack(BackupDataInput data) throws IOException {
BackupDataInputStream inputStream = new BackupDataInputStream(data);
DataInputStream reader = new DataInputStream(inputStream);
trackImporter.importAllRows(reader);
waypointImporter.importAllRows(reader);
pointImporter.importAllRows(reader);
// Get the expected track ID from the entity key
String key = data.getKey();
int splitPos = key.lastIndexOf('_');
String trackIdStr = key.substring(splitPos + 1);
long trackId = Long.parseLong(trackIdStr);
return providerUtils.getTrack(trackId);
}
}
@@ -0,0 +1,145 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.backup;
import com.google.android.apps.mytracks.content.ContentTypeIds;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.Map;
/**
* Helper for backing up and restoring shared preferences.
*
* @author Rodrigo Damazio
*/
class PreferenceBackupHelper {
private static final int BUFFER_SIZE = 2048;
/**
* Exports all shared preferences from the given object as a byte array.
*
* @param preferences the preferences to export
* @return the corresponding byte array
* @throws IOException if there are any errors while writing to the byte array
*/
public byte[] exportPreferences(SharedPreferences preferences) throws IOException {
ByteArrayOutputStream bufStream = new ByteArrayOutputStream(BUFFER_SIZE);
DataOutputStream outWriter = new DataOutputStream(bufStream);
Map<String, ?> values = preferences.getAll();
outWriter.writeInt(values.size());
for (Map.Entry<String, ?> entry : values.entrySet()) {
writePreference(entry.getKey(), entry.getValue(), outWriter);
}
outWriter.flush();
return bufStream.toByteArray();
}
/**
* Imports all preferences from the given byte array.
*
* @param data the byte array to read preferences from
* @param preferences the shared preferences to edit
* @return whether the preference change was successful
* @throws IOException if there are any errors while reading
*/
public boolean importPreferences(byte[] data, SharedPreferences preferences) throws IOException {
Editor editor = preferences.edit();
editor.clear();
ByteArrayInputStream bufStream = new ByteArrayInputStream(data);
DataInputStream reader = new DataInputStream(bufStream);
int numPreferneces = reader.readInt();
for (int i = 0; i < numPreferneces; i++) {
String name = reader.readUTF();
byte typeId = reader.readByte();
readAndSetPreference(name, typeId, reader, editor);
}
return editor.commit();
}
/**
* Reads a single preference and sets it into the given editor.
*
* @param name the name of the preference to read
* @param typeId the type ID of the preference to read
* @param reader the reader to read from
* @param editor the editor to set the preference in
* @throws IOException if there are errors while reading
*/
private void readAndSetPreference(String name, byte typeId,
DataInputStream reader, Editor editor) throws IOException {
switch (typeId) {
case ContentTypeIds.BOOLEAN_TYPE_ID:
editor.putBoolean(name, reader.readBoolean());
return;
case ContentTypeIds.LONG_TYPE_ID:
editor.putLong(name, reader.readLong());
return;
case ContentTypeIds.FLOAT_TYPE_ID:
editor.putFloat(name, reader.readFloat());
return;
case ContentTypeIds.INT_TYPE_ID:
editor.putInt(name, reader.readInt());
return;
case ContentTypeIds.STRING_TYPE_ID:
editor.putString(name, reader.readUTF());
return;
}
}
/**
* Writes a single preference.
*
* @param name the name of the preference to write
* @param value the correctly-typed value of the preference
* @param writer the writer to write to
* @throws IOException if there are errors while writing
*/
private void writePreference(String name, Object value, DataOutputStream writer) throws IOException {
writer.writeUTF(name);
if (value instanceof Boolean) {
writer.writeByte(ContentTypeIds.BOOLEAN_TYPE_ID);
writer.writeBoolean((Boolean) value);
} else if (value instanceof Integer) {
writer.writeByte(ContentTypeIds.INT_TYPE_ID);
writer.writeInt((Integer) value);
} else if (value instanceof Long) {
writer.writeByte(ContentTypeIds.LONG_TYPE_ID);
writer.writeLong((Long) value);
} else if (value instanceof Float) {
writer.writeByte(ContentTypeIds.FLOAT_TYPE_ID);
writer.writeFloat((Float) value);
} else if (value instanceof String) {
writer.writeByte(ContentTypeIds.STRING_TYPE_ID);
writer.writeUTF((String) value);
} else {
throw new IllegalArgumentException("Type " + value.getClass().getName() + " not supported");
}
}
}
@@ -0,0 +1,161 @@
package com.google.android.apps.mytracks.io.backup;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.io.backup.BackupStateManager.TrackState;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Map;
import junit.framework.TestCase;
/**
* Tests for {@link BackupStateManager}.
*
* @author Rodrigo Damazio
*/
public class BackupStateManagerTest extends TestCase {
/**
* Testable version of the state manager which doesn't use actual files.
*/
private static class TestableBackupStateManager extends BackupStateManager {
private final byte[] oldStateData;
private final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
public TestableBackupStateManager(byte[] oldStateData) {
super(null, null);
this.oldStateData = oldStateData;
}
@Override
protected OutputStream openStateOutput() {
return outputStream;
}
public ByteArrayOutputStream getOutputStream() {
return outputStream;
}
@Override
protected InputStream openStateInput() {
if (!hasOldState()) {
throw new IllegalStateException("Don't have old state");
}
return new ByteArrayInputStream(oldStateData);
}
@Override
protected boolean hasOldState() {
return oldStateData != null;
}
}
public void testTrackStateRepresents() {
TrackState state = new TrackState(123L, "name", "desc", "cat");
Track stateTrack = makeTrack(123L, "name", "desc", "cat");
assertTrue(state.represents(stateTrack));
}
public void testTrackStateRepresents_wrongId() {
TrackState state = new TrackState(123L, "name", "desc", "cat");
Track stateTrack = makeTrack(456L, "name", "desc", "cat");
assertFalse(state.represents(stateTrack));
}
public void testTrackStateRepresents_wrongName() {
TrackState state = new TrackState(123L, "name", "desc", "cat");
Track stateTrack = makeTrack(123L, "name2", "desc", "cat");
assertFalse(state.represents(stateTrack));
}
public void testTrackStateRepresents_wrongDescription() {
TrackState state = new TrackState(123L, "name", "desc", "cat");
Track stateTrack = makeTrack(123L, "name", "desc2", "cat");
assertFalse(state.represents(stateTrack));
}
public void testTrackStateRepresents_wrongCategory() {
TrackState state = new TrackState(123L, "name", "desc", "cat");
Track stateTrack = makeTrack(123L, "name", "desc", "cat2");
assertFalse(state.represents(stateTrack));
}
public void testGetOldTrackState_empty() {
TestableBackupStateManager stateManager = new TestableBackupStateManager(null);
Map<Long, TrackState> oldState = stateManager.getOldTrackState();
assertTrue(oldState.isEmpty());
}
public void testWriteReadState() throws Exception {
// Initially create a state manager which knows no state
TestableBackupStateManager stateManager = new TestableBackupStateManager(null);
// Add some state to it
stateManager.addTrackState(makeTrack(123L, "Name1"));
stateManager.addTrackState(makeTrack(456L, "Name2"));
stateManager.flushNewState();
// Get the result
byte[] stateData = stateManager.getOutputStream().toByteArray();
// Now create a state manager with the previous serialized state
stateManager = new TestableBackupStateManager(stateData);
// Read its old state and verify it
Map<Long, TrackState> oldState = stateManager.getOldTrackState();
assertEquals(oldState.toString(), 2, oldState.size());
assertTrue(oldState.containsKey(123L));
assertTrue(oldState.containsKey(456L));
assertStateEquals(123L, "Name1", oldState.get(123L));
assertStateEquals(456L, "Name2", oldState.get(456L));
// Write a new, different state
stateManager.addTrackState(makeTrack(123L, "Name1"));
stateManager.addTrackState(makeTrack(987L, "Name3"));
stateManager.flushNewState();
// Get the new result
stateData = stateManager.getOutputStream().toByteArray();
// Create another state manager to read this last state
stateManager = new TestableBackupStateManager(stateData);
// Read and verify it
oldState = stateManager.getOldTrackState();
assertEquals(oldState.toString(), 2, oldState.size());
assertTrue(oldState.containsKey(123L));
assertTrue(oldState.containsKey(987L));
assertStateEquals(123L, "Name1", oldState.get(123L));
assertStateEquals(987L, "Name3", oldState.get(987L));
}
private Track makeTrack(long id, String name) {
return makeTrack(id, name, "Description for " + name, "Category for " + name);
}
private Track makeTrack(long id, String name, String description, String category) {
Track track = new Track();
track.setId(id);
track.setName(name);
track.setDescription(description);
track.setCategory(category);
return track;
}
private void assertStateEquals(long id, String name, TrackState state) {
assertEquals(id, state.trackId);
assertEquals(name, state.name);
assertEquals("Description for " + name, state.description);
assertEquals("Category for " + name, state.category);
}
}
@@ -0,0 +1,212 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.backup;
import com.google.android.apps.mytracks.content.ContentTypeIds;
import android.database.MatrixCursor;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import junit.framework.TestCase;
/**
* Tests for {@link DatabaseDumper}.
*
* @author Rodrigo Damazio
*/
public class DatabaseDumperTest extends TestCase {
private static final String[] COLUMN_NAMES = {
"intCol", "longCol", "floatCol", "doubleCol", "stringCol", "boolCol"
};
private static final byte[] COLUMN_TYPES = {
ContentTypeIds.INT_TYPE_ID, ContentTypeIds.LONG_TYPE_ID,
ContentTypeIds.FLOAT_TYPE_ID, ContentTypeIds.DOUBLE_TYPE_ID,
ContentTypeIds.STRING_TYPE_ID, ContentTypeIds.BOOLEAN_TYPE_ID
};
private static final String[][] FAKE_DATA = {
{ "42", "123456789", "3.1415", "2.72", "lolcat", "1" },
{ null, "123456789", "3.1415", "2.72", "lolcat", "1" },
{ "42", null, "3.1415", "2.72", "lolcat", "1" },
{ "42", "123456789", null, "2.72", "lolcat", "1" },
{ "42", "123456789", "3.1415", null, "lolcat", "1" },
{ "42", "123456789", "3.1415", "2.72", null, "1" },
{ "42", "123456789", "3.1415", "2.72", "lolcat", null },
};
private static final long[] EXPECTED_FIELD_SETS = {
0x3F, 0x3E, 0x3D, 0x3B, 0x37, 0x2F, 0x1F
};
private MatrixCursor cursor;
@Override
protected void setUp() throws Exception {
super.setUp();
// Add fake data to the cursor
cursor = new MatrixCursor(COLUMN_NAMES);
for (String[] row : FAKE_DATA) {
cursor.addRow(row);
}
}
public void testWriteAllRows_noNulls() throws Exception {
testWriteAllRows(false);
}
public void testWriteAllRows_withNulls() throws Exception {
testWriteAllRows(true);
}
private void testWriteAllRows(boolean hasNullFields) throws Exception {
// Dump it
DatabaseDumper dumper = new DatabaseDumper(COLUMN_NAMES, COLUMN_TYPES, hasNullFields);
ByteArrayOutputStream outStream = new ByteArrayOutputStream(1024);
DataOutputStream writer = new DataOutputStream(outStream );
dumper.writeAllRows(cursor, writer);
// Read the results
byte[] result = outStream.toByteArray();
ByteArrayInputStream inputStream = new ByteArrayInputStream(result);
DataInputStream reader = new DataInputStream(inputStream);
// Verify the header
assertHeader(reader);
// Verify the number of rows
assertEquals(FAKE_DATA.length, reader.readInt());
// Verify the row contents
// Row 0
assertEquals(EXPECTED_FIELD_SETS[0], reader.readLong());
assertEquals(42, reader.readInt());
assertEquals(123456789L, reader.readLong());
assertEquals(3.1415f, reader.readFloat());
assertEquals(2.72, reader.readDouble());
assertEquals("lolcat", reader.readUTF());
assertTrue(reader.readBoolean());
// Row 1
assertEquals(EXPECTED_FIELD_SETS[1], reader.readLong());
if (hasNullFields) reader.readInt();
assertEquals(123456789L, reader.readLong());
assertEquals(3.1415f, reader.readFloat());
assertEquals(2.72, reader.readDouble());
assertEquals("lolcat", reader.readUTF());
assertTrue(reader.readBoolean());
// Row 2
assertEquals(EXPECTED_FIELD_SETS[2], reader.readLong());
assertEquals(42, reader.readInt());
if (hasNullFields) reader.readLong();
assertEquals(3.1415f, reader.readFloat());
assertEquals(2.72, reader.readDouble());
assertEquals("lolcat", reader.readUTF());
assertTrue(reader.readBoolean());
// Row 3
assertEquals(EXPECTED_FIELD_SETS[3], reader.readLong());
assertEquals(42, reader.readInt());
assertEquals(123456789L, reader.readLong());
if (hasNullFields) reader.readFloat();
assertEquals(2.72, reader.readDouble());
assertEquals("lolcat", reader.readUTF());
assertTrue(reader.readBoolean());
// Row 4
assertEquals(EXPECTED_FIELD_SETS[4], reader.readLong());
assertEquals(42, reader.readInt());
assertEquals(123456789L, reader.readLong());
assertEquals(3.1415f, reader.readFloat());
if (hasNullFields) reader.readDouble();
assertEquals("lolcat", reader.readUTF());
assertTrue(reader.readBoolean());
// Row 5
assertEquals(EXPECTED_FIELD_SETS[5], reader.readLong());
assertEquals(42, reader.readInt());
assertEquals(123456789L, reader.readLong());
assertEquals(3.1415f, reader.readFloat());
assertEquals(2.72, reader.readDouble());
if (hasNullFields) reader.readUTF();
assertTrue(reader.readBoolean());
// Row 6
assertEquals(EXPECTED_FIELD_SETS[6], reader.readLong());
assertEquals(42, reader.readInt());
assertEquals(123456789L, reader.readLong());
assertEquals(3.1415f, reader.readFloat());
assertEquals(2.72, reader.readDouble());
assertEquals("lolcat", reader.readUTF());
if (hasNullFields) reader.readBoolean();
}
public void testFewerRows() throws Exception {
// Dump only the first two rows
DatabaseDumper dumper = new DatabaseDumper(COLUMN_NAMES, COLUMN_TYPES, false);
ByteArrayOutputStream outStream = new ByteArrayOutputStream(1024);
DataOutputStream writer = new DataOutputStream(outStream);
dumper.writeHeaders(cursor, 2, writer);
cursor.moveToFirst();
dumper.writeOneRow(cursor, writer);
cursor.moveToNext();
dumper.writeOneRow(cursor, writer);
// Read the results
byte[] result = outStream.toByteArray();
ByteArrayInputStream inputStream = new ByteArrayInputStream(result);
DataInputStream reader = new DataInputStream(inputStream);
// Verify the header
assertHeader(reader);
// Verify the number of rows
assertEquals(2, reader.readInt());
// Row 0
assertEquals(EXPECTED_FIELD_SETS[0], reader.readLong());
assertEquals(42, reader.readInt());
assertEquals(123456789L, reader.readLong());
assertEquals(3.1415f, reader.readFloat());
assertEquals(2.72, reader.readDouble());
assertEquals("lolcat", reader.readUTF());
assertTrue(reader.readBoolean());
// Row 1
assertEquals(EXPECTED_FIELD_SETS[1], reader.readLong());
// Null field not read
assertEquals(123456789L, reader.readLong());
assertEquals(3.1415f, reader.readFloat());
assertEquals(2.72, reader.readDouble());
assertEquals("lolcat", reader.readUTF());
assertTrue(reader.readBoolean());
}
private void assertHeader(DataInputStream reader) throws IOException {
assertEquals(6, reader.readInt());
for (int i = 0; i < COLUMN_NAMES.length; i++) {
assertEquals(COLUMN_NAMES[i], reader.readUTF());
assertEquals(COLUMN_TYPES[i], reader.readByte());
}
}
}
@@ -0,0 +1,301 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.backup;
import com.google.android.apps.mytracks.content.ContentTypeIds;
import android.content.ContentValues;
import android.net.Uri;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.util.ArrayList;
import junit.framework.TestCase;
/**
* Tests for {@link DatabaseImporter}.
*
* @author Rodrigo Damazio
*/
public class DatabaseImporterTest extends TestCase {
private static final Uri DESTINATION_URI = Uri.parse("http://www.google.com/");
private static final int TEST_BULK_SIZE = 10;
private ArrayList<ContentValues> insertedValues;
private class TestableDatabaseImporter extends DatabaseImporter {
public TestableDatabaseImporter(boolean readNullFields) {
super(DESTINATION_URI, null, readNullFields, TEST_BULK_SIZE);
}
@Override
protected void doBulkInsert(ContentValues[] values) {
insertedValues.ensureCapacity(insertedValues.size() + values.length);
// We need to make a copy of the values since the objects are re-used
for (ContentValues contentValues : values) {
insertedValues.add(new ContentValues(contentValues));
}
}
}
@Override
protected void setUp() throws Exception {
super.setUp();
insertedValues = new ArrayList<ContentValues>();
}
public void testImportAllRows() throws Exception {
testImportAllRows(false);
}
public void testImportAllRows_readNullFields() throws Exception {
testImportAllRows(true);
}
private void testImportAllRows(boolean readNullFields) throws Exception {
// Create a fake data stream to be read
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(1024);
DataOutputStream writer = new DataOutputStream(outputStream);
// Add the header
writer.writeInt(6);
writer.writeUTF("col1");
writer.writeByte(ContentTypeIds.INT_TYPE_ID);
writer.writeUTF("col2");
writer.writeByte(ContentTypeIds.BOOLEAN_TYPE_ID);
writer.writeUTF("col3");
writer.writeByte(ContentTypeIds.STRING_TYPE_ID);
writer.writeUTF("col4");
writer.writeByte(ContentTypeIds.FLOAT_TYPE_ID);
writer.writeUTF("col5");
writer.writeByte(ContentTypeIds.DOUBLE_TYPE_ID);
writer.writeUTF("col6");
writer.writeByte(ContentTypeIds.LONG_TYPE_ID);
// Add the number of rows
writer.writeInt(2);
// Add a row with all fields present
writer.writeLong(0x3F);
writer.writeInt(42);
writer.writeBoolean(true);
writer.writeUTF("lolcat");
writer.writeFloat(3.1415f);
writer.writeDouble(2.72);
writer.writeLong(123456789L);
// Add a row with some missing fields
writer.writeLong(0x15);
writer.writeInt(42);
if (readNullFields) writer.writeBoolean(false);
writer.writeUTF("lolcat");
if (readNullFields) writer.writeFloat(0.0f);
writer.writeDouble(2.72);
if (readNullFields) writer.writeLong(0L);
writer.flush();
// Do the importing
DatabaseImporter importer = new TestableDatabaseImporter(readNullFields);
byte[] dataBytes = outputStream.toByteArray();
importer.importAllRows(new DataInputStream(new ByteArrayInputStream(dataBytes)));
assertEquals(2, insertedValues.size());
// Verify the first row
ContentValues value = insertedValues.get(0);
assertEquals(value.toString(), 6, value.size());
assertValue(42, "col1", value);
assertValue(true, "col2", value);
assertValue("lolcat", "col3", value);
assertValue(3.1415f, "col4", value);
assertValue(2.72, "col5", value);
assertValue(123456789L, "col6", value);
// Verify the second row
value = insertedValues.get(1);
assertEquals(value.toString(), 3, value.size());
assertValue(42, "col1", value);
assertValue("lolcat", "col3", value);
assertValue(2.72, "col5", value);
}
public void testImportAllRows_noRows() throws Exception {
// Create a fake data stream to be read
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(1024);
DataOutputStream writer = new DataOutputStream(outputStream);
// Add the header
writer.writeInt(6);
writer.writeUTF("col1");
writer.writeByte(ContentTypeIds.INT_TYPE_ID);
writer.writeUTF("col2");
writer.writeByte(ContentTypeIds.BOOLEAN_TYPE_ID);
writer.writeUTF("col3");
writer.writeByte(ContentTypeIds.STRING_TYPE_ID);
writer.writeUTF("col4");
writer.writeByte(ContentTypeIds.FLOAT_TYPE_ID);
writer.writeUTF("col5");
writer.writeByte(ContentTypeIds.DOUBLE_TYPE_ID);
writer.writeUTF("col6");
writer.writeByte(ContentTypeIds.LONG_TYPE_ID);
// Add the number of rows
writer.writeInt(0);
writer.flush();
// Do the importing
DatabaseImporter importer = new TestableDatabaseImporter(false);
byte[] dataBytes = outputStream.toByteArray();
importer.importAllRows(new DataInputStream(new ByteArrayInputStream(dataBytes)));
assertTrue(insertedValues.isEmpty());
}
public void testImportAllRows_emptyRows() throws Exception {
testImportAllRowsWithEmptyRows(false);
}
public void testImportAllRows_emptyRowsWithNulls() throws Exception {
testImportAllRowsWithEmptyRows(true);
}
private void testImportAllRowsWithEmptyRows(boolean readNullFields) throws Exception {
// Create a fake data stream to be read
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(1024);
DataOutputStream writer = new DataOutputStream(outputStream);
// Add the header
writer.writeInt(6);
writer.writeUTF("col1");
writer.writeByte(ContentTypeIds.INT_TYPE_ID);
writer.writeUTF("col2");
writer.writeByte(ContentTypeIds.BOOLEAN_TYPE_ID);
writer.writeUTF("col3");
writer.writeByte(ContentTypeIds.STRING_TYPE_ID);
writer.writeUTF("col4");
writer.writeByte(ContentTypeIds.FLOAT_TYPE_ID);
writer.writeUTF("col5");
writer.writeByte(ContentTypeIds.DOUBLE_TYPE_ID);
writer.writeUTF("col6");
writer.writeByte(ContentTypeIds.LONG_TYPE_ID);
// Add the number of rows
writer.writeInt(3);
// Add 2 rows with no fields
for (int i = 0; i < 2; i++) {
writer.writeLong(0);
if (readNullFields) {
writer.writeInt(0);
writer.writeBoolean(false);
writer.writeUTF("");
writer.writeFloat(0.0f);
writer.writeDouble(0.0);
writer.writeLong(0L);
}
}
// Add a row with some missing fields
writer.writeLong(0x15);
writer.writeInt(42);
if (readNullFields) writer.writeBoolean(false);
writer.writeUTF("lolcat");
if (readNullFields) writer.writeFloat(0.0f);
writer.writeDouble(2.72);
if (readNullFields) writer.writeLong(0L);
writer.flush();
// Do the importing
DatabaseImporter importer = new TestableDatabaseImporter(readNullFields);
byte[] dataBytes = outputStream.toByteArray();
importer.importAllRows(new DataInputStream(new ByteArrayInputStream(dataBytes)));
assertEquals(insertedValues.toString(), 3, insertedValues.size());
ContentValues value = insertedValues.get(0);
assertEquals(value.toString(), 0, value.size());
value = insertedValues.get(1);
assertEquals(value.toString(), 0, value.size());
// Verify the third row (only one with values)
value = insertedValues.get(2);
assertEquals(value.toString(), 3, value.size());
assertFalse(value.containsKey("col2"));
assertFalse(value.containsKey("col4"));
assertFalse(value.containsKey("col6"));
assertValue(42, "col1", value);
assertValue("lolcat", "col3", value);
assertValue(2.72, "col5", value);
}
public void testImportAllRows_bulks() throws Exception {
// Create a fake data stream to be read
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(1024);
DataOutputStream writer = new DataOutputStream(outputStream);
// Add the header
writer.writeInt(2);
writer.writeUTF("col1");
writer.writeByte(ContentTypeIds.INT_TYPE_ID);
writer.writeUTF("col2");
writer.writeByte(ContentTypeIds.STRING_TYPE_ID);
// Add lots of rows (so the insertions are split in multiple bulks)
int numRows = TEST_BULK_SIZE * 5 / 2;
writer.writeInt(numRows);
for (int i = 0; i < numRows; i++) {
writer.writeLong(3);
writer.writeInt(i);
writer.writeUTF(Integer.toString(i * 2));
}
writer.flush();
// Do the importing
DatabaseImporter importer = new TestableDatabaseImporter(false);
byte[] dataBytes = outputStream.toByteArray();
importer.importAllRows(new DataInputStream(new ByteArrayInputStream(dataBytes)));
// Verify the rows
assertEquals(numRows, insertedValues.size());
for (int i = 0; i < numRows; i++) {
ContentValues value = insertedValues.get(i);
assertEquals(value.toString(), 2, value.size());
assertValue(i, "col1", value);
assertValue(Integer.toString(i * 2), "col2", value);
}
}
private <T> void assertValue(T expectedValue, String name, ContentValues values) {
@SuppressWarnings("unchecked")
T value = (T) values.get(name);
assertNotNull(value);
assertEquals(expectedValue, value);
}
}
@@ -0,0 +1,112 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.backup;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils.Factory;
import com.google.android.apps.mytracks.testing.TestingProviderUtilsFactory;
import com.google.android.testing.mocking.AndroidMock;
import com.google.android.testing.mocking.UsesMocks;
import android.os.ParcelFileDescriptor;
import junit.framework.TestCase;
/**
* Tests for {@link MyTracksBackupAgent}.
*
* @author Rodrigo Damazio
*/
public class MyTracksBackupAgentTest extends TestCase {
// Database dumpers
private DatabaseDumper trackDumper;
private DatabaseDumper waypointDumper;
private DatabaseDumper pointDumper;
// Database importers
private DatabaseImporter trackImporter;
private DatabaseImporter waypointImporter;
private DatabaseImporter pointImporter;
// Other dependencies
private BackupStateManager stateManager;
private PreferenceBackupHelper preferencesHelper;
private MyTracksProviderUtils providerUtils;
private Factory oldProviderUtilsFactory;
/**
* Testable version of the backup agent, with dependencies mocked out.
*/
private class TestableBackupAgent extends MyTracksBackupAgent {
@Override
protected BackupStateManager createStateManager(
ParcelFileDescriptor oldState, ParcelFileDescriptor newState) {
return stateManager;
}
@Override
protected PreferenceBackupHelper createPreferenceBackupHelper() {
return preferencesHelper;
}
@Override
protected void ensureDumpers() {
super.trackDumper = MyTracksBackupAgentTest.this.trackDumper;
super.waypointDumper = MyTracksBackupAgentTest.this.waypointDumper;
super.pointDumper = MyTracksBackupAgentTest.this.pointDumper;
}
@Override
protected void ensureImporters() {
super.trackImporter = MyTracksBackupAgentTest.this.trackImporter;
super.waypointImporter = MyTracksBackupAgentTest.this.waypointImporter;
super.pointImporter = MyTracksBackupAgentTest.this.pointImporter;
}
}
@UsesMocks({ DatabaseDumper.class, DatabaseImporter.class })
@Override
protected void setUp() throws Exception {
super.setUp();
trackDumper = AndroidMock.createMock(DatabaseDumper.class);
waypointDumper = AndroidMock.createMock(DatabaseDumper.class);
pointDumper = AndroidMock.createMock(DatabaseDumper.class);
trackImporter = AndroidMock.createMock(DatabaseImporter.class);
waypointImporter = AndroidMock.createMock(DatabaseImporter.class);
pointImporter = AndroidMock.createMock(DatabaseImporter.class);
stateManager = AndroidMock.createMock(BackupStateManager.class);
preferencesHelper = AndroidMock.createMock(PreferenceBackupHelper.class);
providerUtils = AndroidMock.createMock(MyTracksProviderUtils.class);
oldProviderUtilsFactory =
TestingProviderUtilsFactory.installWithInstance(providerUtils);
}
@Override
protected void tearDown() throws Exception {
TestingProviderUtilsFactory.restoreOldFactory(oldProviderUtilsFactory);
super.tearDown();
}
public void testOnBackup() {
fail("Not yet implemented");
}
public void testOnRestore() {
fail("Not yet implemented");
}
}
@@ -0,0 +1,197 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.backup;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import java.util.HashMap;
import java.util.Map;
import junit.framework.TestCase;
/**
* Tests for {@link PreferenceBackupHelper}.
*
* @author Rodrigo Damazio
*/
public class PreferenceBackupHelperTest extends TestCase {
private Map<String, ?> preferenceValues;
private SharedPreferences preferences;
private PreferenceBackupHelper preferenceBackupHelper;
/**
* Mock shared preferences editor which does not persist state.
*/
private class MockPreferenceEditor implements SharedPreferences.Editor {
private Map<String, Object> newPreferences = new HashMap<String, Object>(preferenceValues);
@Override
public Editor clear() {
newPreferences.clear();
return this;
}
@Override
public boolean commit() {
preferenceValues = newPreferences;
return true;
}
@Override
public Editor putBoolean(String key, boolean value) {
return put(key, value);
}
@Override
public Editor putFloat(String key, float value) {
return put(key, value);
}
@Override
public Editor putInt(String key, int value) {
return put(key, value);
}
@Override
public Editor putLong(String key, long value) {
return put(key, value);
}
@Override
public Editor putString(String key, String value) {
return put(key, value);
}
private <T> Editor put(String key, T value) {
newPreferences.put(key, value);
return this;
}
@Override
public Editor remove(String key) {
newPreferences.remove(key);
return this;
}
}
/**
* Mock shared preferences which does not persist state.
*/
private class MockPreferences implements SharedPreferences {
@Override
public boolean contains(String key) {
return preferenceValues.containsKey(key);
}
@Override
public Editor edit() {
return new MockPreferenceEditor();
}
@Override
public Map<String, ?> getAll() {
return preferenceValues;
}
@Override
public boolean getBoolean(String key, boolean defValue) {
return get(key, defValue);
}
@Override
public float getFloat(String key, float defValue) {
return get(key, defValue);
}
@Override
public int getInt(String key, int defValue) {
return get(key, defValue);
}
@Override
public long getLong(String key, long defValue) {
return get(key, defValue);
}
@Override
public String getString(String key, String defValue) {
return get(key, defValue);
}
@Override
public void registerOnSharedPreferenceChangeListener(
OnSharedPreferenceChangeListener listener) {
throw new UnsupportedOperationException();
}
@Override
public void unregisterOnSharedPreferenceChangeListener(
OnSharedPreferenceChangeListener listener) {
throw new UnsupportedOperationException();
}
@SuppressWarnings("unchecked")
private <T> T get(String key, T defValue) {
Object value = preferenceValues.get(key);
if (value == null) return defValue;
return (T) value;
}
}
@Override
protected void setUp() throws Exception {
super.setUp();
preferenceValues = new HashMap<String, Object>();
preferences = new MockPreferences();
preferenceBackupHelper = new PreferenceBackupHelper();
}
public void testExportImportPreferences() throws Exception {
// Populate with some initial values
Editor editor = preferences.edit();
editor.clear();
editor.putBoolean("bool1", true);
editor.putBoolean("bool2", false);
editor.putFloat("flt1", 3.14f);
editor.putInt("int1", 42);
editor.putLong("long1", 123456789L);
editor.putString("str1", "lolcat");
editor.commit();
// Export it
byte[] exported = preferenceBackupHelper.exportPreferences(preferences);
// Mess with the previous values
editor = preferences.edit();
editor.clear();
editor.putString("str2", "Shouldn't be there after restore");
editor.putBoolean("bool2", true);
editor.commit();
// Import it back
preferenceBackupHelper.importPreferences(exported, preferences);
assertFalse(preferences.contains("str2"));
assertTrue(preferences.getBoolean("bool1", false));
assertFalse(preferences.getBoolean("bool2", true));
assertEquals(3.14f, preferences.getFloat("flt1", 0.0f));
assertEquals(42, preferences.getInt("int1", 0));
assertEquals(123456789L, preferences.getLong("long1", 0));
assertEquals("lolcat", preferences.getString("str1", ""));
}
}