mirror of
https://codeberg.org/OpenTracksApp/OpenTracks.git
synced 2026-10-02 09:33:06 +02:00
Adding backup facilities. TODO: some tests, calling to trigger new automatic backup on Froyo, manual backup to SD option
This commit is contained in:
@@ -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.
|
||||
*
|
||||
|
||||
+4
-16
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
+161
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
+301
@@ -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);
|
||||
}
|
||||
}
|
||||
+112
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
+197
@@ -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", ""));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user