forked from upstream-mirrors/OpenTracks
- remove arrays.xml
- clean up keys.xml - clean up do_not_translate.xml
This commit is contained in:
@@ -0,0 +1,244 @@
|
||||
/*
|
||||
* 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 com.google.android.apps.mytracks.MyTracks;
|
||||
import com.google.android.apps.mytracks.util.FileUtils;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.app.AlertDialog.Builder;
|
||||
import android.app.Dialog;
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.DialogInterface.OnClickListener;
|
||||
import android.content.Intent;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* Helper which shows a UI for writing or restoring a backup,
|
||||
* and calls the appropriate handler for actually executing those
|
||||
* operations.
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public class BackupActivityHelper {
|
||||
|
||||
// Since the user sees this format, we use the local timezone
|
||||
private static final SimpleDateFormat DISPLAY_BACKUP_FORMAT =
|
||||
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
private static final Comparator<Date> REVERSE_DATE_ORDER =
|
||||
new Comparator<Date>() {
|
||||
@Override
|
||||
public int compare(Date s1, Date s2) {
|
||||
return s2.compareTo(s1);
|
||||
}
|
||||
};
|
||||
|
||||
private final FileUtils fileUtils;
|
||||
private final ExternalFileBackup backup;
|
||||
private final Activity activity;
|
||||
|
||||
public BackupActivityHelper(Activity activity) {
|
||||
this.activity = activity;
|
||||
this.fileUtils = new FileUtils();
|
||||
this.backup = new ExternalFileBackup(activity, fileUtils);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a full backup to the default file.
|
||||
* This shows the results to the user.
|
||||
*/
|
||||
public void writeBackup() {
|
||||
if (!fileUtils.isSdCardAvailable()) {
|
||||
showToast(R.string.io_no_external_storage_found);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!backup.isBackupsDirectoryAvailable(true)) {
|
||||
showToast(R.string.io_create_dir_failed);
|
||||
return;
|
||||
}
|
||||
|
||||
final ProgressDialog progressDialog = ProgressDialog.show(
|
||||
activity,
|
||||
activity.getString(R.string.progress_title),
|
||||
activity.getString(R.string.backup_write_progress_message),
|
||||
true);
|
||||
|
||||
// Do the writing in another thread
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
backup.writeToDefaultFile();
|
||||
showToast(R.string.io_write_finished);
|
||||
} catch (IOException e) {
|
||||
Log.e(Constants.TAG, "Failed to write backup", e);
|
||||
showToast(R.string.io_write_failed);
|
||||
} finally {
|
||||
dismissDialog(progressDialog);
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores a full backup from the SD card.
|
||||
* The user will be given a choice of which backup to restore as well as a
|
||||
* confirmation dialog.
|
||||
*/
|
||||
public void restoreBackup() {
|
||||
// Get the list of existing backups
|
||||
if (!fileUtils.isSdCardAvailable()) {
|
||||
showToast(R.string.io_no_external_storage_found);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!backup.isBackupsDirectoryAvailable(false)) {
|
||||
showToast(R.string.no_backups);
|
||||
return;
|
||||
}
|
||||
|
||||
final Date[] backupDates = backup.getAvailableBackups();
|
||||
if (backupDates == null || backupDates.length == 0) {
|
||||
showToast(R.string.no_backups);
|
||||
return;
|
||||
}
|
||||
Arrays.sort(backupDates, REVERSE_DATE_ORDER);
|
||||
|
||||
// Show a confirmation dialog
|
||||
Builder confirmationDialogBuilder = new AlertDialog.Builder(activity);
|
||||
confirmationDialogBuilder.setMessage(R.string.restore_overwrites_warning);
|
||||
confirmationDialogBuilder.setCancelable(true);
|
||||
confirmationDialogBuilder.setPositiveButton(android.R.string.yes,
|
||||
new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
pickBackupForRestore(backupDates);
|
||||
}
|
||||
});
|
||||
confirmationDialogBuilder.setNegativeButton(android.R.string.no, null);
|
||||
confirmationDialogBuilder.create().show();
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a backup list for the user to pick, then restores it.
|
||||
*
|
||||
* @param backupDates the list of available backup files
|
||||
*/
|
||||
private void pickBackupForRestore(final Date[] backupDates) {
|
||||
if (backupDates.length == 1) {
|
||||
// Only one choice, don't bother showing the list
|
||||
restoreFromDateAsync(backupDates[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Make a user-visible version of the backup filenames
|
||||
final String backupDateStrs[] = new String[backupDates.length];
|
||||
for (int i = 0; i < backupDates.length; i++) {
|
||||
backupDateStrs[i] = DISPLAY_BACKUP_FORMAT.format(backupDates[i]);
|
||||
}
|
||||
|
||||
// Show a dialog for the user to pick which backup to restore
|
||||
Builder dialogBuilder = new AlertDialog.Builder(activity);
|
||||
dialogBuilder.setCancelable(true);
|
||||
dialogBuilder.setTitle(R.string.select_backup_to_restore);
|
||||
dialogBuilder.setItems(backupDateStrs, new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
// User picked to restore this one
|
||||
restoreFromDateAsync(backupDates[which]);
|
||||
}
|
||||
});
|
||||
dialogBuilder.create().show();
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a progress dialog, then starts restoring the backup asynchronously.
|
||||
*
|
||||
* @param date the date
|
||||
*/
|
||||
private void restoreFromDateAsync(final Date date) {
|
||||
// Show a progress dialog
|
||||
ProgressDialog.show(
|
||||
activity,
|
||||
activity.getString(R.string.progress_title),
|
||||
activity.getString(R.string.backup_import_progress_message),
|
||||
true);
|
||||
|
||||
// Do the actual importing in another thread (don't block the UI)
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
backup.restoreFromDate(date);
|
||||
showToast(R.string.io_read_finished);
|
||||
} catch (IOException e) {
|
||||
Log.e(Constants.TAG, "Failed to restore backup", e);
|
||||
showToast(R.string.io_read_failed);
|
||||
} finally {
|
||||
// Data may have been restored, "reboot" the app to catch it
|
||||
restartApplication();
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Restarts My Tracks completely.
|
||||
* This forces any modified data to be re-read.
|
||||
*/
|
||||
private void restartApplication() {
|
||||
Intent intent = new Intent(activity, MyTracks.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
activity.startActivity(intent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a toast with the given contents.
|
||||
*/
|
||||
private void showToast(final int resId) {
|
||||
activity.runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Toast.makeText(activity, resId, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely dismisses the given dialog.
|
||||
*/
|
||||
private void dismissDialog(final Dialog dialog) {
|
||||
activity.runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
dialog.dismiss();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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.app.backup.BackupManager;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
/**
|
||||
* Implementation of {@link BackupPreferencesListener} that calls the
|
||||
* {@link BackupManager}.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class BackupPreferencesListenerImpl implements BackupPreferencesListener {
|
||||
|
||||
private final BackupManager backupManager;
|
||||
|
||||
public BackupPreferencesListenerImpl(Context context) {
|
||||
this.backupManager = new BackupManager(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
|
||||
backupManager.dataChanged();
|
||||
}
|
||||
}
|
||||
@@ -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,266 @@
|
||||
/*
|
||||
* 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;
|
||||
private final FileUtils fileUtils;
|
||||
|
||||
public ExternalFileBackup(Context context, FileUtils fileUtils) {
|
||||
this.context = context;
|
||||
this.fileUtils = fileUtils;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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, 0);
|
||||
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, 0);
|
||||
preferencesHelper.importPreferences(reader, preferences);
|
||||
} finally {
|
||||
compressedStream.close();
|
||||
zipFile.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* 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.app.backup.BackupAgent;
|
||||
import android.app.backup.BackupDataInput;
|
||||
import android.app.backup.BackupDataOutput;
|
||||
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
|
||||
*/
|
||||
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, 0);
|
||||
|
||||
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, 0);
|
||||
PreferenceBackupHelper importer = createPreferenceBackupHelper();
|
||||
importer.importPreferences(dataBuffer, preferences);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
/*
|
||||
* 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);
|
||||
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
|
||||
* @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 {
|
||||
ByteArrayInputStream bufStream = new ByteArrayInputStream(data);
|
||||
DataInputStream reader = new DataInputStream(bufStream);
|
||||
|
||||
return importPreferences(reader, preferences);
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports all preferences from the given stream.
|
||||
*
|
||||
* @param reader the stream to read 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(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);
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user