uk.ac.ucl.excites.sapelli.collector.activities.ProjectManagerActivity.java Source code

Java tutorial

Introduction

Here is the source code for uk.ac.ucl.excites.sapelli.collector.activities.ProjectManagerActivity.java

Source

/**
 * Sapelli data collection platform: http://sapelli.org
 * 
 * Copyright 2012-2016 University College London - ExCiteS group
 * 
 * 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 uk.ac.ucl.excites.sapelli.collector.activities;

import java.io.File;
import java.util.Collections;
import java.util.List;

import com.astuetz.PagerSlidingTabStrip;
import com.crashlytics.android.Crashlytics;
import com.ipaulpro.afilechooser.utils.FileUtils;

import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.graphics.Typeface;
import android.net.Uri;
import android.os.Bundle;
import android.support.v4.view.ViewPager;
import android.support.v4.widget.DrawerLayout;
import android.support.v7.app.ActionBar;
import android.support.v7.app.ActionBarDrawerToggle;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.util.Patterns;
import android.util.TypedValue;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.WindowManager;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.TextView;
import uk.ac.ucl.excites.sapelli.collector.BuildConfig;
import uk.ac.ucl.excites.sapelli.collector.CollectorApp;
import uk.ac.ucl.excites.sapelli.collector.CollectorApp.AndroidCollectorClient;
import uk.ac.ucl.excites.sapelli.collector.CollectorClient;
import uk.ac.ucl.excites.sapelli.collector.R;
import uk.ac.ucl.excites.sapelli.collector.db.ProjectStore;
import uk.ac.ucl.excites.sapelli.collector.fragments.ExportFragment;
import uk.ac.ucl.excites.sapelli.collector.fragments.ProjectManagerTabFragment;
import uk.ac.ucl.excites.sapelli.collector.fragments.ProjectManagerTabFragmentPagerAdapter;
import uk.ac.ucl.excites.sapelli.collector.fragments.dialogs.AboutFragment;
import uk.ac.ucl.excites.sapelli.collector.fragments.dialogs.EnterURLFragment;
import uk.ac.ucl.excites.sapelli.collector.fragments.dialogs.ManageReceiversFragment;
import uk.ac.ucl.excites.sapelli.collector.fragments.tabs.MainTabFragment;
import uk.ac.ucl.excites.sapelli.collector.io.FileStorageProvider;
import uk.ac.ucl.excites.sapelli.collector.load.AndroidProjectLoaderStorer;
import uk.ac.ucl.excites.sapelli.collector.load.ProjectLoader;
import uk.ac.ucl.excites.sapelli.collector.load.ProjectLoaderStorer;
import uk.ac.ucl.excites.sapelli.collector.model.Project;
import uk.ac.ucl.excites.sapelli.collector.model.ProjectDescriptor;
import uk.ac.ucl.excites.sapelli.collector.tasks.Backup;
import uk.ac.ucl.excites.sapelli.collector.tasks.ProjectTasks;
import uk.ac.ucl.excites.sapelli.collector.tasks.RecordsTasks;
import uk.ac.ucl.excites.sapelli.collector.util.AsyncDownloader;
import uk.ac.ucl.excites.sapelli.collector.util.AsyncTaskWithWaitingDialog;
import uk.ac.ucl.excites.sapelli.collector.util.DeviceID;
import uk.ac.ucl.excites.sapelli.collector.util.ProjectRunHelpers;
import uk.ac.ucl.excites.sapelli.collector.util.qrcode.IntentIntegrator;
import uk.ac.ucl.excites.sapelli.collector.util.qrcode.IntentResult;
import uk.ac.ucl.excites.sapelli.shared.db.StoreHandle.StoreUser;
import uk.ac.ucl.excites.sapelli.shared.io.FileHelpers;
import uk.ac.ucl.excites.sapelli.shared.io.FileStorageException;
import uk.ac.ucl.excites.sapelli.shared.util.ExceptionHelpers;
import uk.ac.ucl.excites.sapelli.shared.util.StringUtils;
import uk.ac.ucl.excites.sapelli.shared.util.TransactionalStringBuilder;
import uk.ac.ucl.excites.sapelli.shared.util.android.MenuHelpers;
import uk.ac.ucl.excites.sapelli.storage.eximport.ExportResult;
import uk.ac.ucl.excites.sapelli.storage.eximport.csv.CSVRecordsExporter;
import uk.ac.ucl.excites.sapelli.storage.eximport.xml.XMLRecordsExporter;
import uk.ac.ucl.excites.sapelli.storage.model.Record;
import uk.ac.ucl.excites.sapelli.storage.util.UnknownModelException;
import uk.ac.ucl.excites.sapelli.transmission.db.TransmissionStore;

/**
 * @author Julia, Michalis Vitos, mstevens, benelliott
 * 
 */
public class ProjectManagerActivity extends BaseActivity
        implements StoreUser, DeviceID.InitialisationCallback, ProjectLoaderStorer.FileSourceCallback,
        AsyncDownloader.Callback, ListView.OnItemClickListener, RecordsTasks.ImportCallback {

    // STATICS--------------------------------------------------------
    static private final String TAG = "ProjectManagerActivity";

    static protected final String XML_FILE_EXTENSION = "xml";

    static protected final String DEMO_PROJECT = "demo.excites";

    public static final int RETURN_BROWSE_FOR_PROJECT_LOAD = 1;
    public static final int RETURN_BROWSE_FOR_RECORD_IMPORT = 2;

    private static final int PAGER_MARGIN_DIP = 4;

    static public final Typeface FONT_SANS_SERIF_CONDENSED = Typeface.create("sans-serif-condensed",
            Typeface.NORMAL);

    // DYNAMICS-------------------------------------------------------
    private DeviceID deviceID;
    private ProjectStore projectStore;
    private Project currentProject;

    private ArrayAdapter<ProjectDescriptor> projectListAdaptor;

    // UI
    private TextView addProjects;
    private ProjectManagerTabFragmentPagerAdapter pagerAdapter;
    private ViewPager pager;
    private PagerSlidingTabStrip tabs;
    private TextView lblAvailableProjects;
    private ListView projectList;
    private Toolbar mainToolbar;
    private Toolbar drawerToolbar;
    private ActionBarDrawerToggle drawerToggle;
    private DrawerLayout drawerLayout;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        if (getCollectorApp().getBuildInfo().isDemoBuild())
            return;
        //else ...
        // Only if not in demo mode:

        // Hide soft keyboard on create
        getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN);

        // Set-up UI...
        setContentView(R.layout.activity_projectmanager);
        lblAvailableProjects = (TextView) findViewById(R.id.lblAvailableProjects);
        projectList = (ListView) findViewById(R.id.projectList);
        drawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
        mainToolbar = (Toolbar) findViewById(R.id.projMngrToolbar);
        drawerToolbar = (Toolbar) findViewById(R.id.drawerToolbar);
        addProjects = (TextView) findViewById(R.id.addProjects);
        //   Pager & tabs:
        pager = (ViewPager) findViewById(R.id.pager);
        pager.setPageMargin((int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, PAGER_MARGIN_DIP,
                getResources().getDisplayMetrics()));
        pagerAdapter = new ProjectManagerTabFragmentPagerAdapter(this, getSupportFragmentManager());
        pager.setAdapter(pagerAdapter);
        tabs = (PagerSlidingTabStrip) findViewById(R.id.tabs);
        tabs.setViewPager(pager);

        // We use a Toolbar as the ActionBar:
        mainToolbar.setSubtitleTextAppearance(this, R.style.TextAppearance_Sap_ActionBarSubTitleText); // somehow I could specific this at the XML level (see styles.xml)
        setSupportActionBar(mainToolbar);

        // Enable ActionBar icon to behave as action to toggle drawer
        ActionBar actionBar = getSupportActionBar();
        actionBar.setHomeButtonEnabled(true);
        actionBar.setDisplayShowHomeEnabled(true);

        // ActionBarDrawerToggle ties together the the proper interactions
        // between the navigation drawer and the action bar app icon.
        drawerToggle = new ActionBarDrawerToggle(this, drawerLayout, mainToolbar, 0, 0) {
            @Override
            public void onDrawerClosed(View drawerView) {
                super.onDrawerClosed(drawerView);
                supportInvalidateOptionsMenu(); // calls onPrepareOptionsMenu()
            }

            @Override
            public void onDrawerOpened(View drawerView) {
                super.onDrawerOpened(drawerView);
                supportInvalidateOptionsMenu(); // calls onPrepareOptionsMenu()
            }
        };

        // Defer code dependent on restoration of previous instance state.
        // NB: required for the drawer indicator to show up!
        drawerLayout.post(new Runnable() {
            @Override
            public void run() {
                drawerToggle.syncState();
            }
        });

        // Set the drawer toggle as the DrawerListener
        drawerLayout.setDrawerListener(drawerToggle);

        // Set the list's click listener
        projectList.setOnItemClickListener(this);

        // Set the drawerToolbar title & menu:
        drawerToolbar.setTitle(getString(R.string.add_new_project) + ":");
        drawerToolbar.inflateMenu(R.menu.projectload);
        //    Force displaying of icons in overflow menu:
        MenuHelpers.forceMenuIcons(drawerToolbar.getMenu());
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate main actionbar menu:
        getMenuInflater().inflate(R.menu.projectmanager, menu);

        // Inflate & set projectload submenu:
        getMenuInflater().inflate(R.menu.projectload, menu.findItem(R.id.action_load).getSubMenu());

        // Dis/enable remove action:
        menu.findItem(R.id.action_remove).setVisible(currentProject != null);

        // Force displaying of icons in overflow menu:
        MenuHelpers.forceMenuIcons(menu);

        return true;
    }

    @Override
    protected void onResume() {
        super.onResume();

        // Initialise DeviceID:
        DeviceID.Initialise(this, this); // will post a callback upon completion (success/failure)

        // Get ProjectStore instance:
        try {
            projectStore = getCollectorClient().projectStoreHandle.getStore(this);
        } catch (Exception e) {
            Log.e(TAG, getString(R.string.storageAccessFail), e);
            showErrorDialog(getString(R.string.storageAccessFail, ExceptionHelpers.getMessageAndCause(e)), true);
            return;
        }

        // Check & report on database upgrade:
        AndroidCollectorClient client = getCollectorClient();
        if (client.hasDatabaseBeenUpgraded()) {
            List<String> warnings = client.getUpgradeWarnings();
            showWarningDialog(getString(R.string.dbUpgrade, client.getOldDatabaseVersion(),
                    CollectorClient.CURRENT_COLLECTOR_RECORDSTORE_VERSION)
                    + (!warnings.isEmpty() ? "\n\n" + listWarnings(R.string.dbUpgradeWarningsTitle, warnings)
                            : ""));
            client.forgetAboutUpgrade(); // otherwise the dialog box appears for every subsequent onResume()
        }

        // And finally...
        /*if(app.getBuildInfo().isDemoBuild())
           demoMode();
        else*/
        updateProjectList(false);

        // stop tracing
        //Debug.stopMethodTracing();
    }

    /*private void demoMode()
    {
       try
       {
     List<Project> projects = projectStore.retrieveProjects();
     Project p = null;
     if(projects.isEmpty())
     {   // Use /mnt/sdcard/Sapelli/ as the basePath:
        p = new ProjectLoader(fileStorageProvider).load(this.getAssets().open(DEMO_PROJECT, AssetManager.ACCESS_RANDOM));
        projectStore.add(p);
     }
     else
        p = projects.get(0); // Assumption: there is only one stored project in demo mode
     // Run the project
     startActivity(ProjectRunHelpers.getProjectRunIntent(this, p));
       }
       catch(Exception e)
       {
     Log.e(TAG, "Error loading/storing/launching demo project", e);
     showErrorDialog(R.string.demoLoadFail, true);
       }
    }*/

    @Override
    public void initialisationSuccess(DeviceID deviceID) {
        this.deviceID = deviceID;
        if (!BuildConfig.DEBUG) {
            Crashlytics.setLong(CollectorApp.CRASHLYTICS_DEVICE_ID_CRC32, deviceID.getIDAsCRC32Hash());
            Crashlytics.setString(CollectorApp.CRASHLYTICS_DEVICE_ID_MD5, deviceID.getIDAsMD5Hash().toString());
        }
    }

    @Override
    public void initialisationFailure(DeviceID deviceID) {
        deviceID.printInfo();
        showErrorDialog(R.string.noDeviceID, true);
    }

    /**
     * @return the deviceID
     */
    public DeviceID getDeviceID() {
        return deviceID;
    }

    private void updateProjectList(boolean force) {
        if (projectListAdaptor == null || force)
            new RetrieveProjectDescriptorsTask().execute();
    }

    private class RetrieveProjectDescriptorsTask
            extends AsyncTaskWithWaitingDialog<ProjectManagerActivity, Void, List<ProjectDescriptor>> {

        public RetrieveProjectDescriptorsTask() {
            super(ProjectManagerActivity.this, getString(R.string.load_projects));
        }

        @Override
        protected List<ProjectDescriptor> runInBackground(Void... params) {
            return projectStore.retrieveProjectsOrDescriptors();
        }

        @Override
        protected void onPostExecute(List<ProjectDescriptor> result) {
            super.onPostExecute(result); // dismiss dialog

            // Sort alphabetically:
            Collections.sort(result);

            // Update drawer list:
            projectListAdaptor = new ArrayAdapter<ProjectDescriptor>(ProjectManagerActivity.this,
                    R.layout.project_item, result);
            projectList.setAdapter(projectListAdaptor);

            // Set label above list:
            lblAvailableProjects.setText(result.isEmpty() ? R.string.no_projects : R.string.switch_project);

            // Determine which project to switch to:
            ProjectDescriptor switchTo = null; // if this stays null we will go into "no-project mode"
            if (!result.isEmpty()) {
                // In case the currentProject and the previously active one are no longer
                //   available (see below) we switch to the first project in the list:
                switchTo = result.get(0);

                // If there is a currently active project...
                if (currentProject != null) { // ... look for it in the result list:
                    for (int i = 0; i < result.size(); i++)
                        if (result.get(i).equalDescription(currentProject)) { // (Re)select current project:
                            switchTo = result.get(i);
                            break;
                        }
                    // if we get here the currentProject is no longer available
                }
                // If there is a previously active project...
                else if (getPreferences().getActiveProjectSignature() != null) {
                    // ... look for it in the result list:
                    String prevActiveProjectSign = getPreferences().getActiveProjectSignature();
                    for (int i = 0; i < result.size(); i++)
                        if (result.get(i).getSignatureString().equals(prevActiveProjectSign)) { // (Re)select previously active project:
                            switchTo = result.get(i);
                            break;
                        }
                    // if we get here the previously active project is no longer available
                }
            }

            // Switch to the right project:
            switchToProject(switchTo);
        }

    }

    /**
     * Called when user selected a Project(Descriptor) from the Drawer
     * 
     * @param parent
     * @param view
     * @param position
     * @param id
     */
    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        // Switch project:
        switchToProject(projectListAdaptor.getItem(position));

        // Close drawer:
        closeDrawer(null);
    }

    private void switchToProject(final ProjectDescriptor projDescr) {
        // Select in list:
        if (projDescr != null && projectListAdaptor.getPosition(projDescr) != -1)
            projectList.setItemChecked(projectListAdaptor.getPosition(projDescr), true);

        // If we got null or a Project object (instead of a plain ProjectDesciptor):
        if (projDescr == null || projDescr instanceof Project)
            setCurrentProject((Project) projDescr);

        // If we got a ProjectDesciptor:
        else if (projDescr.equals(currentProject))
            return; // this is already the current project

        else // Parse project:
            new ProjectTasks.ReloadProjectTask(this, projectStore, new ProjectTasks.ReloadProjectCallback() {
                @Override
                public void projectReloaded(Project project) {
                    // Set as current project:
                    setCurrentProject(project);

                    // Refresh drawer list (to use project instead of projectDescriptor instance):
                    updateProjectList(true);
                }

                @Override
                public void projectReloadFailure() { // Project could not be loaded (likely the XML file is no longer there), make sure it is fully removed:
                    removeProject(projDescr); // will also update the project list
                }
            }).execute(projDescr);
    }

    /**
     * @param project - may be null
     */
    private void setCurrentProject(Project project) {
        if (currentProject == project && currentProject != null)
            return; // this is already the current project
        //else...
        currentProject = project;

        // Remember current project:
        getPreferences().setActiveProjectSignature(currentProject);

        // Update UI:
        if (currentProject != null) {
            getSupportActionBar().setTitle(currentProject.getName());
            getSupportActionBar().setSubtitle(currentProject.getVariantVersionString());
            tabs.setVisibility(View.VISIBLE);
            pager.setVisibility(View.VISIBLE);
            addProjects.setVisibility(View.GONE);
        } else {
            getSupportActionBar().setTitle(R.string.sapelli);
            getSupportActionBar().setSubtitle(R.string.collector);
            tabs.setVisibility(View.GONE);
            pager.setVisibility(View.GONE);
            addProjects.setVisibility(View.VISIBLE);
        }
        //    Refresh tabs:
        pagerAdapter.refresh();
        //    Refresh menus:
        supportInvalidateOptionsMenu();
    }

    public Project getCurrentProject(boolean errorIfNull) {
        if (currentProject == null) {
            if (errorIfNull)
                showErrorDialog(R.string.selectProject, false);
            return null;
        }
        return currentProject;
    }

    /**
     * Browse for project file to load project from.
     * 
     * @param menuItem
     */
    public void browse(MenuItem menuItem) {
        // Open file picker to let user chose a project file to load:
        try {
            // Use the GET_CONTENT intent from the utility class
            Intent target = FileUtils.createGetContentIntent();

            // Create the chooser Intent
            Intent intent = Intent.createChooser(target, getString(R.string.chooseSapelliFile));

            // Start file picker activity:
            startActivityForResult(intent, RETURN_BROWSE_FOR_PROJECT_LOAD);
        } catch (ActivityNotFoundException e) {
        }

        // Close drawer:
        closeDrawer(null); // won't do anything if it is not open
    }

    public void enterURL(MenuItem menuItem) {
        closeDrawer(null);
        new EnterURLFragment().show(getSupportFragmentManager(), getString(R.string.enter_url));
    }

    public void switchToTab(int index) {
        pager.setCurrentItem(index);
    }

    @Override
    protected void onDestroy() {
        Log.d(TAG, "Destroying...");

        // signal that the activity no longer needs the stores (projecStore and its internally use transmissionStore instance):
        getCollectorApp().collectorClient.projectStoreHandle.doneUsing(this);

        // super:
        super.onDestroy();
    }

    public void closeDrawer(View view) {
        drawerLayout.closeDrawers();
    }

    /**
     * @param view
     */
    public void openAboutDialog(View view) {
        openAboutDialog();
        closeDrawer(null);
    }

    /**
     * @param item
     * @return
     */
    public boolean openAboutDialog(MenuItem item) {
        openAboutDialog();
        return true;
    }

    private void openAboutDialog() {
        new AboutFragment().show(getSupportFragmentManager(), getString(R.string.about));
    }

    /**
     * @param item
     * @return
     */
    public boolean manageReceivers(MenuItem item) {
        ManageReceiversFragment.ShowDialog(this, null);
        return true;
    }

    /**
     * Export all projects
     * 
     * @param item
     * @return
     */
    public boolean exportRecords(MenuItem item) {
        ExportFragment.ShowExportAllDialog(this, true);
        return true;
    }

    /**
     * To be called upon completion of a record/data export (and possibly delete) operation
     * 
     * @param result
     * @param exportedDataDeleted
     */
    public void onDataExportDone(ExportResult result, boolean exportedDataDeleted) {
        if (exportedDataDeleted)
            onDataChanged();
    }

    /**
     * To be called upon completion of a record/data delete operation
     */
    public void onDataChanged() {
        // Refresh all tabs:
        refreshAllTabs();
        // Alternative (refresh only main tab):
        //refreshTap(MainTabFragment.class);
    }

    public void refreshAllTabs() {
        pagerAdapter.refresh();
    }

    public void refreshTab(Class<? extends ProjectManagerTabFragment> tabClass) {
        try {
            pagerAdapter.getTab(tabClass).refresh();
        } catch (Exception e) {
            Log.e(TAG, "Error upon refreshing tab: " + tabClass != null ? tabClass.getSimpleName() : "null", e);
        }
    }

    public boolean importRecords(MenuItem item) {
        // Use the GET_CONTENT intent from the utility class
        Intent target = FileUtils.createGetContentIntent();
        // Create the chooser Intent
        Intent intent = Intent.createChooser(target, "Choose an XML or CSV file");
        try {
            startActivityForResult(intent, RETURN_BROWSE_FOR_RECORD_IMPORT);
        } catch (ActivityNotFoundException e) {
        }

        return true;
    }

    /**
     * Create Sapelli back-up package
     * 
     * @param item
     * @return
     */
    public boolean backupSapelli(MenuItem item) {
        runFileStorageTask(new FileStorageTask() {
            @Override
            public void run(FileStorageProvider fsp) {
                Backup.Run(ProjectManagerActivity.this, fsp);
            }
        });
        return true;
    }

    public void runProject(View view) {
        Project p = getCurrentProject(true);
        if (p != null)
            startActivity(ProjectRunHelpers.getProjectRunIntent(this, p));
    }

    public void loadProject(String path) {
        // Check path:
        if (path == null || path.trim().isEmpty())
            return;

        final String location = path.trim();
        if (Patterns.WEB_URL.matcher(location).matches()) { // Location is a (remote) URL: download Sapelli file:
            runFileStorageTask(new FileStorageTask() {
                @Override
                public void run(FileStorageProvider fsp) {
                    try { // loading & storing of the project will happen upon successful download (via callback)
                        AsyncDownloader.Download(ProjectManagerActivity.this,
                                fsp.getSapelliDownloadsFolder() /*throws FileStorageException*/, location,
                                ProjectManagerActivity.this);
                    } catch (FileStorageException fse) {
                        showErrorDialog(getString(R.string.storageError, fse.getMessage()));
                    }
                }
            });
        } else
            // Load project from local file:
            loadProject(new File(location), null, true);
    }

    @Override
    public void downloadSuccess(String downloadUrl, File downloadedFile) {
        loadProject(downloadedFile, downloadUrl, false); // don't check extension for downloaded files
    }

    @Override
    public void downloadFailure(String downloadUrl, Exception cause) {
        showErrorDialog(
                cause != null ? getString(R.string.downloadErrorWithCause, ExceptionHelpers.getMessage(cause))
                        : getString(R.string.downloadError),
                false);
    }

    /**
     * @param localFile
     * @param sourceURI - may be null
     * @param checkExtension
     */
    public void loadProject(final File localFile, final String sourceURI, boolean checkExtension) {
        if (!FileHelpers.isReadableFile(localFile)) {
            showErrorDialog(R.string.invalidFile);
        } else if (checkExtension && XML_FILE_EXTENSION.equalsIgnoreCase(FileHelpers.getFileExtension(localFile))) {
            showErrorDialog(R.string.noBareXMLProjects);
        } else if (checkExtension && !ProjectLoader.HasSapelliFileExtension(localFile)) { // Warn about extension:
            showOKCancelDialog(R.string.warning,
                    getString(R.string.unsupportedExtension, FileHelpers.getFileExtension(localFile),
                            StringUtils.join(ProjectLoader.SAPELLI_FILE_EXTENSIONS, ", ")),
                    false, new Runnable() {
                        @Override
                        public void run() {
                            loadProject(localFile, sourceURI, false); // try loading anyway
                        }
                    }, false);
        } else { // Actually load & store the project:
            runFileStorageTask(new FileStorageTask() {
                @Override
                public void run(FileStorageProvider fsp) {
                    new AndroidProjectLoaderStorer(ProjectManagerActivity.this, fsp, projectStore).loadAndStore(
                            localFile, sourceURI != null ? sourceURI : Uri.fromFile(localFile).toString(),
                            ProjectManagerActivity.this);
                }
            });
        }

    }

    @Override
    public void projectLoadStoreSuccess(File sapelliFile, String sourceURI, Project project,
            List<String> warnings) {
        // Deal with downloaded file...
        if (Patterns.WEB_URL.matcher(sourceURI).matches())
            // Change temporary name to proper one:
            sapelliFile.renameTo(new File(sapelliFile.getParentFile().getAbsolutePath() + File.separator
                    + FileHelpers.makeValidFileName(project.toString(false) + ".sapelli")));

        // Warnings...
        TransactionalStringBuilder bldr = new TransactionalStringBuilder("\n");
        //   Parser/loader warnings: 
        if (!warnings.isEmpty())
            bldr.append(listWarnings(R.string.projectLoadingWarnings, warnings));
        //   Check file dependencies:
        List<String> missingFiles = project.getMissingFilesRelativePaths(getFileStorageProvider());
        if (!missingFiles.isEmpty()) {
            bldr.append(getString(R.string.missingFiles) + ":");
            for (String missingFile : missingFiles)
                bldr.append(" - " + missingFile);
        }
        //   Show warnings dialog:
        if (!bldr.isEmpty())
            showWarningDialog(bldr.toString()); // no need to worry about message not fitting the dialog, it will have a scrollbar when necessary 

        // Select project:
        setCurrentProject(project);

        // Update project list:
        updateProjectList(true);
    }

    @Override
    public void projectLoadStoreFailure(File sapelliFile, String sourceURI, Exception cause) {
        // Deal with downloaded file...
        if (Patterns.WEB_URL.matcher(sourceURI).matches())
            org.apache.commons.io.FileUtils.deleteQuietly(sapelliFile);
        // Report problem:
        Log.e(TAG, "Could not load/store Sapelli file", cause);
        showErrorDialog(getString(R.string.sapelliFileLoadFailure,
                (Patterns.WEB_URL.matcher(sourceURI).matches() ? sapelliFile.getAbsolutePath() : sourceURI),
                ExceptionHelpers.getMessageAndCause(cause)), false);
    }

    public void scanQR(MenuItem item) {
        // Start the Intent to Scan a QR code
        new IntentIntegrator(this).initiateScan(IntentIntegrator.QR_CODE_TYPES);

        closeDrawer(null);
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
        super.onActivityResult(requestCode, resultCode, intent);

        if (resultCode != Activity.RESULT_OK) {
            //Log.d(TAG, "onActivityResult() called with non-OK resultCode (requestCode: " + requestCode + "; resultCode: " + resultCode + ")!");
            return;
        }
        //else...
        Uri uri = intent.getData();
        boolean skipRefreshOnMainTabResume = false;
        switch (requestCode) {
        // File browse dialog for project loading:
        case RETURN_BROWSE_FOR_PROJECT_LOAD:
            skipRefreshOnMainTabResume = true; // see explanation below
            // Get the File path from the Uri
            loadProject(FileUtils.getPath(this, uri));
            break;

        // File browse dialog for record importing:
        case RETURN_BROWSE_FOR_RECORD_IMPORT:
            skipRefreshOnMainTabResume = true; // see explanation below
            importFrom(FileUtils.getFile(this, uri));
            break;

        // QR Reader
        case IntentIntegrator.REQUEST_CODE:
            IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent);
            if (scanResult != null) {
                skipRefreshOnMainTabResume = true; // see explanation below
                loadProject(scanResult.getContents());
            }
            break;
        }

        /* Note: after onActivityResult() the activity's and each visible tab's onResume() method will
         * be called. In the case of the MainTabFragment this would normally trigger a refreshing of
         * the project data stats, which makes no sense if it happens before/during project loading or
         * data importing (or twice). Hence we tell the MainTabFragment no to refresh itself upon the
         * next onResume() call: */
        if (skipRefreshOnMainTabResume)
            try {
                pagerAdapter.getTab(MainTabFragment.class).setSkipRefreshOnNextResume(true);
            } catch (Exception ignore) {
            }
    }

    public void importFrom(File exportedDataFile) {
        try {
            String extension = FileHelpers.getFileExtension(exportedDataFile).toLowerCase();
            switch (extension) {
            case XMLRecordsExporter.FILE_EXTENSION:
                new RecordsTasks.XMLImportTask(this, this).execute(exportedDataFile);
                break;
            case CSVRecordsExporter.FILE_EXTENSION:
                new RecordsTasks.CSVImportTask(this, this).execute(exportedDataFile);
                break;
            default:
                showErrorDialog(getString(R.string.unknownExportExtension, extension), false);
            }
        } catch (Exception e) {
            showErrorDialog(getString(R.string.importStartFailed, ExceptionHelpers.getMessageAndCause(e)), false);
        }
    }

    @SuppressWarnings("unchecked")
    @Override
    public void importSuccess(List<Record> records, List<String> warnings) {
        // Show parser warnings if needed:
        if (!warnings.isEmpty())
            showWarningDialog(listWarnings(R.string.parsingWarnings, warnings));
        // Store the records:
        new RecordsTasks.StoreTask(this, new RecordsTasks.StoreCallback() {
            @Override
            public void storeSuccess(int newRecords, int updatedRecords, int skippedDuplicates) {
                showInfoDialog(getString(R.string.importSuccess, newRecords, updatedRecords, skippedDuplicates));
                onDataChanged();
            }

            @Override
            public void storeFailure(Exception reason) {
                showErrorDialog(getString(R.string.importStoreFailed, reason.getMessage()), false);
                onDataChanged();
            }
        }).execute(records);
    }

    @Override
    public void importFailure(Exception reason) {
        if (reason instanceof UnknownModelException) {
            UnknownModelException ume = (UnknownModelException) reason;
            showErrorDialog(getString(R.string.importFailed,
                    (ume.getSchemaName() != null ? getString(R.string.schemaFormNotFound, ume.getSchemaName())
                            : ume.getMessage())),
                    false);
        } else
            showErrorDialog(getString(R.string.importFailed, reason.getMessage()), false);
    }

    /**
     * Menu action which removes project if user confirms this is what he/she wants.
     * 
     * @param view
     */
    public void removeProject(MenuItem item) {
        removeProject(getCurrentProject(false), true);
    }

    /**
     * Removes the given project, optionally after asking for confirmation.
     * 
     * @param projDesc
     * @param askConfirmation
     */
    public void removeProject(final ProjectDescriptor projDesc, boolean askConfirmation) {
        if (projDesc == null)
            return;
        if (askConfirmation)
            showYesNoDialog(R.string.remove_project,
                    getString(R.string.removeProjectConfirm, projDesc.toString(false)),
                    R.drawable.ic_delete_black_36dp, new Runnable() {
                        @Override
                        public void run() {
                            removeProject(projDesc);
                        }
                    }, false, null, false);
        else
            removeProject(projDesc); // remove straight away
    }

    /**
     * Removes the given project. Does *not* ask for confirmation.
     * Use with care!
     * 
     * @param projDesc
     */
    public void removeProject(final ProjectDescriptor projDesc) {
        if (projDesc != null)
            new ProjectTasks.RemoveProjectTask(ProjectManagerActivity.this, projectStore,
                    new ProjectTasks.RemoveProjectCallback() {
                        @Override
                        public void projectRemoved() {
                            updateProjectList(true); // Refresh list
                        }
                    }).execute(projDesc);
    }

    /**
     * @return the projectStore
     */
    public ProjectStore getProjectStore() {
        return projectStore;
    }

    /**
     * @return the transmissionStore
     */
    public TransmissionStore getTransmissionStore() {
        return projectStore.getTransmissionStore(); // use the transmissionStore instance used by the ProjectStore
    }

    private String listWarnings(int titleStringId, List<String> warnings) {
        if (warnings.isEmpty())
            return null;
        //else:
        TransactionalStringBuilder bldr = new TransactionalStringBuilder("\n");
        bldr.append(getString(titleStringId) + ":");
        for (String warning : warnings)
            bldr.append(" - " + warning);
        return bldr.toString();
    }

}