package org.dodgybits.shuffle.android.preference.activity; import java.io.File; import java.io.FileInputStream; import java.io.FilenameFilter; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.dodgybits.android.shuffle.R; import org.dodgybits.shuffle.android.core.activity.flurry.FlurryEnabledActivity; import org.dodgybits.shuffle.android.core.model.Context; import org.dodgybits.shuffle.android.core.model.Id; import org.dodgybits.shuffle.android.core.model.Project; import org.dodgybits.shuffle.android.core.model.Task; import org.dodgybits.shuffle.android.core.model.persistence.EntityPersister; import org.dodgybits.shuffle.android.core.model.protocol.ContextProtocolTranslator; import org.dodgybits.shuffle.android.core.model.protocol.EntityDirectory; import org.dodgybits.shuffle.android.core.model.protocol.HashEntityDirectory; import org.dodgybits.shuffle.android.core.model.protocol.ProjectProtocolTranslator; import org.dodgybits.shuffle.android.core.model.protocol.TaskProtocolTranslator; import org.dodgybits.shuffle.android.core.util.StringUtils; import org.dodgybits.shuffle.android.core.view.AlertUtils; import org.dodgybits.shuffle.android.persistence.provider.ContextProvider; import org.dodgybits.shuffle.android.persistence.provider.ProjectProvider; import org.dodgybits.shuffle.android.preference.view.Progress; import org.dodgybits.shuffle.dto.ShuffleProtos.Catalogue; import roboguice.inject.InjectView; import android.database.Cursor; import android.os.AsyncTask; import android.os.Bundle; import android.os.Environment; import android.text.TextUtils; import android.util.Log; import android.view.View; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.ProgressBar; import android.widget.Spinner; import android.widget.TextView; import com.google.inject.Inject; public class PreferencesRestoreBackupActivity extends FlurryEnabledActivity implements View.OnClickListener { private static final String RESTORE_BACKUP_STATE = "restoreBackupState"; private static final String cTag = "PreferencesRestoreBackupActivity"; private enum State {SELECTING, IN_PROGRESS, COMPLETE, ERROR}; private State mState = State.SELECTING; @InjectView(R.id.filename) Spinner mFileSpinner; @InjectView(R.id.saveButton) Button mRestoreButton; @InjectView(R.id.discardButton) Button mCancelButton; @InjectView(R.id.progress_horizontal) ProgressBar mProgressBar; @InjectView(R.id.progress_label) TextView mProgressText; @Inject EntityPersister<Context> mContextPersister; @Inject EntityPersister<Project> mProjectPersister; @Inject EntityPersister<Task> mTaskPersister; private AsyncTask<?, ?, ?> mTask; @Override protected void onCreate(Bundle icicle) { Log.d(cTag, "onCreate+"); super.onCreate(icicle); setDefaultKeyMode(DEFAULT_KEYS_SHORTCUT); setContentView(R.layout.backup_restore); findViewsAndAddListeners(); onUpdateState(); } @Override protected void onResume() { super.onResume(); setupFileSpinner(); } private void findViewsAndAddListeners() { mRestoreButton.setText(R.string.restore_button_title); mRestoreButton.setOnClickListener(this); mCancelButton.setOnClickListener(this); // save progress text when we switch orientation mProgressText.setFreezesText(true); } private void setupFileSpinner() { String storage_state = Environment.getExternalStorageState(); if (! Environment.MEDIA_MOUNTED.equals(storage_state)) { String message = getString(R.string.warning_media_not_mounted, storage_state); Log.e(cTag, message); AlertUtils.showWarning(this, message); setState(State.COMPLETE); return; } File dir = Environment.getExternalStorageDirectory(); String[] files = dir.list(new FilenameFilter() { @Override public boolean accept(File dir, String filename) { // don't show hidden files return !filename.startsWith("."); } }); if (files == null || files.length == 0) { String message = getString(R.string.warning_no_files, storage_state); Log.e(cTag, message); AlertUtils.showWarning(this, message); setState(State.COMPLETE); return; } ArrayAdapter<String> adapter = new ArrayAdapter<String>( this, android.R.layout.simple_list_item_1, files); adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); mFileSpinner.setAdapter(adapter); // select most recent file ending in .bak int selectedIndex = 0; long lastModified = Long.MIN_VALUE; for (int i = 0; i < files.length; i++) { String filename = files[i]; File f = new File(dir, filename); if (f.getName().endsWith(".bak") && f.lastModified() > lastModified) { selectedIndex = i; lastModified = f.lastModified(); } } mFileSpinner.setSelection(selectedIndex); } public void onClick(View v) { switch (v.getId()) { case R.id.saveButton: setState(State.IN_PROGRESS); restoreBackup(); break; case R.id.discardButton: finish(); break; } } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putString(RESTORE_BACKUP_STATE, mState.name()); } @Override protected void onRestoreInstanceState(Bundle savedInstanceState) { super.onRestoreInstanceState(savedInstanceState); String stateName = savedInstanceState.getString(RESTORE_BACKUP_STATE); if (stateName == null) { stateName = State.SELECTING.name(); } setState(State.valueOf(stateName)); } @Override protected void onDestroy() { super.onDestroy(); if (mTask != null && mTask.getStatus() != AsyncTask.Status.RUNNING) { mTask.cancel(true); } } private void setState(State value) { if (mState != value) { mState = value; onUpdateState(); } } private void onUpdateState() { switch (mState) { case SELECTING: setButtonsEnabled(true); mFileSpinner.setEnabled(true); mProgressBar.setVisibility(View.INVISIBLE); mProgressText.setVisibility(View.INVISIBLE); mCancelButton.setText(R.string.cancel_button_title); break; case IN_PROGRESS: setButtonsEnabled(false); mFileSpinner.setEnabled(false); mProgressBar.setProgress(0); mProgressBar.setVisibility(View.VISIBLE); mProgressText.setVisibility(View.VISIBLE); break; case COMPLETE: setButtonsEnabled(true); mFileSpinner.setEnabled(false); mProgressBar.setVisibility(View.VISIBLE); mProgressText.setVisibility(View.VISIBLE); mRestoreButton.setVisibility(View.GONE); mCancelButton.setText(R.string.ok_button_title); break; case ERROR: setButtonsEnabled(true); mFileSpinner.setEnabled(true); mProgressBar.setVisibility(View.VISIBLE); mProgressText.setVisibility(View.VISIBLE); mRestoreButton.setVisibility(View.VISIBLE); mCancelButton.setText(R.string.cancel_button_title); break; } } private void setButtonsEnabled(boolean enabled) { mRestoreButton.setEnabled(enabled); mCancelButton.setEnabled(enabled); } private void restoreBackup() { String filename = mFileSpinner.getSelectedItem().toString(); mTask = new RestoreBackupTask().execute(filename); } private class RestoreBackupTask extends AsyncTask<String, Progress, Void> { public Void doInBackground(String... filename) { try { String message = getString(R.string.status_reading_backup); Log.d(cTag, message); publishProgress(Progress.createProgress(5, message)); File dir = Environment.getExternalStorageDirectory(); File backupFile = new File(dir, filename[0]); FileInputStream in = new FileInputStream(backupFile); Catalogue catalogue = Catalogue.parseFrom(in); in.close(); Log.d(cTag, catalogue.toString()); EntityDirectory<Context> contextLocator = addContexts(catalogue.getContextList(), 10, 20); EntityDirectory<Project> projectLocator = addProjects(catalogue.getProjectList(), contextLocator, 20, 30); addTasks(catalogue.getTaskList(), contextLocator, projectLocator, 30, 100); message = getString(R.string.status_restore_complete); publishProgress(Progress.createProgress(100, message)); } catch (Exception e) { String message = getString(R.string.warning_restore_failed, e.getMessage()); reportError(message); } return null; } private EntityDirectory<Context> addContexts( List<org.dodgybits.shuffle.dto.ShuffleProtos.Context> protoContexts, int progressStart, int progressEnd) { ContextProtocolTranslator translator = new ContextProtocolTranslator(); Set<String> allContextNames = new HashSet<String>(); for (org.dodgybits.shuffle.dto.ShuffleProtos.Context protoContext : protoContexts) { allContextNames.add(protoContext.getName()); } Map<String,Context> existingContexts = fetchContextsByName(allContextNames); // build up the locator and list of new contacts HashEntityDirectory<Context> contextLocator = new HashEntityDirectory<Context>(); List<Context> newContexts = new ArrayList<Context>(); Set<String> newContextNames = new HashSet<String>(); int i = 0; int total = protoContexts.size(); String type = getString(R.string.context_name); for (org.dodgybits.shuffle.dto.ShuffleProtos.Context protoContext : protoContexts) { String contextName = protoContext.getName(); Context context = existingContexts.get(contextName); if (context != null) { Log.d(cTag, "Context " + contextName + " already exists - skipping."); } else { Log.d(cTag, "Context " + contextName + " new - adding."); context = translator.fromMessage(protoContext); newContexts.add(context); newContextNames.add(contextName); } Id contextId = Id.create(protoContext.getId()); contextLocator.addItem(contextId, contextName, context); String text = getString(R.string.restore_progress, type, contextName); int percent = calculatePercent(progressStart, progressEnd, ++i, total); publishProgress(Progress.createProgress(percent, text)); } mContextPersister.bulkInsert(newContexts); // we need to fetch all the newly created contexts to retrieve their new ids // and update the locator accordingly Map<String,Context> savedContexts = fetchContextsByName(newContextNames); for (String contextName : newContextNames) { Context savedContext = savedContexts.get(contextName); Context restoredContext = contextLocator.findByName(contextName); contextLocator.addItem(restoredContext.getLocalId(), contextName, savedContext); } return contextLocator; } /** * Attempts to match existing contexts against a list of context names. * * @param names names to match * @return any matching contexts in a Map, keyed on the context name */ private Map<String,Context> fetchContextsByName(Collection<String> names) { Map<String,Context> contexts = new HashMap<String,Context>(); if (names.size() > 0) { String params = StringUtils.repeat(names.size(), "?", ","); String[] paramValues = names.toArray(new String[0]); Cursor cursor = getContentResolver().query( ContextProvider.Contexts.CONTENT_URI, ContextProvider.Contexts.cFullProjection, ContextProvider.Contexts.NAME + " IN (" + params + ")", paramValues, ContextProvider.Contexts.NAME + " ASC"); while (cursor.moveToNext()) { Context context = mContextPersister.read(cursor); contexts.put(context.getName(), context); } cursor.close(); } return contexts; } private EntityDirectory<Project> addProjects( List<org.dodgybits.shuffle.dto.ShuffleProtos.Project> protoProjects, EntityDirectory<Context> contextLocator, int progressStart, int progressEnd) { ProjectProtocolTranslator translator = new ProjectProtocolTranslator(contextLocator); Set<String> allProjectNames = new HashSet<String>(); for (org.dodgybits.shuffle.dto.ShuffleProtos.Project protoProject : protoProjects) { allProjectNames.add(protoProject.getName()); } Map<String,Project> existingProjects = fetchProjectsByName(allProjectNames); // build up the locator and list of new projects HashEntityDirectory<Project> projectLocator = new HashEntityDirectory<Project>(); List<Project> newProjects = new ArrayList<Project>(); Set<String> newProjectNames = new HashSet<String>(); int i = 0; int total = protoProjects.size(); String type = getString(R.string.project_name); for (org.dodgybits.shuffle.dto.ShuffleProtos.Project protoProject : protoProjects) { String projectName = protoProject.getName(); Project project = existingProjects.get(projectName); if (project != null) { Log.d(cTag, "Project " + projectName + " already exists - skipping."); } else { Log.d(cTag, "Project " + projectName + " new - adding."); project = translator.fromMessage(protoProject); newProjects.add(project); newProjectNames.add(projectName); } Id projectId = Id.create(protoProject.getId()); projectLocator.addItem(projectId, projectName, project); String text = getString(R.string.restore_progress, type, projectName); int percent = calculatePercent(progressStart, progressEnd, ++i, total); publishProgress(Progress.createProgress(percent, text)); } mProjectPersister.bulkInsert(newProjects); // we need to fetch all the newly created contexts to retrieve their new ids // and update the locator accordingly Map<String,Project> savedProjects = fetchProjectsByName(newProjectNames); for (String projectName : newProjectNames) { Project savedProject = savedProjects.get(projectName); Project restoredProject = projectLocator.findByName(projectName); projectLocator.addItem(restoredProject.getLocalId(), projectName, savedProject); } return projectLocator; } /** * Attempts to match existing contexts against a list of context names. * * @return any matching contexts in a Map, keyed on the context name */ private Map<String,Project> fetchProjectsByName(Collection<String> names) { Map<String,Project> projects = new HashMap<String,Project>(); if (names.size() > 0) { String params = StringUtils.repeat(names.size(), "?", ","); String[] paramValues = names.toArray(new String[0]); Cursor cursor = getContentResolver().query( ProjectProvider.Projects.CONTENT_URI, ProjectProvider.Projects.cFullProjection, ProjectProvider.Projects.NAME + " IN (" + params + ")", paramValues, ProjectProvider.Projects.NAME + " ASC"); while (cursor.moveToNext()) { Project project = mProjectPersister.read(cursor); projects.put(project.getName(), project); } cursor.close(); } return projects; } private void addTasks( List<org.dodgybits.shuffle.dto.ShuffleProtos.Task> protoTasks, EntityDirectory<Context> contextLocator, EntityDirectory<Project> projectLocator, int progressStart, int progressEnd) { TaskProtocolTranslator translator = new TaskProtocolTranslator(contextLocator, projectLocator); // add all tasks back, even if they're duplicates String type = getString(R.string.task_name); List<Task> newTasks = new ArrayList<Task>(); int i = 0; int total = protoTasks.size(); for (org.dodgybits.shuffle.dto.ShuffleProtos.Task protoTask : protoTasks) { Task task = translator.fromMessage(protoTask); newTasks.add(task); Log.d(cTag, "Adding task " + task.getDescription()); String text = getString(R.string.restore_progress, type, task.getDescription()); int percent = calculatePercent(progressStart, progressEnd, ++i, total); publishProgress(Progress.createProgress(percent, text)); } mTaskPersister.bulkInsert(newTasks); } private int calculatePercent(int start, int end, int current, int total) { return start + (end - start) * current / total; } private void reportError(String message) { Log.e(cTag, message); publishProgress(Progress.createErrorProgress(message)); } @Override public void onProgressUpdate (Progress... progresses) { Progress progress = progresses[0]; String details = progress.getDetails(); mProgressBar.setProgress(progress.getProgressPercent()); mProgressText.setText(details); if (progress.isError()) { if (!TextUtils.isEmpty(details)) { AlertUtils.showWarning(PreferencesRestoreBackupActivity.this, details); } Runnable action = progress.getErrorUIAction(); if (action != null) { action.run(); } else { setState(State.ERROR); } } else if (progress.isComplete()) { setState(State.COMPLETE); } } @SuppressWarnings("unused") public void onPostExecute() { mTask = null; } } }