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
@@ -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");
}
}
}