forked from upstream-mirrors/OpenTracks
merge heads
This commit is contained in:
+43
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright 2011 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.annotation.TargetApi;
|
||||
import android.app.backup.BackupManager;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
/**
|
||||
* Implementation of {@link BackupPreferencesListener} that calls the
|
||||
* {@link BackupManager}. <br>
|
||||
* For API Level 8 or higher.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
@TargetApi(8)
|
||||
public class Api8BackupPreferencesListener implements BackupPreferencesListener {
|
||||
|
||||
private final BackupManager backupManager;
|
||||
|
||||
public Api8BackupPreferencesListener(Context context) {
|
||||
this.backupManager = new BackupManager(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
|
||||
backupManager.dataChanged();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright 2012 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.util.DialogUtils;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Dialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
import android.widget.Toast;
|
||||
|
||||
/**
|
||||
* Activity to backup data to the SD card.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class BackupActivity extends Activity {
|
||||
|
||||
private static final int DIALOG_PROGRESS_ID = 0;
|
||||
|
||||
private BackupAsyncTask backupAsyncTask;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
Object retained = getLastNonConfigurationInstance();
|
||||
if (retained instanceof BackupAsyncTask) {
|
||||
backupAsyncTask = (BackupAsyncTask) retained;
|
||||
backupAsyncTask.setActivity(this);
|
||||
} else {
|
||||
backupAsyncTask = new BackupAsyncTask(this);
|
||||
backupAsyncTask.execute();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object onRetainNonConfigurationInstance() {
|
||||
backupAsyncTask.setActivity(null);
|
||||
return backupAsyncTask;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Dialog onCreateDialog(int id) {
|
||||
if (id != DIALOG_PROGRESS_ID) {
|
||||
return null;
|
||||
}
|
||||
return DialogUtils.createSpinnerProgressDialog(
|
||||
this, R.string.settings_backup_now_progress_message, new DialogInterface.OnCancelListener() {
|
||||
@Override
|
||||
public void onCancel(DialogInterface dialog) {
|
||||
finish();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes when the associated AsyncTask completes.
|
||||
*
|
||||
* @param success true if the AsyncTask is successful
|
||||
* @param messageId message id to display to user
|
||||
*/
|
||||
public void onAsyncTaskCompleted(boolean success, int messageId) {
|
||||
removeDialog(DIALOG_PROGRESS_ID);
|
||||
Toast.makeText(this, messageId, success ? Toast.LENGTH_SHORT : Toast.LENGTH_LONG).show();
|
||||
finish();
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the progress dialog.
|
||||
*/
|
||||
public void showProgressDialog() {
|
||||
showDialog(DIALOG_PROGRESS_ID);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* Copyright 2012 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.util.FileUtils;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.os.AsyncTask;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* AsyncTask to backup data to the SD card.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class BackupAsyncTask extends AsyncTask<Void, Integer, Boolean> {
|
||||
|
||||
private static final String TAG = BackupAsyncTask.class.getSimpleName();
|
||||
|
||||
private BackupActivity backupActivity;
|
||||
private final ExternalFileBackup externalFileBackup;
|
||||
|
||||
// true if the AsyncTask result is success
|
||||
private boolean success;
|
||||
|
||||
// true if the AsyncTask has completed
|
||||
private boolean completed;
|
||||
|
||||
// message id to return to the activity
|
||||
private int messageId;
|
||||
|
||||
/**
|
||||
* Creates an AsyncTask.
|
||||
*
|
||||
* @param backupActivity the activity currently associated with this
|
||||
* AsyncTask
|
||||
*/
|
||||
public BackupAsyncTask(BackupActivity backupActivity) {
|
||||
this.backupActivity = backupActivity;
|
||||
this.externalFileBackup = new ExternalFileBackup(backupActivity);
|
||||
success = false;
|
||||
completed = false;
|
||||
messageId = R.string.sd_card_error_write_file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current {@link BackupActivity} associated with this AyncTask.
|
||||
*
|
||||
* @param activity the current {@link BackupActivity}, can be null
|
||||
*/
|
||||
public void setActivity(BackupActivity activity) {
|
||||
this.backupActivity = activity;
|
||||
if (completed && backupActivity != null) {
|
||||
backupActivity.onAsyncTaskCompleted(success, messageId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
if (backupActivity != null) {
|
||||
backupActivity.showProgressDialog();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Boolean doInBackground(Void... params) {
|
||||
if (!FileUtils.isSdCardAvailable()) {
|
||||
messageId = R.string.sd_card_error_no_storage;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!externalFileBackup.isBackupsDirectoryAvailable(true)) {
|
||||
messageId = R.string.sd_card_error_create_dir;
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
externalFileBackup.writeToDefaultFile();
|
||||
messageId = R.string.sd_card_success_write_file;
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
Log.d(TAG, "IO exception", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Boolean result) {
|
||||
success = result;
|
||||
completed = true;
|
||||
if (backupActivity != null) {
|
||||
backupActivity.onAsyncTaskCompleted(success, messageId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* 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 static com.google.android.apps.mytracks.content.ContentTypeIds.*;
|
||||
import com.google.android.apps.mytracks.content.TrackPointsColumns;
|
||||
import com.google.android.apps.mytracks.content.TracksColumns;
|
||||
import com.google.android.apps.mytracks.content.WaypointsColumns;
|
||||
|
||||
public class BackupColumns {
|
||||
/** Columns that go into the backup. */
|
||||
public static final String[] POINTS_BACKUP_COLUMNS =
|
||||
{ TrackPointsColumns._ID, TrackPointsColumns.TRACKID, TrackPointsColumns.LATITUDE,
|
||||
TrackPointsColumns.LONGITUDE, TrackPointsColumns.ALTITUDE, TrackPointsColumns.BEARING,
|
||||
TrackPointsColumns.TIME, TrackPointsColumns.ACCURACY, TrackPointsColumns.SPEED,
|
||||
TrackPointsColumns.SENSOR };
|
||||
public static final byte[] POINTS_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, BLOB_TYPE_ID };
|
||||
|
||||
public static final String[] TRACKS_BACKUP_COLUMNS = {
|
||||
TracksColumns._ID, TracksColumns.NAME, TracksColumns.DESCRIPTION, TracksColumns.CATEGORY,
|
||||
TracksColumns.STARTID, TracksColumns.STOPID, TracksColumns.STARTTIME, TracksColumns.STOPTIME,
|
||||
TracksColumns.NUMPOINTS, TracksColumns.TOTALDISTANCE, TracksColumns.TOTALTIME,
|
||||
TracksColumns.MOVINGTIME, TracksColumns.AVGSPEED, TracksColumns.AVGMOVINGSPEED,
|
||||
TracksColumns.MAXSPEED, TracksColumns.MINELEVATION, TracksColumns.MAXELEVATION,
|
||||
TracksColumns.ELEVATIONGAIN, TracksColumns.MINGRADE, TracksColumns.MAXGRADE,
|
||||
TracksColumns.MINLAT, TracksColumns.MAXLAT, TracksColumns.MINLON, TracksColumns.MAXLON,
|
||||
TracksColumns.MAPID, TracksColumns.TABLEID};
|
||||
public static final byte[] TRACKS_BACKUP_COLUMN_TYPES = {
|
||||
LONG_TYPE_ID, STRING_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, STRING_TYPE_ID,
|
||||
STRING_TYPE_ID};
|
||||
|
||||
public static final String[] WAYPOINTS_BACKUP_COLUMNS = {
|
||||
WaypointsColumns._ID, WaypointsColumns.TRACKID, WaypointsColumns.NAME,
|
||||
WaypointsColumns.DESCRIPTION, WaypointsColumns.CATEGORY, WaypointsColumns.ICON,
|
||||
WaypointsColumns.TYPE, WaypointsColumns.LENGTH, WaypointsColumns.DURATION,
|
||||
WaypointsColumns.STARTTIME, WaypointsColumns.STARTID, WaypointsColumns.STOPID,
|
||||
WaypointsColumns.LATITUDE, WaypointsColumns.LONGITUDE, WaypointsColumns.ALTITUDE,
|
||||
WaypointsColumns.BEARING, WaypointsColumns.TIME, WaypointsColumns.ACCURACY,
|
||||
WaypointsColumns.SPEED, WaypointsColumns.TOTALDISTANCE, WaypointsColumns.TOTALTIME,
|
||||
WaypointsColumns.MOVINGTIME, WaypointsColumns.AVGSPEED, WaypointsColumns.AVGMOVINGSPEED,
|
||||
WaypointsColumns.MAXSPEED, WaypointsColumns.MINELEVATION, WaypointsColumns.MAXELEVATION,
|
||||
WaypointsColumns.ELEVATIONGAIN, WaypointsColumns.MINGRADE, WaypointsColumns.MAXGRADE };
|
||||
public static final byte[] WAYPOINTS_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 };
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* 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.OnSharedPreferenceChangeListener;
|
||||
|
||||
/**
|
||||
* Shared preferences listener which notifies the backup system about new data
|
||||
* being available for backup.
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public interface BackupPreferencesListener extends OnSharedPreferenceChangeListener {
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
/*
|
||||
* 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
|
||||
* @param writer the output to write to
|
||||
* @throws IOException if there are errors while writing
|
||||
*/
|
||||
public void writeHeaders(Cursor cursor, int numRows, DataOutputStream writer)
|
||||
throws IOException {
|
||||
initializeCachedValues(cursor);
|
||||
writeQueryMetadata(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
|
||||
* @param writer the output to write to
|
||||
* @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 numRows the number of rows that will be dumped
|
||||
* @param writer the output to write to
|
||||
* @throws IOException if there are any errors while writing
|
||||
*/
|
||||
private void writeQueryMetadata(
|
||||
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 columnTypeId the type of the column to be read
|
||||
* @param cursor the cursor to read from
|
||||
* @param writer the output to write to
|
||||
* @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;
|
||||
case ContentTypeIds.BLOB_TYPE_ID: {
|
||||
byte[] blob = cursor.getBlob(columnIdx);
|
||||
writer.writeInt(blob.length);
|
||||
writer.write(blob);
|
||||
return;
|
||||
}
|
||||
default:
|
||||
throw new IllegalArgumentException(
|
||||
"Type " + columnTypeId + " not supported");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a dummy cell value to the output.
|
||||
*
|
||||
* @param columnTypeId 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;
|
||||
case ContentTypeIds.BLOB_TYPE_ID:
|
||||
writer.writeInt(0);
|
||||
return;
|
||||
default:
|
||||
throw new IllegalArgumentException(
|
||||
"Type " + columnTypeId + " not supported");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 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;
|
||||
}
|
||||
case ContentTypeIds.BLOB_TYPE_ID: {
|
||||
int blobLength = reader.readInt();
|
||||
if (blobLength != 0) {
|
||||
byte[] blob = new byte[blobLength];
|
||||
int readBytes = reader.read(blob, 0, blobLength);
|
||||
if (readBytes != blobLength) {
|
||||
throw new IOException(String.format(
|
||||
"Short read on column %s; expected %d bytes, read %d",
|
||||
name, blobLength, readBytes));
|
||||
}
|
||||
|
||||
if (values != null) {
|
||||
values.put(name, blob);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
default:
|
||||
throw new IOException("Read unknown type " + typeId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
/*
|
||||
* 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 static com.google.android.apps.mytracks.Constants.TAG;
|
||||
|
||||
import com.google.android.apps.mytracks.Constants;
|
||||
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.util.FileUtils;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.database.Cursor;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.TimeZone;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipFile;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
/**
|
||||
* Handler for writing or reading single-file backups.
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
class ExternalFileBackup {
|
||||
// Filename format - in UTC
|
||||
private static final SimpleDateFormat BACKUP_FILENAME_FORMAT =
|
||||
new SimpleDateFormat("'backup-'yyyy-MM-dd_HH-mm-ss'.zip'");
|
||||
static {
|
||||
BACKUP_FILENAME_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||
}
|
||||
|
||||
private static final String BACKUPS_SUBDIR = "backups";
|
||||
private static final int BACKUP_FORMAT_VERSION = 1;
|
||||
private static final String ZIP_ENTRY_NAME =
|
||||
"backup.mytracks.v" + BACKUP_FORMAT_VERSION;
|
||||
private static final int COMPRESSION_LEVEL = 8;
|
||||
|
||||
private final Context context;
|
||||
|
||||
public ExternalFileBackup(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the backups directory is (or can be made) available.
|
||||
*
|
||||
* @param create whether to try creating the directory if it doesn't exist
|
||||
*/
|
||||
public boolean isBackupsDirectoryAvailable(boolean create) {
|
||||
return getBackupsDirectory(create) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the backup directory, or null if not available.
|
||||
*
|
||||
* @param create whether to try creating the directory if it doesn't exist
|
||||
*/
|
||||
private File getBackupsDirectory(boolean create) {
|
||||
String dirName = FileUtils.buildExternalDirectoryPath(BACKUPS_SUBDIR);
|
||||
final File dir = new File(dirName);
|
||||
Log.d(Constants.TAG, "Dir: " + dir.getAbsolutePath());
|
||||
if (create) {
|
||||
// Try to create - if that fails, return null
|
||||
return FileUtils.ensureDirectoryExists(dir) ? dir : null;
|
||||
} else {
|
||||
// Return it if it already exists, otherwise return null
|
||||
return dir.isDirectory() ? dir : null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of available backups to be restored.
|
||||
*/
|
||||
public Date[] getAvailableBackups() {
|
||||
File dir = getBackupsDirectory(false);
|
||||
if (dir == null) { return null; }
|
||||
String[] fileNames = dir.list();
|
||||
|
||||
List<Date> backupDates = new ArrayList<Date>(fileNames.length);
|
||||
for (int i = 0; i < fileNames.length; i++) {
|
||||
String fileName = fileNames[i];
|
||||
try {
|
||||
backupDates.add(BACKUP_FILENAME_FORMAT.parse(fileName));
|
||||
} catch (ParseException e) {
|
||||
// Not a backup file, ignore
|
||||
}
|
||||
}
|
||||
|
||||
return backupDates.toArray(new Date[backupDates.size()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the backup to the default file.
|
||||
*/
|
||||
public void writeToDefaultFile() throws IOException {
|
||||
writeToFile(getFileForDate(new Date()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores the backup from the given date.
|
||||
*/
|
||||
public void restoreFromDate(Date when) throws IOException {
|
||||
restoreFromFile(getFileForDate(when));
|
||||
}
|
||||
|
||||
/**
|
||||
* Produces the proper file descriptor for the given backup date.
|
||||
*/
|
||||
private File getFileForDate(Date when) {
|
||||
File dir = getBackupsDirectory(false);
|
||||
String fileName = BACKUP_FILENAME_FORMAT.format(when);
|
||||
File file = new File(dir, fileName);
|
||||
return file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronously writes a backup to the given file.
|
||||
*/
|
||||
private void writeToFile(File outputFile) throws IOException {
|
||||
Log.d(Constants.TAG,
|
||||
"Writing backup to file " + outputFile.getAbsolutePath());
|
||||
|
||||
// Create all the auxiliary classes that will do the writing
|
||||
PreferenceBackupHelper preferencesHelper = new PreferenceBackupHelper();
|
||||
DatabaseDumper trackDumper = new DatabaseDumper(
|
||||
BackupColumns.TRACKS_BACKUP_COLUMNS,
|
||||
BackupColumns.TRACKS_BACKUP_COLUMN_TYPES,
|
||||
false);
|
||||
DatabaseDumper waypointDumper = new DatabaseDumper(
|
||||
BackupColumns.WAYPOINTS_BACKUP_COLUMNS,
|
||||
BackupColumns.WAYPOINTS_BACKUP_COLUMN_TYPES,
|
||||
false);
|
||||
DatabaseDumper pointDumper = new DatabaseDumper(
|
||||
BackupColumns.POINTS_BACKUP_COLUMNS,
|
||||
BackupColumns.POINTS_BACKUP_COLUMN_TYPES,
|
||||
false);
|
||||
|
||||
// Open the target for writing
|
||||
FileOutputStream outputStream = new FileOutputStream(outputFile);
|
||||
ZipOutputStream compressedStream = new ZipOutputStream(outputStream);
|
||||
compressedStream.setLevel(COMPRESSION_LEVEL);
|
||||
compressedStream.putNextEntry(new ZipEntry(ZIP_ENTRY_NAME));
|
||||
DataOutputStream outWriter = new DataOutputStream(compressedStream);
|
||||
|
||||
try {
|
||||
// Dump the entire contents of each table
|
||||
ContentResolver contentResolver = context.getContentResolver();
|
||||
Cursor tracksCursor = contentResolver.query(
|
||||
TracksColumns.CONTENT_URI, null, null, null, null);
|
||||
try {
|
||||
trackDumper.writeAllRows(tracksCursor, outWriter);
|
||||
} finally {
|
||||
tracksCursor.close();
|
||||
}
|
||||
|
||||
Cursor waypointsCursor = contentResolver.query(
|
||||
WaypointsColumns.CONTENT_URI, null, null, null, null);
|
||||
try {
|
||||
waypointDumper.writeAllRows(waypointsCursor, outWriter);
|
||||
} finally {
|
||||
waypointsCursor.close();
|
||||
}
|
||||
|
||||
Cursor pointsCursor = contentResolver.query(
|
||||
TrackPointsColumns.CONTENT_URI, null, null, null, null);
|
||||
try {
|
||||
pointDumper.writeAllRows(pointsCursor, outWriter);
|
||||
} finally {
|
||||
pointsCursor.close();
|
||||
}
|
||||
|
||||
// Dump preferences
|
||||
SharedPreferences preferences = context.getSharedPreferences(
|
||||
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
|
||||
preferencesHelper.exportPreferences(preferences, outWriter);
|
||||
} catch (IOException e) {
|
||||
// We tried to delete the partially created file, but do nothing
|
||||
// if that also fails.
|
||||
if (!outputFile.delete()) {
|
||||
Log.w(TAG, "Failed to delete file " + outputFile.getAbsolutePath());
|
||||
}
|
||||
|
||||
throw e;
|
||||
} finally {
|
||||
compressedStream.closeEntry();
|
||||
compressedStream.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronously restores the backup from the given file.
|
||||
*/
|
||||
private void restoreFromFile(File inputFile) throws IOException {
|
||||
Log.d(Constants.TAG,
|
||||
"Restoring from file " + inputFile.getAbsolutePath());
|
||||
|
||||
PreferenceBackupHelper preferencesHelper = new PreferenceBackupHelper();
|
||||
ContentResolver resolver = context.getContentResolver();
|
||||
DatabaseImporter trackImporter =
|
||||
new DatabaseImporter(TracksColumns.CONTENT_URI, resolver, false);
|
||||
DatabaseImporter waypointImporter =
|
||||
new DatabaseImporter(WaypointsColumns.CONTENT_URI, resolver, false);
|
||||
DatabaseImporter pointImporter =
|
||||
new DatabaseImporter(TrackPointsColumns.CONTENT_URI, resolver, false);
|
||||
|
||||
ZipFile zipFile = new ZipFile(inputFile, ZipFile.OPEN_READ);
|
||||
ZipEntry zipEntry = zipFile.getEntry(ZIP_ENTRY_NAME);
|
||||
if (zipEntry == null) {
|
||||
throw new IOException("Invalid backup ZIP file");
|
||||
}
|
||||
|
||||
InputStream compressedStream = zipFile.getInputStream(zipEntry);
|
||||
DataInputStream reader = new DataInputStream(compressedStream);
|
||||
|
||||
try {
|
||||
// Delete all previous contents of the tables and preferences.
|
||||
resolver.delete(TracksColumns.CONTENT_URI, null, null);
|
||||
resolver.delete(TrackPointsColumns.CONTENT_URI, null, null);
|
||||
resolver.delete(WaypointsColumns.CONTENT_URI, null, null);
|
||||
|
||||
// Import the new contents of each table
|
||||
trackImporter.importAllRows(reader);
|
||||
waypointImporter.importAllRows(reader);
|
||||
pointImporter.importAllRows(reader);
|
||||
|
||||
// Restore preferences
|
||||
SharedPreferences preferences = context.getSharedPreferences(
|
||||
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
|
||||
preferencesHelper.importPreferences(reader, preferences);
|
||||
} finally {
|
||||
compressedStream.close();
|
||||
zipFile.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* 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.Constants;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.backup.BackupAgent;
|
||||
import android.app.backup.BackupDataInput;
|
||||
import android.app.backup.BackupDataOutput;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Backup agent used to backup and restore all preferences.
|
||||
* We use a regular {@link BackupAgent} instead of the convenient helpers in
|
||||
* order to be future-proof (assuming we'll want to back up tracks later).
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
@TargetApi(8)
|
||||
public class MyTracksBackupAgent extends BackupAgent {
|
||||
private static final String PREFERENCES_ENTITY = "prefs";
|
||||
|
||||
@Override
|
||||
public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
|
||||
ParcelFileDescriptor newState) throws IOException {
|
||||
Log.i(Constants.TAG, "Performing backup");
|
||||
SharedPreferences preferences = this.getSharedPreferences(
|
||||
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
|
||||
|
||||
backupPreferences(data, preferences);
|
||||
Log.i(Constants.TAG, "Backup complete");
|
||||
}
|
||||
|
||||
private void backupPreferences(BackupDataOutput data,
|
||||
SharedPreferences preferences) throws IOException {
|
||||
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();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRestore(BackupDataInput data, int appVersionCode,
|
||||
ParcelFileDescriptor newState) throws IOException {
|
||||
Log.i(Constants.TAG, "Restoring from backup");
|
||||
while (data.readNextHeader()) {
|
||||
String key = data.getKey();
|
||||
Log.d(Constants.TAG, "Restoring entity " + key);
|
||||
if (key.equals(PREFERENCES_ENTITY)) {
|
||||
restorePreferences(data);
|
||||
} else {
|
||||
Log.e(Constants.TAG, "Found unknown backup entity: " + key);
|
||||
data.skipEntityData();
|
||||
}
|
||||
}
|
||||
Log.i(Constants.TAG, "Done restoring from backup");
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(
|
||||
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
|
||||
PreferenceBackupHelper importer = createPreferenceBackupHelper();
|
||||
importer.importPreferences(dataBuffer, preferences);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
/*
|
||||
* 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 com.google.android.apps.mytracks.util.ApiAdapterFactory;
|
||||
|
||||
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);
|
||||
exportPreferences(preferences, outWriter);
|
||||
|
||||
return bufStream.toByteArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports all shared preferences from the given object into the given output
|
||||
* stream.
|
||||
*
|
||||
* @param preferences the preferences to export
|
||||
* @param outWriter the stream to write them to
|
||||
* @throws IOException if there are any errors while writing the output
|
||||
*/
|
||||
public void exportPreferences(
|
||||
SharedPreferences preferences,
|
||||
DataOutputStream outWriter) throws IOException {
|
||||
Map<String, ?> values = preferences.getAll();
|
||||
|
||||
outWriter.writeInt(values.size());
|
||||
for (Map.Entry<String, ?> entry : values.entrySet()) {
|
||||
writePreference(entry.getKey(), entry.getValue(), outWriter);
|
||||
}
|
||||
outWriter.flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports all preferences from the given byte array.
|
||||
*
|
||||
* @param data the byte array to read preferences from
|
||||
* @param preferences the shared preferences to edit
|
||||
* @throws IOException if there are any errors while reading
|
||||
*/
|
||||
public void importPreferences(byte[] data, SharedPreferences preferences)
|
||||
throws IOException {
|
||||
ByteArrayInputStream bufStream = new ByteArrayInputStream(data);
|
||||
DataInputStream reader = new DataInputStream(bufStream);
|
||||
|
||||
importPreferences(reader, preferences);
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports all preferences from the given stream.
|
||||
*
|
||||
* @param reader the stream to read from
|
||||
* @param preferences the shared preferences to edit
|
||||
* @throws IOException if there are any errors while reading
|
||||
*/
|
||||
public void importPreferences(DataInputStream reader,
|
||||
SharedPreferences preferences) throws IOException {
|
||||
Editor editor = preferences.edit();
|
||||
editor.clear();
|
||||
|
||||
int numPreferences = reader.readInt();
|
||||
for (int i = 0; i < numPreferences; i++) {
|
||||
String name = reader.readUTF();
|
||||
byte typeId = reader.readByte();
|
||||
readAndSetPreference(name, typeId, reader, editor);
|
||||
}
|
||||
ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a single preference and sets it into the given editor.
|
||||
*
|
||||
* @param name the name of the preference to read
|
||||
* @param typeId the type ID of the preference to read
|
||||
* @param reader the reader to read from
|
||||
* @param editor the editor to set the preference in
|
||||
* @throws IOException if there are errors while reading
|
||||
*/
|
||||
private void readAndSetPreference(String name, byte typeId,
|
||||
DataInputStream reader, Editor editor) throws IOException {
|
||||
switch (typeId) {
|
||||
case ContentTypeIds.BOOLEAN_TYPE_ID:
|
||||
editor.putBoolean(name, reader.readBoolean());
|
||||
return;
|
||||
case ContentTypeIds.LONG_TYPE_ID:
|
||||
editor.putLong(name, reader.readLong());
|
||||
return;
|
||||
case ContentTypeIds.FLOAT_TYPE_ID:
|
||||
editor.putFloat(name, reader.readFloat());
|
||||
return;
|
||||
case ContentTypeIds.INT_TYPE_ID:
|
||||
editor.putInt(name, reader.readInt());
|
||||
return;
|
||||
case ContentTypeIds.STRING_TYPE_ID:
|
||||
editor.putString(name, reader.readUTF());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a single preference.
|
||||
*
|
||||
* @param name the name of the preference to write
|
||||
* @param value the correctly-typed value of the preference
|
||||
* @param writer the writer to write to
|
||||
* @throws IOException if there are errors while writing
|
||||
*/
|
||||
private void writePreference(String name, Object value, DataOutputStream writer)
|
||||
throws IOException {
|
||||
writer.writeUTF(name);
|
||||
|
||||
if (value instanceof Boolean) {
|
||||
writer.writeByte(ContentTypeIds.BOOLEAN_TYPE_ID);
|
||||
writer.writeBoolean((Boolean) value);
|
||||
} else if (value instanceof Integer) {
|
||||
writer.writeByte(ContentTypeIds.INT_TYPE_ID);
|
||||
writer.writeInt((Integer) value);
|
||||
} else if (value instanceof Long) {
|
||||
writer.writeByte(ContentTypeIds.LONG_TYPE_ID);
|
||||
writer.writeLong((Long) value);
|
||||
} else if (value instanceof Float) {
|
||||
writer.writeByte(ContentTypeIds.FLOAT_TYPE_ID);
|
||||
writer.writeFloat((Float) value);
|
||||
} else if (value instanceof String) {
|
||||
writer.writeByte(ContentTypeIds.STRING_TYPE_ID);
|
||||
writer.writeUTF((String) value);
|
||||
} else {
|
||||
throw new IllegalArgumentException(
|
||||
"Type " + value.getClass().getName() + " not supported");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Copyright 2012 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.TrackListActivity;
|
||||
import com.google.android.apps.mytracks.util.DialogUtils;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Dialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* An activity to restore data from the SD card.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class RestoreActivity extends Activity {
|
||||
|
||||
public static final String EXTRA_DATE = "date";
|
||||
|
||||
private static final String TAG = RestoreActivity.class.getSimpleName();
|
||||
private static final int DIALOG_PROGRESS_ID = 0;
|
||||
|
||||
private RestoreAsyncTask restoreAsyncTask;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
Object retained = getLastNonConfigurationInstance();
|
||||
if (retained instanceof RestoreAsyncTask) {
|
||||
restoreAsyncTask = (RestoreAsyncTask) retained;
|
||||
restoreAsyncTask.setActivity(this);
|
||||
} else {
|
||||
long date = getIntent().getLongExtra(EXTRA_DATE, -1L);
|
||||
if (date == -1L) {
|
||||
Log.d(TAG, "Invalid date");
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
restoreAsyncTask = new RestoreAsyncTask(this, new Date(date));
|
||||
restoreAsyncTask.execute();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object onRetainNonConfigurationInstance() {
|
||||
restoreAsyncTask.setActivity(null);
|
||||
return restoreAsyncTask;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Dialog onCreateDialog(int id) {
|
||||
if (id != DIALOG_PROGRESS_ID) {
|
||||
return null;
|
||||
}
|
||||
return DialogUtils.createSpinnerProgressDialog(this,
|
||||
R.string.settings_backup_restore_progress_message, new DialogInterface.OnCancelListener() {
|
||||
@Override
|
||||
public void onCancel(DialogInterface dialog) {
|
||||
finish();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes when the associated AsyncTask completes.
|
||||
*
|
||||
* @param success true if the AsyncTask is successful
|
||||
* @param messageId message id to display to user
|
||||
*/
|
||||
public void onAsyncTaskCompleted(boolean success, int messageId) {
|
||||
removeDialog(DIALOG_PROGRESS_ID);
|
||||
Toast.makeText(this, messageId, success ? Toast.LENGTH_SHORT : Toast.LENGTH_LONG).show();
|
||||
startActivity(new Intent(this, TrackListActivity.class)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK));
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the progress dialog.
|
||||
*/
|
||||
public void showProgressDialog() {
|
||||
showDialog(DIALOG_PROGRESS_ID);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* Copyright 2012 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.maps.mytracks.R;
|
||||
|
||||
import android.os.AsyncTask;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* AsyncTask to restore data from the SD card.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class RestoreAsyncTask extends AsyncTask<Void, Integer, Boolean> {
|
||||
|
||||
private static final String TAG = RestoreAsyncTask.class.getSimpleName();
|
||||
|
||||
private RestoreActivity restoreActivity;
|
||||
private final Date date;
|
||||
private final ExternalFileBackup externalFileBackup;
|
||||
|
||||
// true if the AsyncTask result is success
|
||||
private boolean success;
|
||||
|
||||
// true if the AsyncTask has completed
|
||||
private boolean completed;
|
||||
|
||||
// message id to return to the activity
|
||||
private int messageId;
|
||||
|
||||
/**
|
||||
* Creates an AsyncTask.
|
||||
*
|
||||
* @param restoreActivity the activity currently associated with this
|
||||
* AsyncTask
|
||||
* @param date the date to retore from
|
||||
*/
|
||||
public RestoreAsyncTask(RestoreActivity restoreActivity, Date date) {
|
||||
this.restoreActivity = restoreActivity;
|
||||
this.date = date;
|
||||
this.externalFileBackup = new ExternalFileBackup(restoreActivity);
|
||||
success = false;
|
||||
completed = false;
|
||||
messageId = R.string.sd_card_error_read_file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current {@link RestoreActivity} associated with this AyncTask.
|
||||
*
|
||||
* @param activity the current {@link RestoreActivity}, can be null
|
||||
*/
|
||||
public void setActivity(RestoreActivity activity) {
|
||||
this.restoreActivity = activity;
|
||||
if (completed && restoreActivity != null) {
|
||||
restoreActivity.onAsyncTaskCompleted(success, messageId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
if (restoreActivity != null) {
|
||||
restoreActivity.showProgressDialog();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Boolean doInBackground(Void... params) {
|
||||
try {
|
||||
externalFileBackup.restoreFromDate(date);
|
||||
messageId = R.string.sd_card_success_read_file;
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
Log.d(TAG, "IO exception", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Boolean result) {
|
||||
success = result;
|
||||
completed = true;
|
||||
if (restoreActivity != null) {
|
||||
restoreActivity.onAsyncTaskCompleted(success, messageId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* Copyright 2012 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.util.FileUtils;
|
||||
import com.google.android.apps.mytracks.util.StringUtils;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.app.Dialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.widget.Toast;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* An activity to choose a date to restore from.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class RestoreChooserActivity extends Activity {
|
||||
|
||||
private static final Comparator<Date> REVERSE_DATE_ORDER = new Comparator<Date>() {
|
||||
@Override
|
||||
public int compare(Date s1, Date s2) {
|
||||
return s2.compareTo(s1);
|
||||
}
|
||||
};
|
||||
private static final int DIALOG_CHOOSER_ID = 0;
|
||||
|
||||
private Date[] backupDates;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
ExternalFileBackup externalFileBackup = new ExternalFileBackup(this);
|
||||
|
||||
// Get the list of existing backups
|
||||
if (!FileUtils.isSdCardAvailable()) {
|
||||
Toast.makeText(this, R.string.sd_card_error_no_storage, Toast.LENGTH_LONG).show();
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!externalFileBackup.isBackupsDirectoryAvailable(false)) {
|
||||
Toast.makeText(this, R.string.settings_backup_restore_no_backup, Toast.LENGTH_LONG).show();
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
backupDates = externalFileBackup.getAvailableBackups();
|
||||
if (backupDates == null || backupDates.length == 0) {
|
||||
Toast.makeText(this, R.string.settings_backup_restore_no_backup, Toast.LENGTH_LONG).show();
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
if (backupDates.length == 1) {
|
||||
startActivity(new Intent(this, RestoreActivity.class)
|
||||
.putExtra(RestoreActivity.EXTRA_DATE, backupDates[0].getTime()));
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
Arrays.sort(backupDates, REVERSE_DATE_ORDER);
|
||||
showDialog(DIALOG_CHOOSER_ID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Dialog onCreateDialog(int id) {
|
||||
if (id != DIALOG_CHOOSER_ID) {
|
||||
return null;
|
||||
}
|
||||
String items[] = new String[backupDates.length];
|
||||
for (int i = 0; i < backupDates.length; i++) {
|
||||
items[i] = StringUtils.formatDateTime(this, backupDates[i].getTime());
|
||||
}
|
||||
return new AlertDialog.Builder(this)
|
||||
.setCancelable(true)
|
||||
.setItems(items, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
startActivity(new Intent(RestoreChooserActivity.this, RestoreActivity.class).putExtra(
|
||||
RestoreActivity.EXTRA_DATE, backupDates[which].getTime()));
|
||||
finish();
|
||||
}
|
||||
})
|
||||
.setOnCancelListener(new DialogInterface.OnCancelListener() {
|
||||
@Override
|
||||
public void onCancel(DialogInterface dialog) {
|
||||
finish();
|
||||
}
|
||||
})
|
||||
.setTitle(R.string.settings_backup_restore_select_title)
|
||||
.create();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user